diff --git a/.github/workflows/PR-test.yml b/.github/workflows/PR-test.yml index eed02fa8..68c082b8 100644 --- a/.github/workflows/PR-test.yml +++ b/.github/workflows/PR-test.yml @@ -2,9 +2,10 @@ # Workflow File : PR-test.yml # File Authors : 曾奥然 # Contributors : Mikachu2333 +# | @swim233 # | # Created On : <2025-06-19> -# Last Modified : <2025-08-17> +# Last Modified : <2026-09-09> # # Test PR # --------------------------------------------------------------- @@ -25,6 +26,10 @@ on: paths: - "src/**" - "lib/**" + - "Makefile" + - "tool/completion/**" + - "test/*zsh-completion*" + - "pkg/deb/**" jobs: test-on-ubuntu: @@ -36,13 +41,18 @@ jobs: - name: 检出代码 uses: actions/checkout@v7 + - name: 安装Zsh补全测试依赖 + run: | + sudo apt-get update + sudo apt-get install -y zsh + - name: 测试构建情况 run: | make - name: 测试test情况 run: | - make test + make test REQUIRE_ZSH=1 test-on-windows: name: 在Windows上测试 diff --git a/Makefile b/Makefile index eaaa5d42..351d04a5 100755 --- a/Makefile +++ b/Makefile @@ -9,9 +9,10 @@ # | @G_I_Y # | @NewbieXvwu # | @Mikachu2333 +# | @swim233 # | # Created On : <2023-08-28> -# Last Modified : <2026-08-14> +# Last Modified : <2026-09-09> # # 请阅读 ./doc/01-开发与构建.md 来使用 # -------------------------------------------------------------- @@ -202,6 +203,9 @@ build-in-release-mode: @$(call Build-Command-For-Release, $(ReleaseMode-Target-Name)) @$(call Finished-Echo-Info, RELEASE) +# 确保从干净源码树执行 install 时已构建 RELEASE mode 二进制 +$(ReleaseMode-Target-Name): build-in-release-mode + build-in-ci-release-mode: @$(call Starting-Echo-Info, CI-RELEASE, $(CIReleaseMode-Target-Name)) @$(call Build-Command-For-Release, $(CIReleaseMode-Target-Name)) @@ -215,7 +219,7 @@ debug: build-in-debug-mode -test: test-make-var test-xy test-fw +test: test-make-var test-xy test-fw test-zsh-completion test-make-var: @echo "On-Linux: $(On-Linux)" @@ -245,6 +249,19 @@ test-fw: @$(CC) test/fw.c $(CFLAGS) -o fw @./fw +test-zsh-completion: + @perl ./test/verify-sync-state-of-zsh-completion + @if command -v zsh >/dev/null 2>&1; then \ + zsh -n ./tool/completion/_chsrc ./test/simulate-zsh-completion.zsh ./test/zsh-completion-integration.zsh && \ + zsh -f ./test/simulate-zsh-completion.zsh && \ + zsh -f ./test/zsh-completion-integration.zsh; \ + elif [ "$(REQUIRE_ZSH)" = "1" ]; then \ + echo "zsh is required for Zsh completion tests" >&2; \ + exit 1; \ + else \ + echo "zsh not found; skipped Zsh completion runtime tests"; \ + fi + check: test # AUR package 安装时将执行此 target @@ -276,10 +293,15 @@ build-deb: clean-deb: @$(MAKE) -C pkg/deb deb-clean +# 源码安装使用 Zsh 通用的 site-functions 目录;Debian 打包时会将其 +# 覆盖为 /usr/share/zsh/vendor-completions,以符合 Debian 的目录约定 +ZSH_COMPLETION_DIR ?= /usr/share/zsh/site-functions + install: $(ReleaseMode-Target-Name) install -D -m 755 $(ReleaseMode-Target-Name) $(DESTDIR)/usr/bin/chsrc install -D -m 644 doc/chsrc.1 $(DESTDIR)/usr/share/man/man1/chsrc.1 install -D -m 644 tool/completion/bash_completion.sh $(DESTDIR)/usr/share/bash-completion/completions/chsrc + install -D -m 644 tool/completion/_chsrc $(DESTDIR)$(ZSH_COMPLETION_DIR)/_chsrc @@ -291,4 +313,4 @@ install: $(ReleaseMode-Target-Name) .PHONY: all b build bd br bcir d t check c \ build-in-dev-mode build-in-debug-mode build-in-release-mode build-in-ci-release-mode \ - debug test test-make-var test-xy test-fw fastcheck test-cli clean install build-deb clean-deb rawstr4c + debug test test-make-var test-xy test-fw test-zsh-completion fastcheck test-cli clean install build-deb clean-deb rawstr4c diff --git a/pkg/deb/deb-installation-test.sh b/pkg/deb/deb-installation-test.sh index 214612b5..869e58ab 100644 --- a/pkg/deb/deb-installation-test.sh +++ b/pkg/deb/deb-installation-test.sh @@ -36,7 +36,20 @@ else echo "✓ chsrc man page exists" fi -# Test 3: Test basic functionality +# Test 3: Check if shell completion files exist +if [ ! -f "/usr/share/bash-completion/completions/chsrc" ]; then + echo "ERROR: Bash completion not found at /usr/share/bash-completion/completions/chsrc" + exit 1 +fi + +if [ ! -f "/usr/share/zsh/vendor-completions/_chsrc" ]; then + echo "ERROR: Zsh completion not found at /usr/share/zsh/vendor-completions/_chsrc" + exit 1 +fi + +echo "✓ Bash and Zsh completion files exist" + +# Test 4: Test basic functionality echo "Testing basic chsrc functionality..." if /usr/bin/chsrc help >/dev/null 2>&1; then echo "✓ command 'chsrc help' works" diff --git a/pkg/deb/debian/rules b/pkg/deb/debian/rules index 98ce5cd6..d9e2e7da 100755 --- a/pkg/deb/debian/rules +++ b/pkg/deb/debian/rules @@ -46,4 +46,6 @@ override_dh_auto_build: @$(MAKE) -C $(Chsrc-Root-Dir) build-in-release-mode override_dh_auto_install: - @$(MAKE) -C $(Chsrc-Root-Dir) install DESTDIR=$(CURDIR)/debian/chsrc + @$(MAKE) -C $(Chsrc-Root-Dir) install \ + DESTDIR=$(CURDIR)/debian/chsrc \ + ZSH_COMPLETION_DIR=/usr/share/zsh/vendor-completions diff --git a/src/chsrc-main.c b/src/chsrc-main.c index ec9679ed..c61716ab 100644 --- a/src/chsrc-main.c +++ b/src/chsrc-main.c @@ -82,6 +82,7 @@ chsrc_register_chefs_and_sauciers () chef_register_contributor ("@techoc", "Rui Yang", "techoc@foxmail.com", NULL); chef_register_contributor ("@BingChunMoLi", "BingChunMoLi", "bingchunmoli@bingchunmoli.com", NULL); chef_register_contributor ("@wcbing", "wcbing", "i@wcbing.top", NULL); + chef_register_contributor ("@swim233", "TheSw1m", "swim853279614@163.com", NULL); // 该注释下一行的用户 ID 为 Gitee ID chef_register_contributor ("@hezonglun", "HeZongLun", "hezonglun123456@outlook.com", NULL); chef_register_contributor ("@Young-Lord", "LY", "ly-niko@qq.com", NULL); diff --git a/test/simulate-zsh-completion.zsh b/test/simulate-zsh-completion.zsh new file mode 100644 index 00000000..aaebdb7b --- /dev/null +++ b/test/simulate-zsh-completion.zsh @@ -0,0 +1,236 @@ +#!/usr/bin/env zsh +# --------------------------------------------------------------- +# SPDX-License-Identifier: GPL-3.0-or-later +# --------------------------------------------------------------- +# Test File : simulate-zsh-completion.zsh +# Test Authors : @swim233 +# Contributors : TheSw1m +# | +# Created On : <2026-09-08> +# Last Modified : <2026-09-09> +# +# 使用同名函数覆盖 Zsh 补全组件,模拟并测试 _chsrc 的状态机。 +# 测试不会读取或修改用户的 Zsh 配置。 +# +# $ zsh -f test/simulate-zsh-completion.zsh +# --------------------------------------------------------------- + +emulate -L zsh +setopt errexit nounset pipefail extendedglob + +typeset test_dir=${0:A:h} +fpath=("${test_dir}/../tool/completion" ${fpath}) +autoload -Uz _chsrc + +typeset -ga captured + +# 模拟 _describe,将符合 PREFIX 的候选项记录到 captured +_describe() { + local array_name=${argv[-1]} + local -a specs + specs=("${(@P)array_name}") + local spec candidate + for spec in ${specs}; do + candidate=${spec%%:*} + if [[ -z ${PREFIX} || ${candidate[1,${#PREFIX}]} == ${PREFIX} ]]; then + captured+=("${candidate}") + fi + done +} + +# 模拟 compadd,仅记录 -- 分隔符之后且符合 PREFIX 的候选项 +compadd() { + local after_separator=0 arg + for arg in ${argv}; do + if (( after_separator )); then + if [[ -z ${PREFIX} || ${arg[1,${#PREFIX}]} == ${PREFIX} ]]; then + captured+=("${arg}") + fi + elif [[ ${arg} == -- ]]; then + after_separator=1 + fi + done +} + +# 模拟 compset -P,从 PREFIX 中移除已经输入的固定前缀 +compset() { + if [[ $1 == -P ]]; then + PREFIX=${PREFIX#$2} + fi +} + +# 测试无需展示补全提示信息 +_message() { return 0 } + +# 构造 Zsh 补全上下文并执行 _chsrc +capture() { + captured=() + words=("${(@)argv}") + CURRENT=${#words} + PREFIX=${words[CURRENT]} + _chsrc +} + +assert_has() { + local expected=$1 + # (I) 返回最后一个匹配元素的下标,未匹配时返回 0; + # (e) 使用字符串精确匹配,避免将候选项当作模式解析。 + if (( ! ${captured[(Ie)${expected}]} )); then + print -u2 -- "Expected completion '$expected' in: ${captured[*]}" + return 1 + fi +} + +assert_lacks() { + local unexpected=$1 + # 与 assert_has 相同,使用 (Ie) 取得精确匹配元素的下标。 + if (( ${captured[(Ie)${unexpected}]} )); then + print -u2 -- "Unexpected completion '$unexpected' in: ${captured[*]}" + return 1 + fi +} + +capture chsrc '' +assert_has version +assert_has measure +assert_has reset +assert_has -scope= +assert_lacks mea +assert_lacks rest +assert_lacks -local +assert_lacks -scope +assert_lacks -english +assert_lacks -no-colour +assert_lacks -h +assert_lacks -help +assert_lacks --help +assert_lacks -v +assert_lacks -version +assert_lacks --version + +capture chsrc -scope=p +assert_has project +assert_lacks default + +typeset -A dispatch_expect=( + help -dry + h -dry + -h -dry + -help -dry + --help -dry + issue -dry + issues -dry + isue -dry + i -dry + version -dry + ver -dry + -v -dry + -version -dry + --version -dry + list mirror + ls mirror + l mirror + measure ruby + mea ruby + m ruby + cesu ruby + ce ruby + c ruby + get ruby + g ruby + set ruby + s ruby + reset ruby + rest ruby + r ruby +) + +typeset command expected +for command expected in "${(@kv)dispatch_expect}"; do + capture chsrc "${command}" '' + assert_has "${expected}" +done + +capture chsrc set '' +assert_has omarchy +assert_has uv-pypi-index +assert_has uv-python-build +assert_has -scope= +assert_lacks pypi +assert_lacks nodejs-binary + +capture chsrc s -dry '' +assert_has ruby + +capture chsrc set -local '' +assert_has ruby + +capture chsrc set -english '' +assert_has ruby + +capture chsrc set -no-colour '' +assert_has ruby + +capture chsrc set -scope project '' +assert_has ruby + +capture chsrc set -scope '' +assert_has default +assert_has project +assert_has user +assert_has system +assert_lacks ruby + +capture chsrc set -scope= +assert_has default +assert_has project +assert_has user +assert_has system + +capture chsrc set -scope=p +assert_has project +assert_lacks default +assert_lacks system + +capture chsrc set -scope=x +assert_lacks default +assert_lacks project + +capture chsrc set -lo +assert_lacks -local + +capture chsrc -en -he +assert_lacks -help +assert_lacks help + +capture chsrc list '' +assert_has mirror +assert_has dish +assert_has lang +assert_has ware +assert_lacks mirrors +assert_lacks dishes +assert_lacks language +assert_lacks software +assert_has ruby + +capture chsrc set ruby '' +assert_has first +assert_has upstream +assert_lacks -dry + +capture chsrc set ruby f +assert_has first +assert_lacks upstream + +capture chsrc set ruby first '' +assert_lacks first +assert_lacks upstream + +capture chsrc set ruby first u +assert_lacks upstream + +capture chsrc set ruby -scope= +assert_lacks default + +print 'Zsh completion state-machine tests passed.' diff --git a/test/verify-sync-state-of-zsh-completion b/test/verify-sync-state-of-zsh-completion new file mode 100644 index 00000000..859cbe7d --- /dev/null +++ b/test/verify-sync-state-of-zsh-completion @@ -0,0 +1,108 @@ +#!/usr/bin/env perl +# --------------------------------------------------------------- +# SPDX-License-Identifier: GPL-3.0-or-later +# --------------------------------------------------------------- +# Test File : verify-sync-state-of-zsh-completion +# Test Authors : @swim233 +# Contributors : TheSw1m +# | +# Created On : <2026-09-08> +# Last Modified : <2026-09-09> +# +# 校验原生 Zsh 补全中的菜品别名与 src/recipe/menu.c 注册菜单保持一致。 +# 仅供内部使用的 sources-only dish 不属于注册菜单,不应出现在补全中。 +# +# $ perl test/verify-sync-state-of-zsh-completion +# --------------------------------------------------------------- + +use strict; +use warnings; +use FindBin qw($Bin); +use File::Find qw(find); + +my $root = "$Bin/.."; +my $menu_path = "$root/src/recipe/menu.c"; +my $completion_path = "$root/tool/completion/_chsrc"; + +open my $menu_fh, '<', $menu_path or die "Cannot read $menu_path: $!\n"; +my @registered; +my $category; +while (my $line = <$menu_fh>) { + if ($line =~ /^#define add\(t\).*&((?:pl|os|wr))_##t##_dish/) { + $category = $1; + next; + } + if ($line =~ /^#undef add/) { + undef $category; + next; + } + if (defined $category && $line =~ /\badd\s*\(\s*([A-Za-z0-9_]+)\s*\)/) { + push @registered, "${category}_$1"; + } +} +close $menu_fh; + +my %aliases_for; +find( + sub { + return unless -f $_ && /\.c\z/; + open my $fh, '<', $File::Find::name + or die "Cannot read $File::Find::name: $!\n"; + local $/; + my $source = <$fh>; + close $fh; + while ($source =~ /def_(?:combo_)?dish\s*\(\s*([A-Za-z0-9_]+)\s*,\s*"([^"]+)"\s*\)/g) { + $aliases_for{$1} = [split m{/}, $2]; + } + }, + "$root/src/recipe" +); + +my @errors; +my %expected; +my %menu_category = (pl => 'lang', os => 'os', wr => 'ware'); +for my $dish (@registered) { + if (!exists $aliases_for{$dish}) { + push @errors, "registered dish $dish has no def_dish/def_combo_dish declaration"; + next; + } + my ($prefix) = $dish =~ /^(pl|os|wr)_/; + for my $alias (@{$aliases_for{$dish}}) { + $expected{$alias} = $menu_category{$prefix}; + } +} + +open my $completion_fh, '<', $completion_path + or die "Cannot read $completion_path: $!\n"; +my %completed; +my $completion_category; +while (my $line = <$completion_fh>) { + if ($line =~ /^dishes_(lang|os|ware)=\(/) { + $completion_category = $1; + next; + } + if (defined $completion_category && $line =~ /^\)/) { + undef $completion_category; + next; + } + if (defined $completion_category && $line =~ /^\s*'([^':]+):/) { + $completed{$1} = $completion_category; + } +} +close $completion_fh; + +for my $alias (sort keys %expected) { + if (!exists $completed{$alias}) { + push @errors, "completion is missing registered dish alias '$alias'"; + } elsif ($completed{$alias} ne $expected{$alias}) { + push @errors, "completion puts '$alias' in $completed{$alias}, expected $expected{$alias}"; + } +} + +for my $alias (sort keys %completed) { + push @errors, "completion contains unregistered dish alias '$alias'" + unless exists $expected{$alias}; +} + +die join("\n", @errors) . "\n" if @errors; +print "Zsh completion dish aliases match the registered menus.\n"; diff --git a/test/zsh-completion-integration.zsh b/test/zsh-completion-integration.zsh new file mode 100644 index 00000000..ecc5f3f3 --- /dev/null +++ b/test/zsh-completion-integration.zsh @@ -0,0 +1,123 @@ +#!/usr/bin/env zsh +# --------------------------------------------------------------- +# SPDX-License-Identifier: GPL-3.0-or-later +# --------------------------------------------------------------- +# Test File : zsh-completion-integration.zsh +# Test Authors : @swim233 +# Contributors : TheSw1m +# | +# Created On : <2026-09-08> +# Last Modified : <2026-09-09> +# +# 在隔离的子 Zsh 中测试真实的 ZLE 补全匹配与插入行为。 +# 子进程使用临时 ZDOTDIR 和 compinit -D,不读取或修改用户及系统配置。 +# +# $ zsh -f test/zsh-completion-integration.zsh +# --------------------------------------------------------------- + +emulate -L zsh +setopt errexit nounset pipefail extendedglob + +zmodload zsh/zpty +zmodload zsh/zselect + +typeset test_dir=${0:A:h} +typeset completion_dir=${test_dir:h}/tool/completion +typeset temp_parent=${TMPDIR:-/tmp} +typeset temp_zdotdir +temp_zdotdir=$(mktemp -d "${temp_parent}/chsrc-zsh-completion.XXXXXXXX") +typeset child_started=0 + +cleanup() { + if (( child_started )); then + zpty -d chsrc_completion_shell 2>/dev/null || true + fi + if [[ -n ${temp_zdotdir:-} && -d ${temp_zdotdir} && + ${temp_zdotdir:t} == chsrc-zsh-completion.* ]]; then + command rm -r -- "${temp_zdotdir}" + fi +} +trap cleanup EXIT HUP INT TERM + +{ + print -r -- "fpath=(${(q)completion_dir} \${fpath})" + print -r -- 'autoload -Uz compinit' + print -r -- 'compinit -D' + print -r -- "PS1='CHSRC_TEST> '" + print -r -- "RPROMPT=''" + print -r -- "bindkey '^I' expand-or-complete" + print -r -- 'setopt autolist beep' +} > "${temp_zdotdir}/.zshrc" + +# 使用 TERM=dumb,避免运行测试的终端影响捕获到的 ZLE 输出。 +# 功能较丰富的终端会给自动插入的后缀增加 ANSI 属性,即使补全行为正确, +# 也会导致下面的纯文本断言失败。 +zpty -b chsrc_completion_shell env TERM=dumb ZDOTDIR="${temp_zdotdir}" zsh -d +child_started=1 + +wait_for_output() { + local needle=$1 chunk='' + integer attempts=300 + REPLY='' + while (( attempts-- > 0 )); do + while zpty -rt chsrc_completion_shell chunk; do + REPLY+="${chunk}" + chunk='' + done + [[ ${REPLY} == *"${needle}"* ]] && return 0 + zselect -t 1 || true + done + print -u2 -r -- "Timed out waiting for ${(qqq)needle}; output was ${(qqq)REPLY}" + return 1 +} + +clear_input() { + zpty -wn chsrc_completion_shell $'\C-u' +} + +collect_pending_output() { + local chunk='' + zselect -t 5 || true + while zpty -rt chsrc_completion_shell chunk; do + REPLY+="${chunk}" + chunk='' + done +} + +complete_and_expect() { + local input=$1 expected=$2 + zpty -wn chsrc_completion_shell "${input}"$'\t' + wait_for_output "${expected}" + clear_input +} + +complete_and_expect 'chsrc set omar' 'chsrc set omarchy ' +complete_and_expect 'chsrc s -scope=p' 'chsrc s -scope=project ' +complete_and_expect 'chsrc set -scope p' 'chsrc set -scope project ' +complete_and_expect 'chsrc -scope=p' 'chsrc -scope=project ' +complete_and_expect 'chsrc set ruby fi' 'chsrc set ruby first ' +complete_and_expect 'chsrc -en -no-' 'chsrc -en -no-color ' + +# 镜像占据最后一个位置参数;在后续空参数处按 Tab 不应再次列出 +# first/upstream。 +zpty -wn chsrc_completion_shell $'chsrc set ruby first \t' +wait_for_output $'\a' +collect_pending_output +if [[ ${REPLY} == *upstream* ]]; then + print -u2 -r -- "Completion offered a second mirror: ${(qqq)REPLY}" + exit 1 +fi +clear_input + +# 在同一个无效位置输入非空内容,也不应被唯一扩展为 upstream。 +# 即使候选项样式隐藏了空前缀菜单,该断言仍可捕获重复镜像补全。 +zpty -wn chsrc_completion_shell $'chsrc set ruby first u\t' +wait_for_output $'\a' +collect_pending_output +if [[ ${REPLY} == *upstream* ]]; then + print -u2 -r -- "Completion inserted a second mirror: ${(qqq)REPLY}" + exit 1 +fi +clear_input + +print 'Real Zsh completion insertion tests passed.' diff --git a/tool/completion/README.md b/tool/completion/README.md new file mode 100644 index 00000000..bc51baff --- /dev/null +++ b/tool/completion/README.md @@ -0,0 +1,43 @@ + + +# Shell 补全说明 + +本目录包含 chsrc 的 Shell 补全实现: + +- `bash_completion.sh`:Bash 补全 +- `_chsrc`:原生 Zsh 补全 + +Zsh 会通过 `#compdef chsrc` 将 `_chsrc` 注册为 `chsrc` 的补全函数。补全 +函数根据 Zsh 提供的 `words`、`CURRENT` 和 `PREFIX` 判断光标位置及当前待 +补全内容。 + +## `_describe` + +`_describe` 是 Zsh 补全系统提供的辅助函数,用于批量添加带说明的候选项。 +本项目使用 `候选项:说明` 格式的数组,使补全菜单可以同时展示命令、选项 +及其用途;`-t` 参数用于为候选项指定标签,方便 Zsh 按类别应用样式。 + +## `compadd` + +`compadd` 是直接向当前补全上下文添加候选项的内建命令。本项目用它添加 +`-scope=`:`-S ''` 禁止在候选项后自动附加空格,使用户可以继续输入作用域; +`-X` 则为这一组候选项提供说明。 + +## `compset` + +`compset` 用于调整 Zsh 的补全上下文。本项目使用 `compset -P '-scope='` +把已输入的 `-scope=` 从 `PREFIX` 移到 `IPREFIX`。这样后续匹配只针对 +`default`、`project`、`user`、`system`,插入结果时仍会保留 `-scope=` +前缀。 diff --git a/tool/completion/_chsrc b/tool/completion/_chsrc new file mode 100644 index 00000000..40047a53 --- /dev/null +++ b/tool/completion/_chsrc @@ -0,0 +1,325 @@ +#compdef chsrc +# --------------------------------------------------------------- +# SPDX-License-Identifier: GPL-3.0-or-later +# --------------------------------------------------------------- +# Shell File : _chsrc +# File Authors : @swim233 +# Contributors : TheSw1m +# | +# Created On : <2026-09-08> +# Last Modified : <2026-09-09> +# +# chsrc 原生 Zsh 补全。菜品别名应与 src/recipe/menu.c 中注册的菜单保持一致, +# test/verify-sync-state-of-zsh-completion 会校验两者的同步状态。 +# --------------------------------------------------------------- + +emulate -L zsh +setopt extendedglob + +local -a commands options scope_options list_targets +local -a dishes_lang dishes_os dishes_ware mirror_choices + +# 命令、选项和 list 目标仅展示一种规范写法,避免补全菜单重复。 +# 解析状态仍接受所有别名,因此用户手动输入别名后仍可获得上下文补全。 +commands=( + 'help:show help (aliases: h, -h, -help, --help)' + 'issue:show related issue links (aliases: issues, isue, i)' + 'list:list mirrors or supported dishes (aliases: ls, l)' + 'measure:measure all sources for a dish (aliases: mea, m, cesu, ce, c)' + 'get:show the current source for a dish (alias: g)' + 'set:change the source for a dish (alias: s)' + 'reset:reset a dish to its upstream source (aliases: rest, r)' + 'version:show version information (aliases: ver, -v, -version, --version)' + '-dry:simulate changes before a help or version flag' + '-ipv6:use IPv6 before a help or version flag' + '-en:use English before a help or version flag (alias: -english)' + '-no-color:disable colored output before a help or version flag (alias: -no-colour)' +) + +options=( + '-dry:simulate changes without running them' + '-ipv6:use IPv6 for speed measurements' + '-en:use English output (alias: -english)' + '-no-color:disable colored output (alias: -no-colour)' + '-h:show help (aliases: -help, --help)' + '-v:show version information' +) + +scope_options=( + 'default:use the scope selected by the recipe' + 'project:change the current project only' + 'user:change the current user only' + 'system:change the whole system' +) + +list_targets=( + 'mirror:list all mirror sites (alias: mirrors)' + 'dish:list all supported dishes (aliases: dishes, dishs)' + 'os:list operating-system dishes' + 'lang:list programming-language dishes (aliases: pl, language)' + 'ware:list software dishes (alias: software)' +) + +dishes_lang=( + 'gem:Ruby and RubyGems' + 'ruby:Ruby and RubyGems' + 'rb:Ruby and RubyGems' + 'rubygem:Ruby and RubyGems' + 'rubygems:Ruby and RubyGems' + 'bundler:Ruby and RubyGems' + 'py:Python package tools' + 'python:Python package tools' + 'pip:Python pip' + 'poetry:Python Poetry' + 'pdm:Python PDM' + 'rye:Python Rye' + 'uv:Python uv tools' + 'uv-pypi-index:uv package index' + 'uv-python-build:uv Python downloads' + 'js:JavaScript package tools' + 'javascript:JavaScript package tools' + 'node:JavaScript package tools' + 'nodejs:JavaScript package tools' + 'bun:Bun' + 'npm:npm' + 'yarn:Yarn' + 'pnpm:pnpm' + 'nvm:nvm' + 'perl:Perl and CPAN' + 'cpan:Perl and CPAN' + 'php:PHP and Composer' + 'composer:PHP and Composer' + 'lua:Lua and LuaRocks' + 'luarocks:Lua and LuaRocks' + 'go:Go modules' + 'golang:Go modules' + 'goproxy:Go modules' + 'rust:Rust crates' + 'cargo:Rust crates' + 'crate:Rust crates' + 'crates:Rust crates' + 'rustup:Rust toolchains' + 'java:Java build tools' + 'maven:Java build tools' + 'mvn:Java build tools' + 'maven-daemon:Java build tools' + 'mvnd:Java build tools' + 'gradle:Java build tools' + 'clojure:Clojure tools' + 'clojars:Clojure tools' + 'cloj:Clojure tools' + 'lein:Clojure tools' + 'dart:Dart packages' + 'pub:Dart packages' + 'flutter:Flutter packages' + 'nuget:NuGet' + 'net:NuGet' + '.net:NuGet' + 'dotnet:NuGet' + 'haskell:Haskell packages' + 'cabal:Haskell packages' + 'stack:Haskell packages' + 'hackage:Haskell packages' + 'ocaml:OCaml packages' + 'opam:OCaml packages' + 'r:R and CRAN' + 'cran:R and CRAN' + 'julia:Julia packages' +) + +dishes_os=( + 'ubuntu:Ubuntu' + 'zorinos:Zorin OS' + 'linuxmint:Linux Mint' + 'debian:Debian' + 'fedora:Fedora Linux' + 'opensuse:openSUSE' + 'opensuse-leap:openSUSE Leap' + 'opensuse-tumbleweed:openSUSE Tumbleweed' + 'kali:Kali Linux' + 'msys2:MSYS2' + 'msys:MSYS2' + 'arch:Arch Linux' + 'archlinux:Arch Linux' + 'archlinuxcn:Arch Linux CN repository' + 'archcn:Arch Linux CN repository' + 'manjaro:Manjaro Linux' + 'omarchy:Omarchy OPR repository' + 'gentoo:Gentoo Linux' + 'rocky:Rocky Linux' + 'rockylinux:Rocky Linux' + 'alma:AlmaLinux' + 'almalinux:AlmaLinux' + 'alpine:Alpine Linux' + 'voidlinux:Void Linux' + 'solus:Solus' + 'trisquel:Trisquel' + 'linuxlite:Linux Lite' + 'ros:Robot Operating System' + 'ros2:Robot Operating System' + 'raspi:Raspberry Pi OS' + 'raspberrypi:Raspberry Pi OS' + 'armbian:Armbian' + 'openwrt:OpenWrt' + 'opkg:OpenWrt' + 'LEDE:OpenWrt' + 'termux:Termux' + 'openkylin:openKylin' + 'openeuler:openEuler' + 'anolis:Anolis OS' + 'openanolis:Anolis OS' + 'deepin:deepin' + 'freebsd:FreeBSD' + 'netbsd:NetBSD' + 'openbsd:OpenBSD' +) + +dishes_ware=( + 'winget:WinGet' + 'brew:Homebrew' + 'homebrew:Homebrew' + 'cocoa:CocoaPods' + 'cocoapods:CocoaPods' + 'cocoapod:CocoaPods' + 'docker:Docker Hub' + 'dockerhub:Docker Hub' + 'flatpak:Flatpak and Flathub' + 'flathub:Flatpak and Flathub' + 'nix:Nix channels' + 'guix:GNU Guix' + 'emacs:Emacs packages' + 'elpa:Emacs packages' + 'latex:TeX packages' + 'ctan:TeX packages' + 'tex:TeX packages' + 'texlive:TeX packages' + 'miktex:TeX packages' + 'tlmgr:TeX packages' + 'mpm:TeX packages' + 'conda:Anaconda' + 'anaconda:Anaconda' +) + +mirror_choices=( + 'first:use the first source selected by the maintainers' + 'upstream:use the upstream source' +) + +if (( CURRENT == 2 )); then + if [[ ${words[CURRENT]} == -scope=* ]]; then + compset -P '-scope=' + _describe -t scopes 'scope' scope_options + return + fi + + local command_ret=1 + _describe -t commands 'chsrc command' commands && command_ret=0 + compadd -S '' -X 'option with an attached value' -- '-scope=' && command_ret=0 + return command_ret +fi + +local command=${words[2]} +local current_word=${words[CURRENT]} + +# 查找光标前的第一个位置参数。选项应位于命令与菜品之间;如果在菜品后 +# 继续提供选项,会改变 chsrc 当前解析器所识别的菜品位置。 +local dish='' +local mirror='' +local expect_scope=0 +local ret=1 +local token +integer i +[[ ${command} == -scope ]] && expect_scope=1 +for (( i = 3; i < CURRENT; i++ )); do + token=${words[i]} + if (( expect_scope )); then + expect_scope=0 + continue + fi + case ${token} in + -scope) + expect_scope=1 + ;; + -*) + ;; + *) + if [[ -z ${dish} ]]; then + dish=${token} + elif [[ -z ${mirror} ]]; then + mirror=${token} + fi + ;; + esac +done + +# CLI 同时接受 "-scope=value" 和 "-scope value"。等待作用域时只补全 +# 作用域值,并且不在菜品位置参数后继续建议选项。 +if [[ -z ${dish} && ${current_word} == -scope=* ]]; then + compset -P '-scope=' + _describe -t scopes 'scope' scope_options + return +fi + +if [[ -z ${dish} ]] && (( expect_scope )); then + _describe -t scopes 'scope' scope_options + return +fi + +case ${command} in + -dry|-scope|-scope=*|-ipv6|-local|-en|-english|-no-color|-no-colour) + if [[ -z ${dish} ]]; then + _describe -t options 'option' options && ret=0 + compadd -S '' -X 'option with an attached value' -- '-scope=' && ret=0 + return ret + else + _message 'only a help or version flag may follow leading options' + fi + ;; + + help|h|-h|-help|--help|issue|issues|isue|i|version|ver|-v|-version|--version) + _describe -t options 'option' options + ;; + + list|ls|l) + if [[ -z ${dish} ]]; then + _describe -t list-targets 'list target' list_targets && ret=0 + _describe -t dishes-lang 'programming-language dish' dishes_lang && ret=0 + _describe -t dishes-os 'operating-system dish' dishes_os && ret=0 + _describe -t dishes-ware 'software dish' dishes_ware && ret=0 + _describe -t options 'option' options && ret=0 + compadd -S '' -X 'option with an attached value' -- '-scope=' && ret=0 + return ret + else + _message 'no more arguments' + fi + ;; + + measure|mea|m|cesu|ce|c|get|g|reset|rest|r) + if [[ -z ${dish} ]]; then + _describe -t dishes-lang 'programming-language dish' dishes_lang && ret=0 + _describe -t dishes-os 'operating-system dish' dishes_os && ret=0 + _describe -t dishes-ware 'software dish' dishes_ware && ret=0 + _describe -t options 'option' options && ret=0 + compadd -S '' -X 'option with an attached value' -- '-scope=' && ret=0 + return ret + else + _message 'no more arguments' + fi + ;; + + set|s) + if [[ -z ${dish} ]]; then + _describe -t dishes-lang 'programming-language dish' dishes_lang && ret=0 + _describe -t dishes-os 'operating-system dish' dishes_os && ret=0 + _describe -t dishes-ware 'software dish' dishes_ware && ret=0 + _describe -t options 'option' options && ret=0 + compadd -S '' -X 'option with an attached value' -- '-scope=' && ret=0 + return ret + elif [[ -z ${mirror} ]]; then + _describe -t mirrors 'mirror selection' mirror_choices || + _message 'mirror code or source URL' + else + _message 'no more arguments' + fi + ;; +esac