强曰为道
与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

Aspell 拼写检查完全教程 / 第8章 编辑器与 CI 集成

第 8 章:编辑器与 CI 集成

本章介绍如何将 Aspell 集成到日常编辑器(Emacs、Vim、VS Code)和 CI/CD 流水线中,实现自动化的拼写检查。


8.1 集成方案总览

场景工具集成方式
Emacsispell.el内置 Aspell 后端
Vimvim-mundo / aspell.vim外部命令调用
VS CodeCode Spell Checker扩展(非 Aspell 直接集成)
CI/CDGitHub Actions / GitLab CI脚本调用 aspell list
Git Hookspre-commit提交前检查
Makefilemake spell构建流程集成

8.2 Emacs 集成

8.2.1 基本配置

Emacs 内置的 ispell.el 模块原生支持 Aspell:

;; ~/.emacs.d/init.el 或 ~/.emacs

;; 使用 aspell 替代 ispell
(setq ispell-program-name "aspell")

;; 设置 aspell 参数
(setq ispell-extra-args '("--sug-mode=ultra"))

;; 设置默认语言
(setq ispell-dictionary "en_US")

;; 忽略大写单词
(setq ispell-silently-savep t)

8.2.2 常用命令

快捷键命令说明
M-$ispell-word检查光标处的单词
M-x ispell-bufferispell-buffer检查整个缓冲区
M-x ispell-regionispell-region检查选中区域
M-x ispell-change-dictionaryispell-change-dictionary切换词典
M-x ispell-kill-ispellispell-kill-ispell终止 Aspell 进程
M-x ispell-add-word-to-personal-dict添加到个人词典添加当前单词

8.2.3 交互操作

ispell-buffer 发现拼写错误时,会显示建议列表:

& teh 3 0: the, tea, tee

SPC: skip    r: replace    R: replace all
a: accept    A: accept all i: insert
l: lookup    x: exit       q: quit
按键操作
Space跳过当前单词
r替换为选中的建议
R替换文件中所有出现
a接受(仅本次)
A接受(整个缓冲区)
i手动输入替换词
l在词典中查找
x退出并保存
q退出不保存

8.2.4 按模式配置

;; 对 TeX/LaTeX 文件使用 tex 模式
(add-hook 'LaTeX-mode-hook
          (lambda ()
            (setq ispell-extra-args '("--mode=tex" "--sug-mode=ultra"))))

;; 对 HTML 文件使用 html 模式
(add-hook 'html-mode-hook
          (lambda ()
            (setq ispell-extra-args '("--mode=html"))))

;; 对 Markdown 文件使用 markdown 模式
(add-hook 'markdown-mode-hook
          (lambda ()
            (setq ispell-extra-args '("--mode=markdown"))))

;; 对 Org 模式
(add-hook 'org-mode-hook
          (lambda ()
            (setq ispell-extra-args '("--mode=markdown"))))

8.2.5 使用 Flyspell 实时检查

Flyspell 是 Emacs 的实时拼写检查模式:

;; 全局启用 flyspell
;;(global-flyspell-mode)  ;; 可能影响性能

;; 对文本模式启用 flyspell
(add-hook 'text-mode-hook 'flyspell-mode)

;; 对编程模式检查注释和字符串
(add-hook 'prog-mode-hook 'flyspell-prog-mode)

;; flyspell 快捷键绑定
(define-key flyspell-mode-map (kbd "C-;") 'flyspell-auto-correct-previous-word)

;; 延迟检查(提高大文件性能)
(setq flyspell-issue-message-flag nil)
(setq flyspell-large-region 10000)

8.2.6 自定义个人词典路径

;; 使用项目级个人词典
(setq ispell-personal-dictionary
      (expand-file-name ".aspell.pws" (projectile-project-root)))

;; 或根据 buffer 自动选择
(defun my/aspell-personal-dict ()
  "根据项目自动设置个人词典路径"
  (let ((project-root (or (ignore-errors (projectile-project-root))
                          default-directory)))
    (setq ispell-personal-dictionary
          (expand-file-name ".aspell.pws" project-root))))

(add-hook 'find-file-hook 'my/aspell-personal-dict)

8.3 Vim 集成

8.3.1 基本配置

Vim 可以通过内置的 spell 功能或外部 Aspell 调用进行拼写检查。

使用 Vim 内置拼写检查

" ~/.vimrc

" 启用拼写检查
set spell
set spelllang=en_us

" 拼写检查快捷键
nnoremap <F5> :setlocal spell!<CR>
nnoremap <F6> ]s                    " 下一个拼写错误
nnoremap <F7> [s                    " 上一个拼写错误
nnoremap <F8> z=                    " 显示建议
nnoremap <leader>sa zg              " 添加到好词列表
nnoremap <leader>sd zug             " 从好词列表删除

使用外部 Aspell

" 通过外部命令调用 aspell
function! AspellCheck()
    let l:filename = expand('%')
    execute '!aspell check --mode=markdown --personal=' . shellescape(g:aspell_personal) l:filename
endfunction

" 设置个人词典
let g:aspell_personal = '~/.aspell.en_US.pws'

" 映射快捷键
nnoremap <leader>sc :call AspellCheck()<CR>

8.3.2 Vim 拼写检查命令

命令说明
:set spell启用拼写检查
:set nospell禁用拼写检查
:set spelllang=en_us设置语言
]s跳到下一个错误
[s跳到上一个错误
z=显示建议列表
zg将单词加入好词列表
zug撤销加入好词列表
zw将单词标记为坏词
:spellr重复上次替换

8.3.3 vim-aspell 插件

" 使用 vim-aspell 插件(如果安装)
" 安装:
" Plug 'vim-scripts/vim-aspell'

let g:aspell_lang = "en_US"
let g:aspell_sug_mode = "ultra"

" 检查当前文件
nnoremap <leader>sc :AspellCheck<CR>
" 切换实时检查
nnoremap <leader>st :AspellToggle<CR>

8.3.4 Neovim 配置

-- ~/.config/nvim/init.lua (Neovim)

-- 基本拼写设置
vim.opt.spell = true
vim.opt.spelllang = { "en_us" }

-- 使用 Treesitter 只检查注释和字符串(编程模式)
-- 需要 nvim-treesitter 插件
vim.api.nvim_create_autocmd("FileType", {
    pattern = { "python", "javascript", "lua" },
    callback = function()
        vim.opt_local.spell = false  -- 编程模式默认关闭
    end,
})

-- 对文本文件启用
vim.api.nvim_create_autocmd("FileType", {
    pattern = { "markdown", "text", "tex", "html" },
    callback = function()
        vim.opt_local.spell = true
    end,
})

8.4 VS Code 集成

8.4.1 Code Spell Checker 扩展

VS Code 主要通过 Code Spell Checker 扩展(非 Aspell 直接集成)进行拼写检查:

// .vscode/settings.json
{
    // 启用拼写检查
    "cSpell.enabled": true,

    // 设置语言
    "cSpell.language": "en",

    // 自定义词典
    "cSpell.customDictionaries": {
        "project": {
            "name": "project",
            "path": "${workspaceFolder}/.cspell/project-words.txt",
            "addWords": true
        }
    },

    // 忽略路径
    "cSpell.ignorePaths": [
        "node_modules",
        "public",
        "*.min.js"
    ],

    // 忽略大写单词
    "cSpell.flagWords": [],

    // 文件类型检查
    "cSpell.enableFiletypes": [
        "markdown",
        "plaintext",
        "latex"
    ]
}

8.4.2 项目级词典

# 创建项目词典目录
mkdir -p .cspell

# 项目词典文件
cat > .cspell/project-words.txt << 'EOF'
Aspell
API
Docker
Kubernetes
EOF

# 添加到版本控制
git add .cspell/

8.4.3 与 Aspell 词典同步

#!/usr/bin/env python3
"""sync_cspell_aspell.py — 同步 VS Code 词典和 Aspell 个人词典"""

import os

def sync_dictionaries(aspell_dict: str, cspell_dict: str):
    """将 Aspell 词典同步到 VS Code 词典"""

    # 读取 Aspell 词典
    with open(aspell_dict) as f:
        aspell_words = set()
        for line in f:
            line = line.strip()
            if line and not line.startswith('#') and not line.startswith('personal_ws'):
                aspell_words.add(line)

    # 读取 VS Code 词典
    with open(cspell_dict) as f:
        cspell_words = set(line.strip() for line in f if line.strip())

    # 合并
    merged = sorted(aspell_words | cspell_words)

    # 写回 VS Code 词典
    with open(cspell_dict, 'w') as f:
        for word in merged:
            f.write(word + '\n')

    print(f"同步完成: {len(merged)} 个单词")

sync_dictionaries(
    os.path.expanduser('~/.aspell.en_US.pws'),
    './.cspell/project-words.txt'
)

8.5 CI/CD 集成

8.5.1 GitHub Actions

# .github/workflows/spellcheck.yml
name: Spellcheck

on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]

jobs:
  spellcheck:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Install Aspell
        run: |
          sudo apt-get update
          sudo apt-get install -y aspell aspell-en

      - name: Check spelling
        run: |
          # 使用项目词典检查所有 Markdown 文件
          EXIT_CODE=0
          while IFS= read -r file; do
            echo "检查: $file"
            ERRORS=$(aspell list \
              --mode=markdown \
              --personal=.aspell/project.pws \
              --extra-dicts=.aspell/tech.pws \
              < "$file" 2>/dev/null)

            if [ -n "$ERRORS" ]; then
              echo "  拼写错误:"
              echo "$ERRORS" | sort -u | sed 's/^/    /'
              EXIT_CODE=1
            fi
          done < <(find docs/ -name "*.md" -type f)

          exit $EXIT_CODE

      - name: Comment on PR (if errors found)
        if: failure() && github.event_name == 'pull_request'
        uses: actions/github-script@v7
        with:
          script: |
            github.rest.issues.createComment({
              owner: context.repo.owner,
              repo: context.repo.repo,
              issue_number: context.issue.number,
              body: '⚠️ 拼写检查失败。请检查文档中的拼写错误,或将正确的专有名词加入 `.aspell/project.pws`。'
            })

8.5.2 GitLab CI

# .gitlab-ci.yml
stages:
  - lint

spellcheck:
  stage: lint
  image: ubuntu:24.04
  before_script:
    - apt-get update
    - apt-get install -y aspell aspell-en
  script:
    - |
      EXIT_CODE=0
      for file in $(find docs/ -name "*.md" -type f); do
        echo "Checking: $file"
        ERRORS=$(aspell list \
          --mode=markdown \
          --personal=.aspell/project.pws \
          < "$file" 2>/dev/null)
        if [ -n "$ERRORS" ]; then
          echo "  Spelling errors:"
          echo "$ERRORS" | sort -u | sed 's/^/    /'
          EXIT_CODE=1
        fi
      done
      exit $EXIT_CODE
  only:
    - merge_requests
    - main

8.5.3 自定义 CI 动作

# .github/actions/spellcheck/action.yml
name: 'Aspell Spellcheck'
description: 'Run Aspell spell check on documentation files'
inputs:
  personal-dict:
    description: 'Path to personal dictionary'
    required: false
    default: '.aspell/project.pws'
  file-pattern:
    description: 'Glob pattern for files to check'
    required: false
    default: '**/*.md'
  language:
    description: 'Language code'
    required: false
    default: 'en'
runs:
  using: 'composite'
  steps:
    - name: Install Aspell
      shell: bash
      run: |
        sudo apt-get update
        sudo apt-get install -y aspell aspell-${{ inputs.language }}

    - name: Run spell check
      shell: bash
      run: |
        EXIT_CODE=0
        while IFS= read -r file; do
          echo "::group::Checking $file"
          ERRORS=$(aspell list \
            --mode=markdown \
            --personal=${{ inputs.personal-dict }} \
            < "$file" 2>/dev/null)

          if [ -n "$ERRORS" ]; then
            echo "::error file=$file::Spelling errors found"
            echo "$ERRORS" | sort -u
            EXIT_CODE=1
          else
            echo "✓ No errors"
          fi
          echo "::endgroup::"
        done < <(find . -name "${{ inputs.file-pattern }}" -type f | sed 's|^\./||')

        exit $EXIT_CODE

使用自定义动作:

# .github/workflows/docs.yml
name: Documentation

on: [push, pull_request]

jobs:
  spellcheck:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: ./.github/actions/spellcheck
        with:
          personal-dict: '.aspell/project.pws'
          file-pattern: '**/*.md'

8.6 Git Hooks 集成

8.6.1 pre-commit 框架

# .pre-commit-config.yaml
repos:
  - repo: local
    hooks:
      - id: aspell-check
        name: Aspell spell check
        entry: bash -c 'aspell list --mode=markdown --personal=.aspell/project.pws < "$@"'
        language: system
        files: '\.md$'
        pass_filenames: true

8.6.2 手动 Git Hook

#!/bin/bash
# .git/hooks/pre-commit — 提交前拼写检查

echo "运行拼写检查..."

# 获取即将提交的 .md 文件
MD_FILES=$(git diff --cached --name-only --diff-filter=ACM | grep '\.md$')

if [ -z "$MD_FILES" ]; then
    echo "没有需要检查的 .md 文件"
    exit 0
fi

EXIT_CODE=0
for file in $MD_FILES; do
    if [ -f "$file" ]; then
        # 获取暂存区的文件内容
        CONTENT=$(git show ":$file")
        ERRORS=$(echo "$CONTENT" | aspell list \
            --mode=markdown \
            --personal=.aspell/project.pws 2>/dev/null)

        if [ -n "$ERRORS" ]; then
            echo "拼写错误: $file"
            echo "$ERRORS" | sort -u | sed 's/^/  /'
            EXIT_CODE=1
        fi
    fi
done

if [ $EXIT_CODE -ne 0 ]; then
    echo ""
    echo "拼写检查失败。使用以下命令跳过检查:"
    echo "  git commit --no-verify"
    echo ""
    echo "或将正确的专有名词加入 .aspell/project.pws"
    exit 1
fi

echo "拼写检查通过"
exit 0
# 安装 hook
chmod +x .git/hooks/pre-commit

# 测试
git commit -m "test: checking spellcheck hook"

8.7 Makefile 集成

# Makefile — 项目构建含拼写检查

.PHONY: spell spell-fix

# 词典路径
ASPELL_DICT = .aspell/project.pws
ASPELL_EXTRA = .aspell/tech.pws

# 拼写检查
spell:
	@echo "=== 拼写检查 ==="
	@EXIT_CODE=0; \
	for file in $$(find . -name "*.md" -not -path "./node_modules/*"); do \
		ERRORS=$$(aspell list \
			--mode=markdown \
			--personal=$(ASPELL_DICT) \
			--extra-dicts=$(ASPELL_EXTRA) \
			< "$$file" 2>/dev/null); \
		if [ -n "$$ERRORS" ]; then \
			echo "✗ $$file"; \
			echo "$$ERRORS" | sort -u | sed 's/^/  /'; \
			EXIT_CODE=1; \
		else \
			echo "✓ $$file"; \
		fi; \
	done; \
	exit $$EXIT_CODE

# 交互式修复
spell-fix:
	@for file in $$(find . -name "*.md" -not -path "./node_modules/*"); do \
		echo "检查: $$file"; \
		aspell check --mode=markdown --personal=$(ASPELL_DICT) "$$file"; \
	done

# 添加单词到项目词典
spell-add:
	@echo "添加单词到 $(ASPELL_DICT)"
	@read -p "单词: " word; \
	echo "$$word" >> $(ASPELL_DICT); \
	echo "已添加: $$word"

# 统计错误
spell-count:
	@for file in $$(find . -name "*.md" -not -path "./node_modules/*"); do \
		COUNT=$$(aspell list < "$$file" 2>/dev/null | wc -l); \
		[ "$$COUNT" -gt 0 ] && echo "$$COUNT $$file"; \
	done | sort -rn
# 使用
make spell        # 检查所有文件
make spell-fix    # 交互式修复
make spell-add    # 添加单词
make spell-count  # 统计错误数

8.8 自动化检查脚本

8.8.1 综合检查脚本

#!/bin/bash
# spellcheck.sh — 综合拼写检查脚本

set -euo pipefail

# 配置
LANG_CODE="en"
PERSONAL_DICT=".aspell/project.pws"
EXTRA_DICTS=".aspell/tech.pws"
FILE_PATTERN="**/*.md"
EXIT_CODE=0

# 颜色
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[0;33m'
NC='\033[0m' # No Color

# 函数:检查单个文件
check_file() {
    local file="$1"
    local errors

    errors=$(aspell list \
        --mode=markdown \
        --personal="$PERSONAL_DICT" \
        --extra-dicts="$EXTRA_DICTS" \
        < "$file" 2>/dev/null)

    if [ -n "$errors" ]; then
        echo -e "${RED}${NC} $file"
        echo "$errors" | sort -u | while read -r word; do
            # 找到行号
            lines=$(grep -n -i -w "$word" "$file" | cut -d: -f1 | tr '\n' ',' | sed 's/,$//')
            echo -e "  ${YELLOW}$word${NC} (行: $lines)"
        done
        return 1
    else
        echo -e "${GREEN}${NC} $file"
        return 0
    fi
}

# 主逻辑
echo "=== Aspell 拼写检查 ==="
echo "语言: $LANG_CODE"
echo "个人词典: $PERSONAL_DICT"
echo ""

# 检查 aspell 是否可用
if ! command -v aspell &> /dev/null; then
    echo -e "${RED}错误: aspell 未找到${NC}"
    exit 1
fi

# 检查词典是否存在
if [ ! -f "$PERSONAL_DICT" ]; then
    echo -e "${YELLOW}警告: 个人词典不存在,创建空词典${NC}"
    mkdir -p "$(dirname "$PERSONAL_DICT")"
    echo "personal_ws-1.1 $LANG_CODE 0" > "$PERSONAL_DICT"
fi

# 遍历文件
TOTAL=0
ERRORS=0

while IFS= read -r -d '' file; do
    ((TOTAL++))
    if ! check_file "$file"; then
        ((ERRORS++))
    fi
done < <(find . -name "*.md" -not -path "./node_modules/*" -print0)

echo ""
echo "=== 结果 ==="
echo "检查文件: $TOTAL"
echo "有错误: $ERRORS"

if [ $ERRORS -gt 0 ]; then
    echo ""
    echo "提示:将正确的专有名词加入 $PERSONAL_DICT"
    exit 1
fi

exit 0

8.8.2 增量检查(只检查变更文件)

#!/bin/bash
# spellcheck-incremental.sh — 只检查 Git 变更的文件

CHANGED_FILES=$(git diff --name-only HEAD~1 -- '*.md')

if [ -z "$CHANGED_FILES" ]; then
    echo "没有变更的 .md 文件"
    exit 0
fi

EXIT_CODE=0
for file in $CHANGED_FILES; do
    if [ -f "$file" ]; then
        ERRORS=$(aspell list \
            --mode=markdown \
            --personal=.aspell/project.pws \
            < "$file" 2>/dev/null)

        if [ -n "$ERRORS" ]; then
            echo "拼写错误: $file"
            echo "$ERRORS" | sort -u | sed 's/^/  /'
            EXIT_CODE=1
        fi
    fi
done

exit $EXIT_CODE

8.9 业务场景

场景 1:技术博客写作流程

# 1. 使用 Emacs 写作
emacs content/posts/new-article.md

# 2. 实时 flyspell 检查(自动启用)

# 3. 提交前 CI 检查
git add content/posts/new-article.md
git commit -m "feat: add new article"
# → pre-commit hook 自动运行拼写检查

# 4. 推送到 GitHub
git push origin main
# → GitHub Actions 自动运行完整拼写检查

场景 2:开源项目文档维护

# .github/workflows/docs-quality.yml
name: Documentation Quality

on:
  pull_request:
    paths:
      - 'docs/**'
      - '*.md'

jobs:
  quality:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Install tools
        run: |
          sudo apt-get install -y aspell aspell-en
          npm install -g markdownlint-cli

      - name: Spell check
        run: ./scripts/spellcheck.sh

      - name: Markdown lint
        run: markdownlint docs/

场景 3:学术论文校对

;; Emacs 配置:LaTeX 论文校对模式
(defun my/latex-proofread-setup ()
  "设置 LaTeX 校对环境"
  (setq ispell-extra-args '("--mode=tex" "--sug-mode=bad-spellers"))
  (setq ispell-personal-dictionary "~/Documents/papers/academic.pws")
  (flyspell-mode 1))

(add-hook 'LaTeX-mode-hook 'my/latex-proofread-setup)

8.10 本章小结

要点说明
Emacs内置 ispell.el 支持,ispell-program-name 设为 aspell
Vimset spell 启用,]s/[s 导航,z= 查看建议
VS CodeCode Spell Checker 扩展(独立词典体系)
GitHub Actionsaspell list + find 批量检查
Git Hookspre-commit 提交前检查
Makefilemake spell 构建流程集成

下一步

第 9 章:Docker 中使用 Aspell — 学习在容器化环境中使用 Aspell 进行批量检查和自动化。