Aspell 拼写检查完全教程 / 第8章 编辑器与 CI 集成
第 8 章:编辑器与 CI 集成
本章介绍如何将 Aspell 集成到日常编辑器(Emacs、Vim、VS Code)和 CI/CD 流水线中,实现自动化的拼写检查。
8.1 集成方案总览
| 场景 | 工具 | 集成方式 |
|---|---|---|
| Emacs | ispell.el | 内置 Aspell 后端 |
| Vim | vim-mundo / aspell.vim | 外部命令调用 |
| VS Code | Code Spell Checker | 扩展(非 Aspell 直接集成) |
| CI/CD | GitHub Actions / GitLab CI | 脚本调用 aspell list |
| Git Hooks | pre-commit | 提交前检查 |
| Makefile | make 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-buffer | ispell-buffer | 检查整个缓冲区 |
M-x ispell-region | ispell-region | 检查选中区域 |
M-x ispell-change-dictionary | ispell-change-dictionary | 切换词典 |
M-x ispell-kill-ispell | ispell-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 |
| Vim | set spell 启用,]s/[s 导航,z= 查看建议 |
| VS Code | Code Spell Checker 扩展(独立词典体系) |
| GitHub Actions | aspell list + find 批量检查 |
| Git Hooks | pre-commit 提交前检查 |
| Makefile | make spell 构建流程集成 |
下一步
→ 第 9 章:Docker 中使用 Aspell — 学习在容器化环境中使用 Aspell 进行批量检查和自动化。