Show HN: Zcomplete – Shell 命令行输入纠错工具
Show HN: Zcomplete – Shell Typo Correction

原始链接: https://github.com/omarfakih1/zcomplete

**zcomplete** 是一款轻量级、高性能的命令行工具,适用于 Zsh、Bash 和 Fish,可实时纠正输入错误的命令。 **主要特性:** * **智能纠错:** 能够针对拼写错误(例如 `gti` → `git`)、前缀错误(`mkd` → `mkdir`)以及子序列匹配提供修正建议。它会根据你的历史记录和使用模式进行学习,并按频率和近期使用情况对命令进行排序。 * **安全至上:** 内置“危险命令”列表(如 `rm` 或 `git reset --hard`),执行时需用户明确确认。用户可设置模式:`safe`(默认)、`unsafe` 或 `bypass`。 * **高性能:** 使用 Rust 编写,运行速度极快,对 Shell 响应时间的额外开销小于 1 毫秒。 * **兼容性与隐私:** 支持 Linux 和 macOS (arm64/x86-64),无需复杂配置,且所有数据均保存在本地(无任何数据上传)。 * **简易安装:** 通过单条 Shell 命令即可安装。它能无缝集成且不会覆盖现有配置,并完全兼容你的 Shell 环境。 无论你是要修复简单的拼写错误还是复杂的子命令,zcomplete 都能通过学习你的习惯来节省时间,在确保持续工作流的同时,为可能具有破坏性的命令提供一道安全防线。

对不起。
相关文章

原文

You mistype a command. Instead of command not found, zcomplete figures out which command you meant from the ones you actually run, and offers to run it with your arguments.

$ mkd build
zcomplete: run mkdir instead of 'mkd'? [Y/n] y

$ gti status
zcomplete: run git instead of 'gti'? [Y/n] y
On branch main
nothing to commit, working tree clean

$ cargo tset
error: no such command: `tset`
zcomplete: run cargo test? [Y/n] y

zsh, bash and fish. One binary, one dependency (libc), about 550 KB.

curl -fsSL https://raw.githubusercontent.com/omarfakih1/zcomplete/main/install.sh | sh

That downloads the binary, sets up each shell it finds, and offers to seed the database from your history. It asks before touching any config file. -s -- -y answers yes to everything.

Then restart your shell, or exec $SHELL. Done.

The installer checks the binary against its published sha256 and refuses to install if the checksum is missing or wrong. macOS and Linux, arm64 and x86-64. --prefix=DIR defaults to ~/.local. Replace | sh with | less to read install.sh first.

cargo install --git https://github.com/omarfakih1/zcomplete
zcomplete init --all && zcomplete import

init --all adds one line to the config of every shell you have. Run it twice and the second run does nothing. It appends and never rewrites, so the rest of your config is untouched. --zsh, --bash or --fish picks one.

import reads your history file and asks the shell for its aliases, functions and builtins. Those aren't on PATH, so without it they'd be discarded as words that aren't commands.

The line init writes, if you'd rather add it yourself:

shell line file
zsh eval "$(zcomplete init zsh)" ~/.zshrc
bash eval "$(zcomplete init bash)" ~/.bashrc
fish zcomplete init fish | source ~/.config/fish/config.fish

(zcomplete init zsh without the dashes prints the integration script. That's what the line above runs. You don't type it yourself.)

zcomplete doctor says what's set up and what isn't.

key
y or Enter run it
n or i leave it alone
u fix the subcommand too, when one is offered
1-9 pick from the list when the match is unclear

One keypress, no Enter. u is for a line where both words are wrong:

$ zcom he
zcomplete: run zcomplete instead of 'zcom'?  u: also he -> help [Y/n] u
zcomplete 0.1.2 - run the command you meant

y there would run zcomplete he and let it fail.

zcomplete safe      # confirm every correction (default)
zcomplete unsafe    # run ordinary corrections, confirm dangerous ones
zcomplete bypass    # never confirm
zcomplete off       # stop correcting

The mode is read on every correction, so a change reaches shells that are already open. ZCOMPLETE_MODE=safe ./script.sh overrides it for one command.

Dangerous means listed in src/safety.rs: rm, dd, mkfs*, git push --force, git reset --hard, terraform destroy, kubectl delete, recursive chmod, curl ... | sh, and about thirty more. Flags are read the way the shell reads them, so -rf, -r -f, --force and a -- terminator all count, and git clean -n is not treated like git clean -fd.

Every command you run gets a rank, and ranks decay with age:

score = rank × 4      used within the hour
        rank × 2      within the day
        rank ÷ 2      within the week
        rank ÷ 4      older

Commands you ran in the current directory count for more, so ma gives make inside a project and man everywhere else.

A typed word is matched four ways: prefix (mkdmkdir), initials (dcdocker-compose), subsequence (dkrdocker) and typo (gtigit, slls). Match quality decides the winner and usage only breaks ties. Sorting the other way round makes gti mean gtimeout.

Two things hold in every mode, bypass included:

  • Nothing is suggested that isn't a command. A word enters the database only if the shell could resolve it at the time, and every candidate is checked against PATH again before you're offered it.
  • Non-interactive shells are left alone. No controlling terminal means no correction, so scripts and CI never get rewritten.

git sttaus, cargo tset, docker psu. No list of tools is hardcoded. Second words get counted, and a command needs two of them to come back before its second argument is read as a verb at all. So git status and git commit qualify git within a day, and the foo in grep foo file.c never qualifies grep. For a command it knows nothing about, zcomplete reads its --help once and caches the answer, which is why git sttaus works on a fresh install.

A subcommand can't be checked against PATH ahead of time, so it's corrected after the command fails rather than instead of running it. That's safe because a command that failed on its verb did nothing. A line that failed for any other reason is left alone, and a subcommand is only learned once it has exited zero, so git sttaus can never teach zcomplete that git has a sttaus.

Two limits: one command per line (cp a b && git sttaus is refused, since rerunning would repeat the copy), and the first two words only (npm run buidl is out of reach).

Confirm mkdmkdir three times and it becomes the direct answer. Refuse the same suggestion twice and it retires. Or set it yourself:

zcomplete bind gs git        # gs always means git
zcomplete unbind gs
zcomplete ignore sl          # never suggest sl
zcomplete init --all             set up every shell you have (--zsh, --bash, --fish)
zcomplete stats [-n N]           what it has learned, strongest first
zcomplete stats <command>        subcommands it would offer for one command
zcomplete query <word>           what a word would resolve to (--score for detail)
zcomplete query <cmd> <word>     the same, for a subcommand
zcomplete import [zsh|bash|fish] seed from history (--dry-run to preview)
zcomplete forget <command>...    unlearn; --all empties everything
zcomplete ignore [<command>...]  list, add to, or --remove from the ignore list
zcomplete bind <word> <command>  pin a shortcut; unbind removes it
zcomplete mode                   show the current mode
zcomplete safe | unsafe | bypass set it
zcomplete on | off               enable or disable without editing shell config
zcomplete doctor                 check the installation

A command that worked needs no correction, so nothing starts for it. The hook appends one line to a per-session file using only shell builtins. Per command, on an M-series Mac:

zsh, bash               0.067 ms
fish                    0.15  ms
starting any process    1.4   ms   (for comparison)

When a correction is actually needed, 2000 learned commands and 4257 executables on PATH put the whole run at 2.2-2.7 ms, against a 1.9 ms floor for a Rust binary that does nothing. Under a millisecond of that is zcomplete's own work.

Everything sits in one directory you can delete at any time (~/.local/share/zcomplete, or $XDG_DATA_HOME/zcomplete). Every file in it has a ceiling:

file what it is bound
commands.bin the database ~160 KB, 4096 scoped rows and 512 shortcuts, weakest evicted
path.<hash> the names on one PATH the 4 most recently used
journal.<pid> what one shell hasn't folded yet one per live shell, emptied at each fold
commands.corrupt.* a database that wouldn't read, kept in case you want it the 2 most recent

Twelve different PATHs and 2000 learned commands come to 48 KB.

The PATH listings used to have no ceiling, and every distinct PATH left one behind for good. Ten venvs meant ten copies, kept for ever. Fixed.

The database holds command names. Never arguments. It's mode 0600 in a directory created 0700.

No config file. The mode lives in the database, so zcomplete safe reaches open shells and there's no second file to keep in step.

variable
ZCOMPLETE_MODE override the mode for one command
ZCOMPLETE_DISABLE switch corrections off for one shell
ZCOMPLETE_DATA_DIR move the database
NO_COLOR, TERM colour
HISTFILE, XDG_DATA_HOME read by import, honoured for the data path

zsh and bash correct an alias a moment later than a program. Both run the not-found handler in a forked child, which loses what an alias or function does to the shell itself (cd, set, export). When the answer is one of those, zcomplete hands it back for the next prompt to run in the real shell. Looks the same, costs one extra call on a command that had already failed.

Your prompt sees the word you typed, not the command that ran. The fork can't tell the shell it corrected anything. Usually invisible, but a prompt that reacts to specific commands reacts to the typo. Under powerlevel10k a corrected clea leaves the blank line a typed clear would have suppressed; POWERLEVEL9K_PROMPT_ADD_NEWLINE=false removes it.

fish corrects the line before it runs, so zcomplete binds enter there. fish calls fish_command_not_found only after abandoning the job, so correcting from inside it would hand cat the keyboard in printf x | ct. Rewriting the line first keeps pipes, redirections, job control and $status normal. zcomplete chains onto whatever enter was already bound to, so load it last.

bash 3.2, the version macOS ships, can't intercept anything. command_not_found_handle arrived in bash 4.0. On 3.2 zcomplete still learns, and offers the fix at the next prompt instead:

$ mkd build
bash: mkd: command not found
zcomplete: run mkdir build? [Y/n] y

That runs in the real shell, so cd works. Install bash 4+ for in-place correction.

Run the install command again. It replaces the binary and leaves your database, mode and shell config alone.

curl -fsSL https://raw.githubusercontent.com/omarfakih1/zcomplete/main/install.sh | sh

The new binary is written beside the old one and renamed over it, so a shell mid-command keeps running the version it started with. Open shells use the old one until the hook next starts a process. exec $SHELL picks up the new one straight away, and is needed anyway if the integration script itself changed:

zcomplete --version && exec $SHELL

From a checkout: git pull && ./install.sh && exec $SHELL. --version=vX.Y.Z installs a specific release, which is also how you go back.

curl -fsSL https://raw.githubusercontent.com/omarfakih1/zcomplete/main/uninstall.sh | sh

Takes the lines back out of your shell config (leaving a .zcomplete.bak beside each), deletes the database, removes the binary. --keep-data keeps what it learned. Shells you already have open keep the hook until you exec $SHELL.

cargo test doesn't cover src/init/. Those are tested by driving a real zsh, bash and fish under a pty, which needs all three installed and takes about a minute:

cargo test && cargo build --release && python3 tests/shells.py

tests/bench.sh times the hot path against /usr/bin/true.

MIT.

联系我们 contact @ memedata.com