Bläddra i källkod

Add completions for linux packages

世界 1 år sedan
förälder
incheckning
95606191d8

+ 9 - 0
.goreleaser.fury.yaml

@@ -49,10 +49,19 @@ nfpms:
       - src: release/config/config.json
         dst: /etc/sing-box/config.json
         type: config
+
       - src: release/config/sing-box.service
         dst: /usr/lib/systemd/system/sing-box.service
       - src: release/config/[email protected]
         dst: /usr/lib/systemd/system/[email protected]
+
+      - src: release/completions/sing-box.bash
+        dst: /usr/share/bash-completion/completions/sing-box.bash
+      - src: release/completions/sing-box.fish
+        dst: /usr/share/fish/vendor_completions.d/sing-box.fish
+      - src: release/completions/sing-box.zsh
+        dst: /usr/share/zsh/site-functions/_sing-box
+
       - src: LICENSE
         dst: /usr/share/licenses/sing-box/LICENSE
     deb:

+ 41 - 12
.goreleaser.yaml

@@ -1,3 +1,4 @@
+version: 2
 project_name: sing-box
 builds:
   - &template
@@ -25,7 +26,6 @@ builds:
     targets:
       - linux_386
       - linux_amd64_v1
-      - linux_amd64_v3
       - linux_arm64
       - linux_arm_6
       - linux_arm_7
@@ -33,7 +33,6 @@ builds:
       - linux_riscv64
       - linux_mips64le
       - windows_amd64_v1
-      - windows_amd64_v3
       - windows_386
       - windows_arm64
       - darwin_amd64_v1
@@ -90,8 +89,6 @@ builds:
       - android_arm64
       - android_386
       - android_amd64
-snapshot:
-  name_template: "{{ .Version }}.{{ .ShortCommit }}"
 archives:
   - &template
     id: archive
@@ -105,7 +102,7 @@ archives:
     wrap_in_directory: true
     files:
       - LICENSE
-    name_template: '{{ .ProjectName }}-{{ .Version }}-{{ .Os }}-{{ .Arch }}{{ with .Arm }}v{{ . }}{{ end }}{{ with .Mips }}_{{ . }}{{ end }}{{ if not (eq .Amd64 "v1") }}{{ .Amd64 }}{{ end }}'
+    name_template: '{{ .ProjectName }}-{{ .Version }}-{{ .Os }}-{{ .Arch }}{{ with .Arm }}v{{ . }}{{ end }}{{ if and .Mips (not (eq .Mips "hardfloat")) }}_{{ .Mips }}{{ end }}{{ if not (eq .Amd64 "v1") }}{{ .Amd64 }}{{ end }}'
   - id: archive-legacy
     <<: *template
     builds:
@@ -114,7 +111,7 @@ archives:
 nfpms:
   - id: package
     package_name: sing-box
-    file_name_template: '{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}{{ with .Arm }}v{{ . }}{{ end }}{{ with .Mips }}_{{ . }}{{ end }}{{ if not (eq .Amd64 "v1") }}{{ .Amd64 }}{{ end }}'
+    file_name_template: '{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}{{ with .Arm }}v{{ . }}{{ end }}{{ if and .Mips (not (eq .Mips "hardfloat")) }}_{{ .Mips }}{{ end }}{{ if not (eq .Amd64 "v1") }}{{ .Amd64 }}{{ end }}'
     builds:
       - main
     homepage: https://sing-box.sagernet.org/
@@ -125,15 +122,26 @@ nfpms:
       - deb
       - rpm
       - archlinux
+#      - apk
+#      - ipk
     priority: extra
     contents:
       - src: release/config/config.json
         dst: /etc/sing-box/config.json
         type: config
+
       - src: release/config/sing-box.service
         dst: /usr/lib/systemd/system/sing-box.service
       - src: release/config/[email protected]
         dst: /usr/lib/systemd/system/[email protected]
+
+      - src: release/completions/sing-box.bash
+        dst: /usr/share/bash-completion/completions/sing-box.bash
+      - src: release/completions/sing-box.fish
+        dst: /usr/share/fish/vendor_completions.d/sing-box.fish
+      - src: release/completions/sing-box.zsh
+        dst: /usr/share/zsh/site-functions/_sing-box
+
       - src: LICENSE
         dst: /usr/share/licenses/sing-box/LICENSE
     deb:
@@ -145,13 +153,34 @@ nfpms:
       signature:
         key_file: "{{ .Env.NFPM_KEY_PATH }}"
     overrides:
-      deb:
-        conflicts:
-          - sing-box-beta
-      rpm:
-        conflicts:
-          - sing-box-beta
+      apk:
+        contents:
+          - src: release/config/config.json
+            dst: /etc/sing-box/config.json
+            type: config
+
+          - src: release/config/sing-box.initd
+            dst: /etc/init.d/sing-box
+
+          - src: release/completions/sing-box.bash
+            dst: /usr/share/bash-completion/completions/sing-box.bash
+          - src: release/completions/sing-box.fish
+            dst: /usr/share/fish/vendor_completions.d/sing-box.fish
+          - src: release/completions/sing-box.zsh
+            dst: /usr/share/zsh/site-functions/_sing-box
+
+          - src: LICENSE
+            dst: /usr/share/licenses/sing-box/LICENSE
+      ipk:
+        contents:
+          - src: release/config/config.json
+            dst: /etc/sing-box/config.json
+            type: config
 
+          - src: release/config/openwrt.init
+            dst: /etc/init.d/sing-box
+          - src: release/config/openwrt.conf
+            dst: /etc/config/sing-box
 source:
   enabled: false
   name_template: '{{ .ProjectName }}-{{ .Version }}.source'

+ 3 - 1
Makefile

@@ -32,6 +32,9 @@ ci_build:
 	go build $(PARAMS) $(MAIN)
 	go build $(MAIN_PARAMS) $(MAIN)
 
+generate_completions:
+	go run -v --tags generate,generate_completions $(MAIN)
+
 install:
 	go build -o $(PREFIX)/bin/$(NAME) $(MAIN_PARAMS) $(MAIN)
 
@@ -71,7 +74,6 @@ release:
 		dist/*.deb \
 		dist/*.rpm \
 		dist/*_amd64.pkg.tar.zst \
-		dist/*_amd64v3.pkg.tar.zst \
 		dist/*_arm64.pkg.tar.zst \
 		dist/release
 	ghr --replace --draft --prerelease -p 3 "v${VERSION}" dist/release

+ 68 - 0
cmd/sing-box/cmd.go

@@ -0,0 +1,68 @@
+package main
+
+import (
+	"context"
+	"os"
+	"os/user"
+	"strconv"
+	"time"
+
+	_ "github.com/sagernet/sing-box/include"
+	"github.com/sagernet/sing-box/log"
+	"github.com/sagernet/sing/service/filemanager"
+
+	"github.com/spf13/cobra"
+)
+
+var (
+	globalCtx         context.Context
+	configPaths       []string
+	configDirectories []string
+	workingDir        string
+	disableColor      bool
+)
+
+var mainCommand = &cobra.Command{
+	Use:              "sing-box",
+	PersistentPreRun: preRun,
+}
+
+func init() {
+	mainCommand.PersistentFlags().StringArrayVarP(&configPaths, "config", "c", nil, "set configuration file path")
+	mainCommand.PersistentFlags().StringArrayVarP(&configDirectories, "config-directory", "C", nil, "set configuration directory path")
+	mainCommand.PersistentFlags().StringVarP(&workingDir, "directory", "D", "", "set working directory")
+	mainCommand.PersistentFlags().BoolVarP(&disableColor, "disable-color", "", false, "disable color output")
+}
+
+func preRun(cmd *cobra.Command, args []string) {
+	globalCtx = context.Background()
+	sudoUser := os.Getenv("SUDO_USER")
+	sudoUID, _ := strconv.Atoi(os.Getenv("SUDO_UID"))
+	sudoGID, _ := strconv.Atoi(os.Getenv("SUDO_GID"))
+	if sudoUID == 0 && sudoGID == 0 && sudoUser != "" {
+		sudoUserObject, _ := user.Lookup(sudoUser)
+		if sudoUserObject != nil {
+			sudoUID, _ = strconv.Atoi(sudoUserObject.Uid)
+			sudoGID, _ = strconv.Atoi(sudoUserObject.Gid)
+		}
+	}
+	if sudoUID > 0 && sudoGID > 0 {
+		globalCtx = filemanager.WithDefault(globalCtx, "", "", sudoUID, sudoGID)
+	}
+	if disableColor {
+		log.SetStdLogger(log.NewDefaultFactory(context.Background(), log.Formatter{BaseTime: time.Now(), DisableColors: true}, os.Stderr, "", nil, false).Logger())
+	}
+	if workingDir != "" {
+		_, err := os.Stat(workingDir)
+		if err != nil {
+			filemanager.MkdirAll(globalCtx, workingDir, 0o777)
+		}
+		err = os.Chdir(workingDir)
+		if err != nil {
+			log.Fatal(err)
+		}
+	}
+	if len(configPaths) == 0 && len(configDirectories) == 0 {
+		configPaths = append(configPaths, "config.json")
+	}
+}

+ 28 - 0
cmd/sing-box/generate_completions.go

@@ -0,0 +1,28 @@
+//go:build generate && generate_completions
+
+package main
+
+import "github.com/sagernet/sing-box/log"
+
+func main() {
+	err := generateCompletions()
+	if err != nil {
+		log.Fatal(err)
+	}
+}
+
+func generateCompletions() error {
+	err := mainCommand.GenBashCompletionFile("release/completions/sing-box.bash")
+	if err != nil {
+		return err
+	}
+	err = mainCommand.GenFishCompletionFile("release/completions/sing-box.fish", true)
+	if err != nil {
+		return err
+	}
+	err = mainCommand.GenZshCompletionFile("release/completions/sing-box.zsh")
+	if err != nil {
+		return err
+	}
+	return nil
+}

+ 3 - 66
cmd/sing-box/main.go

@@ -1,74 +1,11 @@
-package main
-
-import (
-	"context"
-	"os"
-	"os/user"
-	"strconv"
-	"time"
-
-	_ "github.com/sagernet/sing-box/include"
-	"github.com/sagernet/sing-box/log"
-	"github.com/sagernet/sing/service/filemanager"
-
-	"github.com/spf13/cobra"
-)
+//go:build !generate
 
-var (
-	globalCtx         context.Context
-	configPaths       []string
-	configDirectories []string
-	workingDir        string
-	disableColor      bool
-)
-
-var mainCommand = &cobra.Command{
-	Use:              "sing-box",
-	PersistentPreRun: preRun,
-}
+package main
 
-func init() {
-	mainCommand.PersistentFlags().StringArrayVarP(&configPaths, "config", "c", nil, "set configuration file path")
-	mainCommand.PersistentFlags().StringArrayVarP(&configDirectories, "config-directory", "C", nil, "set configuration directory path")
-	mainCommand.PersistentFlags().StringVarP(&workingDir, "directory", "D", "", "set working directory")
-	mainCommand.PersistentFlags().BoolVarP(&disableColor, "disable-color", "", false, "disable color output")
-}
+import "github.com/sagernet/sing-box/log"
 
 func main() {
 	if err := mainCommand.Execute(); err != nil {
 		log.Fatal(err)
 	}
 }
-
-func preRun(cmd *cobra.Command, args []string) {
-	globalCtx = context.Background()
-	sudoUser := os.Getenv("SUDO_USER")
-	sudoUID, _ := strconv.Atoi(os.Getenv("SUDO_UID"))
-	sudoGID, _ := strconv.Atoi(os.Getenv("SUDO_GID"))
-	if sudoUID == 0 && sudoGID == 0 && sudoUser != "" {
-		sudoUserObject, _ := user.Lookup(sudoUser)
-		if sudoUserObject != nil {
-			sudoUID, _ = strconv.Atoi(sudoUserObject.Uid)
-			sudoGID, _ = strconv.Atoi(sudoUserObject.Gid)
-		}
-	}
-	if sudoUID > 0 && sudoGID > 0 {
-		globalCtx = filemanager.WithDefault(globalCtx, "", "", sudoUID, sudoGID)
-	}
-	if disableColor {
-		log.SetStdLogger(log.NewDefaultFactory(context.Background(), log.Formatter{BaseTime: time.Now(), DisableColors: true}, os.Stderr, "", nil, false).Logger())
-	}
-	if workingDir != "" {
-		_, err := os.Stat(workingDir)
-		if err != nil {
-			filemanager.MkdirAll(globalCtx, workingDir, 0o777)
-		}
-		err = os.Chdir(workingDir)
-		if err != nil {
-			log.Fatal(err)
-		}
-	}
-	if len(configPaths) == 0 && len(configDirectories) == 0 {
-		configPaths = append(configPaths, "config.json")
-	}
-}

+ 1549 - 0
release/completions/sing-box.bash

@@ -0,0 +1,1549 @@
+# bash completion for sing-box                             -*- shell-script -*-
+
+__sing-box_debug()
+{
+    if [[ -n ${BASH_COMP_DEBUG_FILE:-} ]]; then
+        echo "$*" >> "${BASH_COMP_DEBUG_FILE}"
+    fi
+}
+
+# Homebrew on Macs have version 1.3 of bash-completion which doesn't include
+# _init_completion. This is a very minimal version of that function.
+__sing-box_init_completion()
+{
+    COMPREPLY=()
+    _get_comp_words_by_ref "$@" cur prev words cword
+}
+
+__sing-box_index_of_word()
+{
+    local w word=$1
+    shift
+    index=0
+    for w in "$@"; do
+        [[ $w = "$word" ]] && return
+        index=$((index+1))
+    done
+    index=-1
+}
+
+__sing-box_contains_word()
+{
+    local w word=$1; shift
+    for w in "$@"; do
+        [[ $w = "$word" ]] && return
+    done
+    return 1
+}
+
+__sing-box_handle_go_custom_completion()
+{
+    __sing-box_debug "${FUNCNAME[0]}: cur is ${cur}, words[*] is ${words[*]}, #words[@] is ${#words[@]}"
+
+    local shellCompDirectiveError=1
+    local shellCompDirectiveNoSpace=2
+    local shellCompDirectiveNoFileComp=4
+    local shellCompDirectiveFilterFileExt=8
+    local shellCompDirectiveFilterDirs=16
+
+    local out requestComp lastParam lastChar comp directive args
+
+    # Prepare the command to request completions for the program.
+    # Calling ${words[0]} instead of directly sing-box allows handling aliases
+    args=("${words[@]:1}")
+    # Disable ActiveHelp which is not supported for bash completion v1
+    requestComp="SING_BOX_ACTIVE_HELP=0 ${words[0]} __completeNoDesc ${args[*]}"
+
+    lastParam=${words[$((${#words[@]}-1))]}
+    lastChar=${lastParam:$((${#lastParam}-1)):1}
+    __sing-box_debug "${FUNCNAME[0]}: lastParam ${lastParam}, lastChar ${lastChar}"
+
+    if [ -z "${cur}" ] && [ "${lastChar}" != "=" ]; then
+        # If the last parameter is complete (there is a space following it)
+        # We add an extra empty parameter so we can indicate this to the go method.
+        __sing-box_debug "${FUNCNAME[0]}: Adding extra empty parameter"
+        requestComp="${requestComp} \"\""
+    fi
+
+    __sing-box_debug "${FUNCNAME[0]}: calling ${requestComp}"
+    # Use eval to handle any environment variables and such
+    out=$(eval "${requestComp}" 2>/dev/null)
+
+    # Extract the directive integer at the very end of the output following a colon (:)
+    directive=${out##*:}
+    # Remove the directive
+    out=${out%:*}
+    if [ "${directive}" = "${out}" ]; then
+        # There is not directive specified
+        directive=0
+    fi
+    __sing-box_debug "${FUNCNAME[0]}: the completion directive is: ${directive}"
+    __sing-box_debug "${FUNCNAME[0]}: the completions are: ${out}"
+
+    if [ $((directive & shellCompDirectiveError)) -ne 0 ]; then
+        # Error code.  No completion.
+        __sing-box_debug "${FUNCNAME[0]}: received error from custom completion go code"
+        return
+    else
+        if [ $((directive & shellCompDirectiveNoSpace)) -ne 0 ]; then
+            if [[ $(type -t compopt) = "builtin" ]]; then
+                __sing-box_debug "${FUNCNAME[0]}: activating no space"
+                compopt -o nospace
+            fi
+        fi
+        if [ $((directive & shellCompDirectiveNoFileComp)) -ne 0 ]; then
+            if [[ $(type -t compopt) = "builtin" ]]; then
+                __sing-box_debug "${FUNCNAME[0]}: activating no file completion"
+                compopt +o default
+            fi
+        fi
+    fi
+
+    if [ $((directive & shellCompDirectiveFilterFileExt)) -ne 0 ]; then
+        # File extension filtering
+        local fullFilter filter filteringCmd
+        # Do not use quotes around the $out variable or else newline
+        # characters will be kept.
+        for filter in ${out}; do
+            fullFilter+="$filter|"
+        done
+
+        filteringCmd="_filedir $fullFilter"
+        __sing-box_debug "File filtering command: $filteringCmd"
+        $filteringCmd
+    elif [ $((directive & shellCompDirectiveFilterDirs)) -ne 0 ]; then
+        # File completion for directories only
+        local subdir
+        # Use printf to strip any trailing newline
+        subdir=$(printf "%s" "${out}")
+        if [ -n "$subdir" ]; then
+            __sing-box_debug "Listing directories in $subdir"
+            __sing-box_handle_subdirs_in_dir_flag "$subdir"
+        else
+            __sing-box_debug "Listing directories in ."
+            _filedir -d
+        fi
+    else
+        while IFS='' read -r comp; do
+            COMPREPLY+=("$comp")
+        done < <(compgen -W "${out}" -- "$cur")
+    fi
+}
+
+__sing-box_handle_reply()
+{
+    __sing-box_debug "${FUNCNAME[0]}"
+    local comp
+    case $cur in
+        -*)
+            if [[ $(type -t compopt) = "builtin" ]]; then
+                compopt -o nospace
+            fi
+            local allflags
+            if [ ${#must_have_one_flag[@]} -ne 0 ]; then
+                allflags=("${must_have_one_flag[@]}")
+            else
+                allflags=("${flags[*]} ${two_word_flags[*]}")
+            fi
+            while IFS='' read -r comp; do
+                COMPREPLY+=("$comp")
+            done < <(compgen -W "${allflags[*]}" -- "$cur")
+            if [[ $(type -t compopt) = "builtin" ]]; then
+                [[ "${COMPREPLY[0]}" == *= ]] || compopt +o nospace
+            fi
+
+            # complete after --flag=abc
+            if [[ $cur == *=* ]]; then
+                if [[ $(type -t compopt) = "builtin" ]]; then
+                    compopt +o nospace
+                fi
+
+                local index flag
+                flag="${cur%=*}"
+                __sing-box_index_of_word "${flag}" "${flags_with_completion[@]}"
+                COMPREPLY=()
+                if [[ ${index} -ge 0 ]]; then
+                    PREFIX=""
+                    cur="${cur#*=}"
+                    ${flags_completion[${index}]}
+                    if [ -n "${ZSH_VERSION:-}" ]; then
+                        # zsh completion needs --flag= prefix
+                        eval "COMPREPLY=( \"\${COMPREPLY[@]/#/${flag}=}\" )"
+                    fi
+                fi
+            fi
+
+            if [[ -z "${flag_parsing_disabled}" ]]; then
+                # If flag parsing is enabled, we have completed the flags and can return.
+                # If flag parsing is disabled, we may not know all (or any) of the flags, so we fallthrough
+                # to possibly call handle_go_custom_completion.
+                return 0;
+            fi
+            ;;
+    esac
+
+    # check if we are handling a flag with special work handling
+    local index
+    __sing-box_index_of_word "${prev}" "${flags_with_completion[@]}"
+    if [[ ${index} -ge 0 ]]; then
+        ${flags_completion[${index}]}
+        return
+    fi
+
+    # we are parsing a flag and don't have a special handler, no completion
+    if [[ ${cur} != "${words[cword]}" ]]; then
+        return
+    fi
+
+    local completions
+    completions=("${commands[@]}")
+    if [[ ${#must_have_one_noun[@]} -ne 0 ]]; then
+        completions+=("${must_have_one_noun[@]}")
+    elif [[ -n "${has_completion_function}" ]]; then
+        # if a go completion function is provided, defer to that function
+        __sing-box_handle_go_custom_completion
+    fi
+    if [[ ${#must_have_one_flag[@]} -ne 0 ]]; then
+        completions+=("${must_have_one_flag[@]}")
+    fi
+    while IFS='' read -r comp; do
+        COMPREPLY+=("$comp")
+    done < <(compgen -W "${completions[*]}" -- "$cur")
+
+    if [[ ${#COMPREPLY[@]} -eq 0 && ${#noun_aliases[@]} -gt 0 && ${#must_have_one_noun[@]} -ne 0 ]]; then
+        while IFS='' read -r comp; do
+            COMPREPLY+=("$comp")
+        done < <(compgen -W "${noun_aliases[*]}" -- "$cur")
+    fi
+
+    if [[ ${#COMPREPLY[@]} -eq 0 ]]; then
+        if declare -F __sing-box_custom_func >/dev/null; then
+            # try command name qualified custom func
+            __sing-box_custom_func
+        else
+            # otherwise fall back to unqualified for compatibility
+            declare -F __custom_func >/dev/null && __custom_func
+        fi
+    fi
+
+    # available in bash-completion >= 2, not always present on macOS
+    if declare -F __ltrim_colon_completions >/dev/null; then
+        __ltrim_colon_completions "$cur"
+    fi
+
+    # If there is only 1 completion and it is a flag with an = it will be completed
+    # but we don't want a space after the =
+    if [[ "${#COMPREPLY[@]}" -eq "1" ]] && [[ $(type -t compopt) = "builtin" ]] && [[ "${COMPREPLY[0]}" == --*= ]]; then
+       compopt -o nospace
+    fi
+}
+
+# The arguments should be in the form "ext1|ext2|extn"
+__sing-box_handle_filename_extension_flag()
+{
+    local ext="$1"
+    _filedir "@(${ext})"
+}
+
+__sing-box_handle_subdirs_in_dir_flag()
+{
+    local dir="$1"
+    pushd "${dir}" >/dev/null 2>&1 && _filedir -d && popd >/dev/null 2>&1 || return
+}
+
+__sing-box_handle_flag()
+{
+    __sing-box_debug "${FUNCNAME[0]}: c is $c words[c] is ${words[c]}"
+
+    # if a command required a flag, and we found it, unset must_have_one_flag()
+    local flagname=${words[c]}
+    local flagvalue=""
+    # if the word contained an =
+    if [[ ${words[c]} == *"="* ]]; then
+        flagvalue=${flagname#*=} # take in as flagvalue after the =
+        flagname=${flagname%=*} # strip everything after the =
+        flagname="${flagname}=" # but put the = back
+    fi
+    __sing-box_debug "${FUNCNAME[0]}: looking for ${flagname}"
+    if __sing-box_contains_word "${flagname}" "${must_have_one_flag[@]}"; then
+        must_have_one_flag=()
+    fi
+
+    # if you set a flag which only applies to this command, don't show subcommands
+    if __sing-box_contains_word "${flagname}" "${local_nonpersistent_flags[@]}"; then
+      commands=()
+    fi
+
+    # keep flag value with flagname as flaghash
+    # flaghash variable is an associative array which is only supported in bash > 3.
+    if [[ -z "${BASH_VERSION:-}" || "${BASH_VERSINFO[0]:-}" -gt 3 ]]; then
+        if [ -n "${flagvalue}" ] ; then
+            flaghash[${flagname}]=${flagvalue}
+        elif [ -n "${words[ $((c+1)) ]}" ] ; then
+            flaghash[${flagname}]=${words[ $((c+1)) ]}
+        else
+            flaghash[${flagname}]="true" # pad "true" for bool flag
+        fi
+    fi
+
+    # skip the argument to a two word flag
+    if [[ ${words[c]} != *"="* ]] && __sing-box_contains_word "${words[c]}" "${two_word_flags[@]}"; then
+        __sing-box_debug "${FUNCNAME[0]}: found a flag ${words[c]}, skip the next argument"
+        c=$((c+1))
+        # if we are looking for a flags value, don't show commands
+        if [[ $c -eq $cword ]]; then
+            commands=()
+        fi
+    fi
+
+    c=$((c+1))
+
+}
+
+__sing-box_handle_noun()
+{
+    __sing-box_debug "${FUNCNAME[0]}: c is $c words[c] is ${words[c]}"
+
+    if __sing-box_contains_word "${words[c]}" "${must_have_one_noun[@]}"; then
+        must_have_one_noun=()
+    elif __sing-box_contains_word "${words[c]}" "${noun_aliases[@]}"; then
+        must_have_one_noun=()
+    fi
+
+    nouns+=("${words[c]}")
+    c=$((c+1))
+}
+
+__sing-box_handle_command()
+{
+    __sing-box_debug "${FUNCNAME[0]}: c is $c words[c] is ${words[c]}"
+
+    local next_command
+    if [[ -n ${last_command} ]]; then
+        next_command="_${last_command}_${words[c]//:/__}"
+    else
+        if [[ $c -eq 0 ]]; then
+            next_command="_sing-box_root_command"
+        else
+            next_command="_${words[c]//:/__}"
+        fi
+    fi
+    c=$((c+1))
+    __sing-box_debug "${FUNCNAME[0]}: looking for ${next_command}"
+    declare -F "$next_command" >/dev/null && $next_command
+}
+
+__sing-box_handle_word()
+{
+    if [[ $c -ge $cword ]]; then
+        __sing-box_handle_reply
+        return
+    fi
+    __sing-box_debug "${FUNCNAME[0]}: c is $c words[c] is ${words[c]}"
+    if [[ "${words[c]}" == -* ]]; then
+        __sing-box_handle_flag
+    elif __sing-box_contains_word "${words[c]}" "${commands[@]}"; then
+        __sing-box_handle_command
+    elif [[ $c -eq 0 ]]; then
+        __sing-box_handle_command
+    elif __sing-box_contains_word "${words[c]}" "${command_aliases[@]}"; then
+        # aliashash variable is an associative array which is only supported in bash > 3.
+        if [[ -z "${BASH_VERSION:-}" || "${BASH_VERSINFO[0]:-}" -gt 3 ]]; then
+            words[c]=${aliashash[${words[c]}]}
+            __sing-box_handle_command
+        else
+            __sing-box_handle_noun
+        fi
+    else
+        __sing-box_handle_noun
+    fi
+    __sing-box_handle_word
+}
+
+_sing-box_check()
+{
+    last_command="sing-box_check"
+
+    command_aliases=()
+
+    commands=()
+
+    flags=()
+    two_word_flags=()
+    local_nonpersistent_flags=()
+    flags_with_completion=()
+    flags_completion=()
+
+    flags+=("--config=")
+    two_word_flags+=("--config")
+    two_word_flags+=("-c")
+    flags+=("--config-directory=")
+    two_word_flags+=("--config-directory")
+    two_word_flags+=("-C")
+    flags+=("--directory=")
+    two_word_flags+=("--directory")
+    two_word_flags+=("-D")
+    flags+=("--disable-color")
+
+    must_have_one_flag=()
+    must_have_one_noun=()
+    noun_aliases=()
+}
+
+_sing-box_format()
+{
+    last_command="sing-box_format"
+
+    command_aliases=()
+
+    commands=()
+
+    flags=()
+    two_word_flags=()
+    local_nonpersistent_flags=()
+    flags_with_completion=()
+    flags_completion=()
+
+    flags+=("--write")
+    flags+=("-w")
+    local_nonpersistent_flags+=("--write")
+    local_nonpersistent_flags+=("-w")
+    flags+=("--config=")
+    two_word_flags+=("--config")
+    two_word_flags+=("-c")
+    flags+=("--config-directory=")
+    two_word_flags+=("--config-directory")
+    two_word_flags+=("-C")
+    flags+=("--directory=")
+    two_word_flags+=("--directory")
+    two_word_flags+=("-D")
+    flags+=("--disable-color")
+
+    must_have_one_flag=()
+    must_have_one_noun=()
+    noun_aliases=()
+}
+
+_sing-box_generate_ech-keypair()
+{
+    last_command="sing-box_generate_ech-keypair"
+
+    command_aliases=()
+
+    commands=()
+
+    flags=()
+    two_word_flags=()
+    local_nonpersistent_flags=()
+    flags_with_completion=()
+    flags_completion=()
+
+    flags+=("--pq-signature-schemes-enabled")
+    local_nonpersistent_flags+=("--pq-signature-schemes-enabled")
+    flags+=("--config=")
+    two_word_flags+=("--config")
+    two_word_flags+=("-c")
+    flags+=("--config-directory=")
+    two_word_flags+=("--config-directory")
+    two_word_flags+=("-C")
+    flags+=("--directory=")
+    two_word_flags+=("--directory")
+    two_word_flags+=("-D")
+    flags+=("--disable-color")
+
+    must_have_one_flag=()
+    must_have_one_noun=()
+    noun_aliases=()
+}
+
+_sing-box_generate_rand()
+{
+    last_command="sing-box_generate_rand"
+
+    command_aliases=()
+
+    commands=()
+
+    flags=()
+    two_word_flags=()
+    local_nonpersistent_flags=()
+    flags_with_completion=()
+    flags_completion=()
+
+    flags+=("--base64")
+    local_nonpersistent_flags+=("--base64")
+    flags+=("--hex")
+    local_nonpersistent_flags+=("--hex")
+    flags+=("--config=")
+    two_word_flags+=("--config")
+    two_word_flags+=("-c")
+    flags+=("--config-directory=")
+    two_word_flags+=("--config-directory")
+    two_word_flags+=("-C")
+    flags+=("--directory=")
+    two_word_flags+=("--directory")
+    two_word_flags+=("-D")
+    flags+=("--disable-color")
+
+    must_have_one_flag=()
+    must_have_one_noun=()
+    noun_aliases=()
+}
+
+_sing-box_generate_reality-keypair()
+{
+    last_command="sing-box_generate_reality-keypair"
+
+    command_aliases=()
+
+    commands=()
+
+    flags=()
+    two_word_flags=()
+    local_nonpersistent_flags=()
+    flags_with_completion=()
+    flags_completion=()
+
+    flags+=("--config=")
+    two_word_flags+=("--config")
+    two_word_flags+=("-c")
+    flags+=("--config-directory=")
+    two_word_flags+=("--config-directory")
+    two_word_flags+=("-C")
+    flags+=("--directory=")
+    two_word_flags+=("--directory")
+    two_word_flags+=("-D")
+    flags+=("--disable-color")
+
+    must_have_one_flag=()
+    must_have_one_noun=()
+    noun_aliases=()
+}
+
+_sing-box_generate_tls-keypair()
+{
+    last_command="sing-box_generate_tls-keypair"
+
+    command_aliases=()
+
+    commands=()
+
+    flags=()
+    two_word_flags=()
+    local_nonpersistent_flags=()
+    flags_with_completion=()
+    flags_completion=()
+
+    flags+=("--months=")
+    two_word_flags+=("--months")
+    two_word_flags+=("-m")
+    local_nonpersistent_flags+=("--months")
+    local_nonpersistent_flags+=("--months=")
+    local_nonpersistent_flags+=("-m")
+    flags+=("--config=")
+    two_word_flags+=("--config")
+    two_word_flags+=("-c")
+    flags+=("--config-directory=")
+    two_word_flags+=("--config-directory")
+    two_word_flags+=("-C")
+    flags+=("--directory=")
+    two_word_flags+=("--directory")
+    two_word_flags+=("-D")
+    flags+=("--disable-color")
+
+    must_have_one_flag=()
+    must_have_one_noun=()
+    noun_aliases=()
+}
+
+_sing-box_generate_uuid()
+{
+    last_command="sing-box_generate_uuid"
+
+    command_aliases=()
+
+    commands=()
+
+    flags=()
+    two_word_flags=()
+    local_nonpersistent_flags=()
+    flags_with_completion=()
+    flags_completion=()
+
+    flags+=("--config=")
+    two_word_flags+=("--config")
+    two_word_flags+=("-c")
+    flags+=("--config-directory=")
+    two_word_flags+=("--config-directory")
+    two_word_flags+=("-C")
+    flags+=("--directory=")
+    two_word_flags+=("--directory")
+    two_word_flags+=("-D")
+    flags+=("--disable-color")
+
+    must_have_one_flag=()
+    must_have_one_noun=()
+    noun_aliases=()
+}
+
+_sing-box_generate_vapid-keypair()
+{
+    last_command="sing-box_generate_vapid-keypair"
+
+    command_aliases=()
+
+    commands=()
+
+    flags=()
+    two_word_flags=()
+    local_nonpersistent_flags=()
+    flags_with_completion=()
+    flags_completion=()
+
+    flags+=("--config=")
+    two_word_flags+=("--config")
+    two_word_flags+=("-c")
+    flags+=("--config-directory=")
+    two_word_flags+=("--config-directory")
+    two_word_flags+=("-C")
+    flags+=("--directory=")
+    two_word_flags+=("--directory")
+    two_word_flags+=("-D")
+    flags+=("--disable-color")
+
+    must_have_one_flag=()
+    must_have_one_noun=()
+    noun_aliases=()
+}
+
+_sing-box_generate_wg-keypair()
+{
+    last_command="sing-box_generate_wg-keypair"
+
+    command_aliases=()
+
+    commands=()
+
+    flags=()
+    two_word_flags=()
+    local_nonpersistent_flags=()
+    flags_with_completion=()
+    flags_completion=()
+
+    flags+=("--config=")
+    two_word_flags+=("--config")
+    two_word_flags+=("-c")
+    flags+=("--config-directory=")
+    two_word_flags+=("--config-directory")
+    two_word_flags+=("-C")
+    flags+=("--directory=")
+    two_word_flags+=("--directory")
+    two_word_flags+=("-D")
+    flags+=("--disable-color")
+
+    must_have_one_flag=()
+    must_have_one_noun=()
+    noun_aliases=()
+}
+
+_sing-box_generate()
+{
+    last_command="sing-box_generate"
+
+    command_aliases=()
+
+    commands=()
+    commands+=("ech-keypair")
+    commands+=("rand")
+    commands+=("reality-keypair")
+    commands+=("tls-keypair")
+    commands+=("uuid")
+    commands+=("vapid-keypair")
+    commands+=("wg-keypair")
+
+    flags=()
+    two_word_flags=()
+    local_nonpersistent_flags=()
+    flags_with_completion=()
+    flags_completion=()
+
+    flags+=("--config=")
+    two_word_flags+=("--config")
+    two_word_flags+=("-c")
+    flags+=("--config-directory=")
+    two_word_flags+=("--config-directory")
+    two_word_flags+=("-C")
+    flags+=("--directory=")
+    two_word_flags+=("--directory")
+    two_word_flags+=("-D")
+    flags+=("--disable-color")
+
+    must_have_one_flag=()
+    must_have_one_noun=()
+    noun_aliases=()
+}
+
+_sing-box_geoip_export()
+{
+    last_command="sing-box_geoip_export"
+
+    command_aliases=()
+
+    commands=()
+
+    flags=()
+    two_word_flags=()
+    local_nonpersistent_flags=()
+    flags_with_completion=()
+    flags_completion=()
+
+    flags+=("--output=")
+    two_word_flags+=("--output")
+    two_word_flags+=("-o")
+    local_nonpersistent_flags+=("--output")
+    local_nonpersistent_flags+=("--output=")
+    local_nonpersistent_flags+=("-o")
+    flags+=("--config=")
+    two_word_flags+=("--config")
+    two_word_flags+=("-c")
+    flags+=("--config-directory=")
+    two_word_flags+=("--config-directory")
+    two_word_flags+=("-C")
+    flags+=("--directory=")
+    two_word_flags+=("--directory")
+    two_word_flags+=("-D")
+    flags+=("--disable-color")
+    flags+=("--file=")
+    two_word_flags+=("--file")
+    two_word_flags+=("-f")
+
+    must_have_one_flag=()
+    must_have_one_noun=()
+    noun_aliases=()
+}
+
+_sing-box_geoip_list()
+{
+    last_command="sing-box_geoip_list"
+
+    command_aliases=()
+
+    commands=()
+
+    flags=()
+    two_word_flags=()
+    local_nonpersistent_flags=()
+    flags_with_completion=()
+    flags_completion=()
+
+    flags+=("--config=")
+    two_word_flags+=("--config")
+    two_word_flags+=("-c")
+    flags+=("--config-directory=")
+    two_word_flags+=("--config-directory")
+    two_word_flags+=("-C")
+    flags+=("--directory=")
+    two_word_flags+=("--directory")
+    two_word_flags+=("-D")
+    flags+=("--disable-color")
+    flags+=("--file=")
+    two_word_flags+=("--file")
+    two_word_flags+=("-f")
+
+    must_have_one_flag=()
+    must_have_one_noun=()
+    noun_aliases=()
+}
+
+_sing-box_geoip_lookup()
+{
+    last_command="sing-box_geoip_lookup"
+
+    command_aliases=()
+
+    commands=()
+
+    flags=()
+    two_word_flags=()
+    local_nonpersistent_flags=()
+    flags_with_completion=()
+    flags_completion=()
+
+    flags+=("--config=")
+    two_word_flags+=("--config")
+    two_word_flags+=("-c")
+    flags+=("--config-directory=")
+    two_word_flags+=("--config-directory")
+    two_word_flags+=("-C")
+    flags+=("--directory=")
+    two_word_flags+=("--directory")
+    two_word_flags+=("-D")
+    flags+=("--disable-color")
+    flags+=("--file=")
+    two_word_flags+=("--file")
+    two_word_flags+=("-f")
+
+    must_have_one_flag=()
+    must_have_one_noun=()
+    noun_aliases=()
+}
+
+_sing-box_geoip()
+{
+    last_command="sing-box_geoip"
+
+    command_aliases=()
+
+    commands=()
+    commands+=("export")
+    commands+=("list")
+    commands+=("lookup")
+
+    flags=()
+    two_word_flags=()
+    local_nonpersistent_flags=()
+    flags_with_completion=()
+    flags_completion=()
+
+    flags+=("--file=")
+    two_word_flags+=("--file")
+    two_word_flags+=("-f")
+    flags+=("--config=")
+    two_word_flags+=("--config")
+    two_word_flags+=("-c")
+    flags+=("--config-directory=")
+    two_word_flags+=("--config-directory")
+    two_word_flags+=("-C")
+    flags+=("--directory=")
+    two_word_flags+=("--directory")
+    two_word_flags+=("-D")
+    flags+=("--disable-color")
+
+    must_have_one_flag=()
+    must_have_one_noun=()
+    noun_aliases=()
+}
+
+_sing-box_geosite_export()
+{
+    last_command="sing-box_geosite_export"
+
+    command_aliases=()
+
+    commands=()
+
+    flags=()
+    two_word_flags=()
+    local_nonpersistent_flags=()
+    flags_with_completion=()
+    flags_completion=()
+
+    flags+=("--output=")
+    two_word_flags+=("--output")
+    two_word_flags+=("-o")
+    local_nonpersistent_flags+=("--output")
+    local_nonpersistent_flags+=("--output=")
+    local_nonpersistent_flags+=("-o")
+    flags+=("--config=")
+    two_word_flags+=("--config")
+    two_word_flags+=("-c")
+    flags+=("--config-directory=")
+    two_word_flags+=("--config-directory")
+    two_word_flags+=("-C")
+    flags+=("--directory=")
+    two_word_flags+=("--directory")
+    two_word_flags+=("-D")
+    flags+=("--disable-color")
+    flags+=("--file=")
+    two_word_flags+=("--file")
+    two_word_flags+=("-f")
+
+    must_have_one_flag=()
+    must_have_one_noun=()
+    noun_aliases=()
+}
+
+_sing-box_geosite_list()
+{
+    last_command="sing-box_geosite_list"
+
+    command_aliases=()
+
+    commands=()
+
+    flags=()
+    two_word_flags=()
+    local_nonpersistent_flags=()
+    flags_with_completion=()
+    flags_completion=()
+
+    flags+=("--config=")
+    two_word_flags+=("--config")
+    two_word_flags+=("-c")
+    flags+=("--config-directory=")
+    two_word_flags+=("--config-directory")
+    two_word_flags+=("-C")
+    flags+=("--directory=")
+    two_word_flags+=("--directory")
+    two_word_flags+=("-D")
+    flags+=("--disable-color")
+    flags+=("--file=")
+    two_word_flags+=("--file")
+    two_word_flags+=("-f")
+
+    must_have_one_flag=()
+    must_have_one_noun=()
+    noun_aliases=()
+}
+
+_sing-box_geosite_lookup()
+{
+    last_command="sing-box_geosite_lookup"
+
+    command_aliases=()
+
+    commands=()
+
+    flags=()
+    two_word_flags=()
+    local_nonpersistent_flags=()
+    flags_with_completion=()
+    flags_completion=()
+
+    flags+=("--config=")
+    two_word_flags+=("--config")
+    two_word_flags+=("-c")
+    flags+=("--config-directory=")
+    two_word_flags+=("--config-directory")
+    two_word_flags+=("-C")
+    flags+=("--directory=")
+    two_word_flags+=("--directory")
+    two_word_flags+=("-D")
+    flags+=("--disable-color")
+    flags+=("--file=")
+    two_word_flags+=("--file")
+    two_word_flags+=("-f")
+
+    must_have_one_flag=()
+    must_have_one_noun=()
+    noun_aliases=()
+}
+
+_sing-box_geosite()
+{
+    last_command="sing-box_geosite"
+
+    command_aliases=()
+
+    commands=()
+    commands+=("export")
+    commands+=("list")
+    commands+=("lookup")
+
+    flags=()
+    two_word_flags=()
+    local_nonpersistent_flags=()
+    flags_with_completion=()
+    flags_completion=()
+
+    flags+=("--file=")
+    two_word_flags+=("--file")
+    two_word_flags+=("-f")
+    flags+=("--config=")
+    two_word_flags+=("--config")
+    two_word_flags+=("-c")
+    flags+=("--config-directory=")
+    two_word_flags+=("--config-directory")
+    two_word_flags+=("-C")
+    flags+=("--directory=")
+    two_word_flags+=("--directory")
+    two_word_flags+=("-D")
+    flags+=("--disable-color")
+
+    must_have_one_flag=()
+    must_have_one_noun=()
+    noun_aliases=()
+}
+
+_sing-box_merge()
+{
+    last_command="sing-box_merge"
+
+    command_aliases=()
+
+    commands=()
+
+    flags=()
+    two_word_flags=()
+    local_nonpersistent_flags=()
+    flags_with_completion=()
+    flags_completion=()
+
+    flags+=("--config=")
+    two_word_flags+=("--config")
+    two_word_flags+=("-c")
+    flags+=("--config-directory=")
+    two_word_flags+=("--config-directory")
+    two_word_flags+=("-C")
+    flags+=("--directory=")
+    two_word_flags+=("--directory")
+    two_word_flags+=("-D")
+    flags+=("--disable-color")
+
+    must_have_one_flag=()
+    must_have_one_noun=()
+    noun_aliases=()
+}
+
+_sing-box_rule-set_compile()
+{
+    last_command="sing-box_rule-set_compile"
+
+    command_aliases=()
+
+    commands=()
+
+    flags=()
+    two_word_flags=()
+    local_nonpersistent_flags=()
+    flags_with_completion=()
+    flags_completion=()
+
+    flags+=("--output=")
+    two_word_flags+=("--output")
+    two_word_flags+=("-o")
+    local_nonpersistent_flags+=("--output")
+    local_nonpersistent_flags+=("--output=")
+    local_nonpersistent_flags+=("-o")
+    flags+=("--config=")
+    two_word_flags+=("--config")
+    two_word_flags+=("-c")
+    flags+=("--config-directory=")
+    two_word_flags+=("--config-directory")
+    two_word_flags+=("-C")
+    flags+=("--directory=")
+    two_word_flags+=("--directory")
+    two_word_flags+=("-D")
+    flags+=("--disable-color")
+
+    must_have_one_flag=()
+    must_have_one_noun=()
+    noun_aliases=()
+}
+
+_sing-box_rule-set_convert()
+{
+    last_command="sing-box_rule-set_convert"
+
+    command_aliases=()
+
+    commands=()
+
+    flags=()
+    two_word_flags=()
+    local_nonpersistent_flags=()
+    flags_with_completion=()
+    flags_completion=()
+
+    flags+=("--output=")
+    two_word_flags+=("--output")
+    two_word_flags+=("-o")
+    local_nonpersistent_flags+=("--output")
+    local_nonpersistent_flags+=("--output=")
+    local_nonpersistent_flags+=("-o")
+    flags+=("--type=")
+    two_word_flags+=("--type")
+    two_word_flags+=("-t")
+    local_nonpersistent_flags+=("--type")
+    local_nonpersistent_flags+=("--type=")
+    local_nonpersistent_flags+=("-t")
+    flags+=("--config=")
+    two_word_flags+=("--config")
+    two_word_flags+=("-c")
+    flags+=("--config-directory=")
+    two_word_flags+=("--config-directory")
+    two_word_flags+=("-C")
+    flags+=("--directory=")
+    two_word_flags+=("--directory")
+    two_word_flags+=("-D")
+    flags+=("--disable-color")
+
+    must_have_one_flag=()
+    must_have_one_noun=()
+    noun_aliases=()
+}
+
+_sing-box_rule-set_decompile()
+{
+    last_command="sing-box_rule-set_decompile"
+
+    command_aliases=()
+
+    commands=()
+
+    flags=()
+    two_word_flags=()
+    local_nonpersistent_flags=()
+    flags_with_completion=()
+    flags_completion=()
+
+    flags+=("--output=")
+    two_word_flags+=("--output")
+    two_word_flags+=("-o")
+    local_nonpersistent_flags+=("--output")
+    local_nonpersistent_flags+=("--output=")
+    local_nonpersistent_flags+=("-o")
+    flags+=("--config=")
+    two_word_flags+=("--config")
+    two_word_flags+=("-c")
+    flags+=("--config-directory=")
+    two_word_flags+=("--config-directory")
+    two_word_flags+=("-C")
+    flags+=("--directory=")
+    two_word_flags+=("--directory")
+    two_word_flags+=("-D")
+    flags+=("--disable-color")
+
+    must_have_one_flag=()
+    must_have_one_noun=()
+    noun_aliases=()
+}
+
+_sing-box_rule-set_format()
+{
+    last_command="sing-box_rule-set_format"
+
+    command_aliases=()
+
+    commands=()
+
+    flags=()
+    two_word_flags=()
+    local_nonpersistent_flags=()
+    flags_with_completion=()
+    flags_completion=()
+
+    flags+=("--write")
+    flags+=("-w")
+    local_nonpersistent_flags+=("--write")
+    local_nonpersistent_flags+=("-w")
+    flags+=("--config=")
+    two_word_flags+=("--config")
+    two_word_flags+=("-c")
+    flags+=("--config-directory=")
+    two_word_flags+=("--config-directory")
+    two_word_flags+=("-C")
+    flags+=("--directory=")
+    two_word_flags+=("--directory")
+    two_word_flags+=("-D")
+    flags+=("--disable-color")
+
+    must_have_one_flag=()
+    must_have_one_noun=()
+    noun_aliases=()
+}
+
+_sing-box_rule-set_match()
+{
+    last_command="sing-box_rule-set_match"
+
+    command_aliases=()
+
+    commands=()
+
+    flags=()
+    two_word_flags=()
+    local_nonpersistent_flags=()
+    flags_with_completion=()
+    flags_completion=()
+
+    flags+=("--format=")
+    two_word_flags+=("--format")
+    two_word_flags+=("-f")
+    local_nonpersistent_flags+=("--format")
+    local_nonpersistent_flags+=("--format=")
+    local_nonpersistent_flags+=("-f")
+    flags+=("--config=")
+    two_word_flags+=("--config")
+    two_word_flags+=("-c")
+    flags+=("--config-directory=")
+    two_word_flags+=("--config-directory")
+    two_word_flags+=("-C")
+    flags+=("--directory=")
+    two_word_flags+=("--directory")
+    two_word_flags+=("-D")
+    flags+=("--disable-color")
+
+    must_have_one_flag=()
+    must_have_one_noun=()
+    noun_aliases=()
+}
+
+_sing-box_rule-set_upgrade()
+{
+    last_command="sing-box_rule-set_upgrade"
+
+    command_aliases=()
+
+    commands=()
+
+    flags=()
+    two_word_flags=()
+    local_nonpersistent_flags=()
+    flags_with_completion=()
+    flags_completion=()
+
+    flags+=("--write")
+    flags+=("-w")
+    local_nonpersistent_flags+=("--write")
+    local_nonpersistent_flags+=("-w")
+    flags+=("--config=")
+    two_word_flags+=("--config")
+    two_word_flags+=("-c")
+    flags+=("--config-directory=")
+    two_word_flags+=("--config-directory")
+    two_word_flags+=("-C")
+    flags+=("--directory=")
+    two_word_flags+=("--directory")
+    two_word_flags+=("-D")
+    flags+=("--disable-color")
+
+    must_have_one_flag=()
+    must_have_one_noun=()
+    noun_aliases=()
+}
+
+_sing-box_rule-set()
+{
+    last_command="sing-box_rule-set"
+
+    command_aliases=()
+
+    commands=()
+    commands+=("compile")
+    commands+=("convert")
+    commands+=("decompile")
+    commands+=("format")
+    commands+=("match")
+    commands+=("upgrade")
+
+    flags=()
+    two_word_flags=()
+    local_nonpersistent_flags=()
+    flags_with_completion=()
+    flags_completion=()
+
+    flags+=("--config=")
+    two_word_flags+=("--config")
+    two_word_flags+=("-c")
+    flags+=("--config-directory=")
+    two_word_flags+=("--config-directory")
+    two_word_flags+=("-C")
+    flags+=("--directory=")
+    two_word_flags+=("--directory")
+    two_word_flags+=("-D")
+    flags+=("--disable-color")
+
+    must_have_one_flag=()
+    must_have_one_noun=()
+    noun_aliases=()
+}
+
+_sing-box_run()
+{
+    last_command="sing-box_run"
+
+    command_aliases=()
+
+    commands=()
+
+    flags=()
+    two_word_flags=()
+    local_nonpersistent_flags=()
+    flags_with_completion=()
+    flags_completion=()
+
+    flags+=("--config=")
+    two_word_flags+=("--config")
+    two_word_flags+=("-c")
+    flags+=("--config-directory=")
+    two_word_flags+=("--config-directory")
+    two_word_flags+=("-C")
+    flags+=("--directory=")
+    two_word_flags+=("--directory")
+    two_word_flags+=("-D")
+    flags+=("--disable-color")
+
+    must_have_one_flag=()
+    must_have_one_noun=()
+    noun_aliases=()
+}
+
+_sing-box_tools_connect()
+{
+    last_command="sing-box_tools_connect"
+
+    command_aliases=()
+
+    commands=()
+
+    flags=()
+    two_word_flags=()
+    local_nonpersistent_flags=()
+    flags_with_completion=()
+    flags_completion=()
+
+    flags+=("--network=")
+    two_word_flags+=("--network")
+    two_word_flags+=("-n")
+    local_nonpersistent_flags+=("--network")
+    local_nonpersistent_flags+=("--network=")
+    local_nonpersistent_flags+=("-n")
+    flags+=("--config=")
+    two_word_flags+=("--config")
+    two_word_flags+=("-c")
+    flags+=("--config-directory=")
+    two_word_flags+=("--config-directory")
+    two_word_flags+=("-C")
+    flags+=("--directory=")
+    two_word_flags+=("--directory")
+    two_word_flags+=("-D")
+    flags+=("--disable-color")
+    flags+=("--outbound=")
+    two_word_flags+=("--outbound")
+    two_word_flags+=("-o")
+
+    must_have_one_flag=()
+    must_have_one_noun=()
+    noun_aliases=()
+}
+
+_sing-box_tools_fetch()
+{
+    last_command="sing-box_tools_fetch"
+
+    command_aliases=()
+
+    commands=()
+
+    flags=()
+    two_word_flags=()
+    local_nonpersistent_flags=()
+    flags_with_completion=()
+    flags_completion=()
+
+    flags+=("--config=")
+    two_word_flags+=("--config")
+    two_word_flags+=("-c")
+    flags+=("--config-directory=")
+    two_word_flags+=("--config-directory")
+    two_word_flags+=("-C")
+    flags+=("--directory=")
+    two_word_flags+=("--directory")
+    two_word_flags+=("-D")
+    flags+=("--disable-color")
+    flags+=("--outbound=")
+    two_word_flags+=("--outbound")
+    two_word_flags+=("-o")
+
+    must_have_one_flag=()
+    must_have_one_noun=()
+    noun_aliases=()
+}
+
+_sing-box_tools_synctime()
+{
+    last_command="sing-box_tools_synctime"
+
+    command_aliases=()
+
+    commands=()
+
+    flags=()
+    two_word_flags=()
+    local_nonpersistent_flags=()
+    flags_with_completion=()
+    flags_completion=()
+
+    flags+=("--format=")
+    two_word_flags+=("--format")
+    two_word_flags+=("-f")
+    local_nonpersistent_flags+=("--format")
+    local_nonpersistent_flags+=("--format=")
+    local_nonpersistent_flags+=("-f")
+    flags+=("--server=")
+    two_word_flags+=("--server")
+    two_word_flags+=("-s")
+    local_nonpersistent_flags+=("--server")
+    local_nonpersistent_flags+=("--server=")
+    local_nonpersistent_flags+=("-s")
+    flags+=("--write")
+    flags+=("-w")
+    local_nonpersistent_flags+=("--write")
+    local_nonpersistent_flags+=("-w")
+    flags+=("--config=")
+    two_word_flags+=("--config")
+    two_word_flags+=("-c")
+    flags+=("--config-directory=")
+    two_word_flags+=("--config-directory")
+    two_word_flags+=("-C")
+    flags+=("--directory=")
+    two_word_flags+=("--directory")
+    two_word_flags+=("-D")
+    flags+=("--disable-color")
+    flags+=("--outbound=")
+    two_word_flags+=("--outbound")
+    two_word_flags+=("-o")
+
+    must_have_one_flag=()
+    must_have_one_noun=()
+    noun_aliases=()
+}
+
+_sing-box_tools()
+{
+    last_command="sing-box_tools"
+
+    command_aliases=()
+
+    commands=()
+    commands+=("connect")
+    commands+=("fetch")
+    commands+=("synctime")
+
+    flags=()
+    two_word_flags=()
+    local_nonpersistent_flags=()
+    flags_with_completion=()
+    flags_completion=()
+
+    flags+=("--outbound=")
+    two_word_flags+=("--outbound")
+    two_word_flags+=("-o")
+    flags+=("--config=")
+    two_word_flags+=("--config")
+    two_word_flags+=("-c")
+    flags+=("--config-directory=")
+    two_word_flags+=("--config-directory")
+    two_word_flags+=("-C")
+    flags+=("--directory=")
+    two_word_flags+=("--directory")
+    two_word_flags+=("-D")
+    flags+=("--disable-color")
+
+    must_have_one_flag=()
+    must_have_one_noun=()
+    noun_aliases=()
+}
+
+_sing-box_version()
+{
+    last_command="sing-box_version"
+
+    command_aliases=()
+
+    commands=()
+
+    flags=()
+    two_word_flags=()
+    local_nonpersistent_flags=()
+    flags_with_completion=()
+    flags_completion=()
+
+    flags+=("--name")
+    flags+=("-n")
+    local_nonpersistent_flags+=("--name")
+    local_nonpersistent_flags+=("-n")
+    flags+=("--config=")
+    two_word_flags+=("--config")
+    two_word_flags+=("-c")
+    flags+=("--config-directory=")
+    two_word_flags+=("--config-directory")
+    two_word_flags+=("-C")
+    flags+=("--directory=")
+    two_word_flags+=("--directory")
+    two_word_flags+=("-D")
+    flags+=("--disable-color")
+
+    must_have_one_flag=()
+    must_have_one_noun=()
+    noun_aliases=()
+}
+
+_sing-box_root_command()
+{
+    last_command="sing-box"
+
+    command_aliases=()
+
+    commands=()
+    commands+=("check")
+    commands+=("format")
+    commands+=("generate")
+    commands+=("geoip")
+    commands+=("geosite")
+    commands+=("merge")
+    commands+=("rule-set")
+    commands+=("run")
+    commands+=("tools")
+    commands+=("version")
+
+    flags=()
+    two_word_flags=()
+    local_nonpersistent_flags=()
+    flags_with_completion=()
+    flags_completion=()
+
+    flags+=("--config=")
+    two_word_flags+=("--config")
+    two_word_flags+=("-c")
+    flags+=("--config-directory=")
+    two_word_flags+=("--config-directory")
+    two_word_flags+=("-C")
+    flags+=("--directory=")
+    two_word_flags+=("--directory")
+    two_word_flags+=("-D")
+    flags+=("--disable-color")
+
+    must_have_one_flag=()
+    must_have_one_noun=()
+    noun_aliases=()
+}
+
+__start_sing-box()
+{
+    local cur prev words cword split
+    declare -A flaghash 2>/dev/null || :
+    declare -A aliashash 2>/dev/null || :
+    if declare -F _init_completion >/dev/null 2>&1; then
+        _init_completion -s || return
+    else
+        __sing-box_init_completion -n "=" || return
+    fi
+
+    local c=0
+    local flag_parsing_disabled=
+    local flags=()
+    local two_word_flags=()
+    local local_nonpersistent_flags=()
+    local flags_with_completion=()
+    local flags_completion=()
+    local commands=("sing-box")
+    local command_aliases=()
+    local must_have_one_flag=()
+    local must_have_one_noun=()
+    local has_completion_function=""
+    local last_command=""
+    local nouns=()
+    local noun_aliases=()
+
+    __sing-box_handle_word
+}
+
+if [[ $(type -t compopt) = "builtin" ]]; then
+    complete -o default -F __start_sing-box sing-box
+else
+    complete -o default -o nospace -F __start_sing-box sing-box
+fi
+
+# ex: ts=4 sw=4 et filetype=sh

+ 235 - 0
release/completions/sing-box.fish

@@ -0,0 +1,235 @@
+# fish completion for sing-box                             -*- shell-script -*-
+
+function __sing_box_debug
+    set -l file "$BASH_COMP_DEBUG_FILE"
+    if test -n "$file"
+        echo "$argv" >> $file
+    end
+end
+
+function __sing_box_perform_completion
+    __sing_box_debug "Starting __sing_box_perform_completion"
+
+    # Extract all args except the last one
+    set -l args (commandline -opc)
+    # Extract the last arg and escape it in case it is a space
+    set -l lastArg (string escape -- (commandline -ct))
+
+    __sing_box_debug "args: $args"
+    __sing_box_debug "last arg: $lastArg"
+
+    # Disable ActiveHelp which is not supported for fish shell
+    set -l requestComp "SING_BOX_ACTIVE_HELP=0 $args[1] __complete $args[2..-1] $lastArg"
+
+    __sing_box_debug "Calling $requestComp"
+    set -l results (eval $requestComp 2> /dev/null)
+
+    # Some programs may output extra empty lines after the directive.
+    # Let's ignore them or else it will break completion.
+    # Ref: https://github.com/spf13/cobra/issues/1279
+    for line in $results[-1..1]
+        if test (string trim -- $line) = ""
+            # Found an empty line, remove it
+            set results $results[1..-2]
+        else
+            # Found non-empty line, we have our proper output
+            break
+        end
+    end
+
+    set -l comps $results[1..-2]
+    set -l directiveLine $results[-1]
+
+    # For Fish, when completing a flag with an = (e.g., <program> -n=<TAB>)
+    # completions must be prefixed with the flag
+    set -l flagPrefix (string match -r -- '-.*=' "$lastArg")
+
+    __sing_box_debug "Comps: $comps"
+    __sing_box_debug "DirectiveLine: $directiveLine"
+    __sing_box_debug "flagPrefix: $flagPrefix"
+
+    for comp in $comps
+        printf "%s%s\n" "$flagPrefix" "$comp"
+    end
+
+    printf "%s\n" "$directiveLine"
+end
+
+# this function limits calls to __sing_box_perform_completion, by caching the result behind $__sing_box_perform_completion_once_result
+function __sing_box_perform_completion_once
+    __sing_box_debug "Starting __sing_box_perform_completion_once"
+
+    if test -n "$__sing_box_perform_completion_once_result"
+        __sing_box_debug "Seems like a valid result already exists, skipping __sing_box_perform_completion"
+        return 0
+    end
+
+    set --global __sing_box_perform_completion_once_result (__sing_box_perform_completion)
+    if test -z "$__sing_box_perform_completion_once_result"
+        __sing_box_debug "No completions, probably due to a failure"
+        return 1
+    end
+
+    __sing_box_debug "Performed completions and set __sing_box_perform_completion_once_result"
+    return 0
+end
+
+# this function is used to clear the $__sing_box_perform_completion_once_result variable after completions are run
+function __sing_box_clear_perform_completion_once_result
+    __sing_box_debug ""
+    __sing_box_debug "========= clearing previously set __sing_box_perform_completion_once_result variable =========="
+    set --erase __sing_box_perform_completion_once_result
+    __sing_box_debug "Successfully erased the variable __sing_box_perform_completion_once_result"
+end
+
+function __sing_box_requires_order_preservation
+    __sing_box_debug ""
+    __sing_box_debug "========= checking if order preservation is required =========="
+
+    __sing_box_perform_completion_once
+    if test -z "$__sing_box_perform_completion_once_result"
+        __sing_box_debug "Error determining if order preservation is required"
+        return 1
+    end
+
+    set -l directive (string sub --start 2 $__sing_box_perform_completion_once_result[-1])
+    __sing_box_debug "Directive is: $directive"
+
+    set -l shellCompDirectiveKeepOrder 32
+    set -l keeporder (math (math --scale 0 $directive / $shellCompDirectiveKeepOrder) % 2)
+    __sing_box_debug "Keeporder is: $keeporder"
+
+    if test $keeporder -ne 0
+        __sing_box_debug "This does require order preservation"
+        return 0
+    end
+
+    __sing_box_debug "This doesn't require order preservation"
+    return 1
+end
+
+
+# This function does two things:
+# - Obtain the completions and store them in the global __sing_box_comp_results
+# - Return false if file completion should be performed
+function __sing_box_prepare_completions
+    __sing_box_debug ""
+    __sing_box_debug "========= starting completion logic =========="
+
+    # Start fresh
+    set --erase __sing_box_comp_results
+
+    __sing_box_perform_completion_once
+    __sing_box_debug "Completion results: $__sing_box_perform_completion_once_result"
+
+    if test -z "$__sing_box_perform_completion_once_result"
+        __sing_box_debug "No completion, probably due to a failure"
+        # Might as well do file completion, in case it helps
+        return 1
+    end
+
+    set -l directive (string sub --start 2 $__sing_box_perform_completion_once_result[-1])
+    set --global __sing_box_comp_results $__sing_box_perform_completion_once_result[1..-2]
+
+    __sing_box_debug "Completions are: $__sing_box_comp_results"
+    __sing_box_debug "Directive is: $directive"
+
+    set -l shellCompDirectiveError 1
+    set -l shellCompDirectiveNoSpace 2
+    set -l shellCompDirectiveNoFileComp 4
+    set -l shellCompDirectiveFilterFileExt 8
+    set -l shellCompDirectiveFilterDirs 16
+
+    if test -z "$directive"
+        set directive 0
+    end
+
+    set -l compErr (math (math --scale 0 $directive / $shellCompDirectiveError) % 2)
+    if test $compErr -eq 1
+        __sing_box_debug "Received error directive: aborting."
+        # Might as well do file completion, in case it helps
+        return 1
+    end
+
+    set -l filefilter (math (math --scale 0 $directive / $shellCompDirectiveFilterFileExt) % 2)
+    set -l dirfilter (math (math --scale 0 $directive / $shellCompDirectiveFilterDirs) % 2)
+    if test $filefilter -eq 1; or test $dirfilter -eq 1
+        __sing_box_debug "File extension filtering or directory filtering not supported"
+        # Do full file completion instead
+        return 1
+    end
+
+    set -l nospace (math (math --scale 0 $directive / $shellCompDirectiveNoSpace) % 2)
+    set -l nofiles (math (math --scale 0 $directive / $shellCompDirectiveNoFileComp) % 2)
+
+    __sing_box_debug "nospace: $nospace, nofiles: $nofiles"
+
+    # If we want to prevent a space, or if file completion is NOT disabled,
+    # we need to count the number of valid completions.
+    # To do so, we will filter on prefix as the completions we have received
+    # may not already be filtered so as to allow fish to match on different
+    # criteria than the prefix.
+    if test $nospace -ne 0; or test $nofiles -eq 0
+        set -l prefix (commandline -t | string escape --style=regex)
+        __sing_box_debug "prefix: $prefix"
+
+        set -l completions (string match -r -- "^$prefix.*" $__sing_box_comp_results)
+        set --global __sing_box_comp_results $completions
+        __sing_box_debug "Filtered completions are: $__sing_box_comp_results"
+
+        # Important not to quote the variable for count to work
+        set -l numComps (count $__sing_box_comp_results)
+        __sing_box_debug "numComps: $numComps"
+
+        if test $numComps -eq 1; and test $nospace -ne 0
+            # We must first split on \t to get rid of the descriptions to be
+            # able to check what the actual completion will be.
+            # We don't need descriptions anyway since there is only a single
+            # real completion which the shell will expand immediately.
+            set -l split (string split --max 1 \t $__sing_box_comp_results[1])
+
+            # Fish won't add a space if the completion ends with any
+            # of the following characters: @=/:.,
+            set -l lastChar (string sub -s -1 -- $split)
+            if not string match -r -q "[@=/:.,]" -- "$lastChar"
+                # In other cases, to support the "nospace" directive we trick the shell
+                # by outputting an extra, longer completion.
+                __sing_box_debug "Adding second completion to perform nospace directive"
+                set --global __sing_box_comp_results $split[1] $split[1].
+                __sing_box_debug "Completions are now: $__sing_box_comp_results"
+            end
+        end
+
+        if test $numComps -eq 0; and test $nofiles -eq 0
+            # To be consistent with bash and zsh, we only trigger file
+            # completion when there are no other completions
+            __sing_box_debug "Requesting file completion"
+            return 1
+        end
+    end
+
+    return 0
+end
+
+# Since Fish completions are only loaded once the user triggers them, we trigger them ourselves
+# so we can properly delete any completions provided by another script.
+# Only do this if the program can be found, or else fish may print some errors; besides,
+# the existing completions will only be loaded if the program can be found.
+if type -q "sing-box"
+    # The space after the program name is essential to trigger completion for the program
+    # and not completion of the program name itself.
+    # Also, we use '> /dev/null 2>&1' since '&>' is not supported in older versions of fish.
+    complete --do-complete "sing-box " > /dev/null 2>&1
+end
+
+# Remove any pre-existing completions for the program since we will be handling all of them.
+complete -c sing-box -e
+
+# this will get called after the two calls below and clear the $__sing_box_perform_completion_once_result global
+complete -c sing-box -n '__sing_box_clear_perform_completion_once_result'
+# The call to __sing_box_prepare_completions will setup __sing_box_comp_results
+# which provides the program's completion choices.
+# If this doesn't require order preservation, we don't use the -k flag
+complete -c sing-box -n 'not __sing_box_requires_order_preservation && __sing_box_prepare_completions' -f -a '$__sing_box_comp_results'
+# otherwise we use the -k flag
+complete -k -c sing-box -n '__sing_box_requires_order_preservation && __sing_box_prepare_completions' -f -a '$__sing_box_comp_results'

+ 212 - 0
release/completions/sing-box.zsh

@@ -0,0 +1,212 @@
+#compdef sing-box
+compdef _sing-box sing-box
+
+# zsh completion for sing-box                             -*- shell-script -*-
+
+__sing-box_debug()
+{
+    local file="$BASH_COMP_DEBUG_FILE"
+    if [[ -n ${file} ]]; then
+        echo "$*" >> "${file}"
+    fi
+}
+
+_sing-box()
+{
+    local shellCompDirectiveError=1
+    local shellCompDirectiveNoSpace=2
+    local shellCompDirectiveNoFileComp=4
+    local shellCompDirectiveFilterFileExt=8
+    local shellCompDirectiveFilterDirs=16
+    local shellCompDirectiveKeepOrder=32
+
+    local lastParam lastChar flagPrefix requestComp out directive comp lastComp noSpace keepOrder
+    local -a completions
+
+    __sing-box_debug "\n========= starting completion logic =========="
+    __sing-box_debug "CURRENT: ${CURRENT}, words[*]: ${words[*]}"
+
+    # The user could have moved the cursor backwards on the command-line.
+    # We need to trigger completion from the $CURRENT location, so we need
+    # to truncate the command-line ($words) up to the $CURRENT location.
+    # (We cannot use $CURSOR as its value does not work when a command is an alias.)
+    words=("${=words[1,CURRENT]}")
+    __sing-box_debug "Truncated words[*]: ${words[*]},"
+
+    lastParam=${words[-1]}
+    lastChar=${lastParam[-1]}
+    __sing-box_debug "lastParam: ${lastParam}, lastChar: ${lastChar}"
+
+    # For zsh, when completing a flag with an = (e.g., sing-box -n=<TAB>)
+    # completions must be prefixed with the flag
+    setopt local_options BASH_REMATCH
+    if [[ "${lastParam}" =~ '-.*=' ]]; then
+        # We are dealing with a flag with an =
+        flagPrefix="-P ${BASH_REMATCH}"
+    fi
+
+    # Prepare the command to obtain completions
+    requestComp="${words[1]} __complete ${words[2,-1]}"
+    if [ "${lastChar}" = "" ]; then
+        # If the last parameter is complete (there is a space following it)
+        # We add an extra empty parameter so we can indicate this to the go completion code.
+        __sing-box_debug "Adding extra empty parameter"
+        requestComp="${requestComp} \"\""
+    fi
+
+    __sing-box_debug "About to call: eval ${requestComp}"
+
+    # Use eval to handle any environment variables and such
+    out=$(eval ${requestComp} 2>/dev/null)
+    __sing-box_debug "completion output: ${out}"
+
+    # Extract the directive integer following a : from the last line
+    local lastLine
+    while IFS='\n' read -r line; do
+        lastLine=${line}
+    done < <(printf "%s\n" "${out[@]}")
+    __sing-box_debug "last line: ${lastLine}"
+
+    if [ "${lastLine[1]}" = : ]; then
+        directive=${lastLine[2,-1]}
+        # Remove the directive including the : and the newline
+        local suffix
+        (( suffix=${#lastLine}+2))
+        out=${out[1,-$suffix]}
+    else
+        # There is no directive specified.  Leave $out as is.
+        __sing-box_debug "No directive found.  Setting do default"
+        directive=0
+    fi
+
+    __sing-box_debug "directive: ${directive}"
+    __sing-box_debug "completions: ${out}"
+    __sing-box_debug "flagPrefix: ${flagPrefix}"
+
+    if [ $((directive & shellCompDirectiveError)) -ne 0 ]; then
+        __sing-box_debug "Completion received error. Ignoring completions."
+        return
+    fi
+
+    local activeHelpMarker="_activeHelp_ "
+    local endIndex=${#activeHelpMarker}
+    local startIndex=$((${#activeHelpMarker}+1))
+    local hasActiveHelp=0
+    while IFS='\n' read -r comp; do
+        # Check if this is an activeHelp statement (i.e., prefixed with $activeHelpMarker)
+        if [ "${comp[1,$endIndex]}" = "$activeHelpMarker" ];then
+            __sing-box_debug "ActiveHelp found: $comp"
+            comp="${comp[$startIndex,-1]}"
+            if [ -n "$comp" ]; then
+                compadd -x "${comp}"
+                __sing-box_debug "ActiveHelp will need delimiter"
+                hasActiveHelp=1
+            fi
+
+            continue
+        fi
+
+        if [ -n "$comp" ]; then
+            # If requested, completions are returned with a description.
+            # The description is preceded by a TAB character.
+            # For zsh's _describe, we need to use a : instead of a TAB.
+            # We first need to escape any : as part of the completion itself.
+            comp=${comp//:/\\:}
+
+            local tab="$(printf '\t')"
+            comp=${comp//$tab/:}
+
+            __sing-box_debug "Adding completion: ${comp}"
+            completions+=${comp}
+            lastComp=$comp
+        fi
+    done < <(printf "%s\n" "${out[@]}")
+
+    # Add a delimiter after the activeHelp statements, but only if:
+    # - there are completions following the activeHelp statements, or
+    # - file completion will be performed (so there will be choices after the activeHelp)
+    if [ $hasActiveHelp -eq 1 ]; then
+        if [ ${#completions} -ne 0 ] || [ $((directive & shellCompDirectiveNoFileComp)) -eq 0 ]; then
+            __sing-box_debug "Adding activeHelp delimiter"
+            compadd -x "--"
+            hasActiveHelp=0
+        fi
+    fi
+
+    if [ $((directive & shellCompDirectiveNoSpace)) -ne 0 ]; then
+        __sing-box_debug "Activating nospace."
+        noSpace="-S ''"
+    fi
+
+    if [ $((directive & shellCompDirectiveKeepOrder)) -ne 0 ]; then
+        __sing-box_debug "Activating keep order."
+        keepOrder="-V"
+    fi
+
+    if [ $((directive & shellCompDirectiveFilterFileExt)) -ne 0 ]; then
+        # File extension filtering
+        local filteringCmd
+        filteringCmd='_files'
+        for filter in ${completions[@]}; do
+            if [ ${filter[1]} != '*' ]; then
+                # zsh requires a glob pattern to do file filtering
+                filter="\*.$filter"
+            fi
+            filteringCmd+=" -g $filter"
+        done
+        filteringCmd+=" ${flagPrefix}"
+
+        __sing-box_debug "File filtering command: $filteringCmd"
+        _arguments '*:filename:'"$filteringCmd"
+    elif [ $((directive & shellCompDirectiveFilterDirs)) -ne 0 ]; then
+        # File completion for directories only
+        local subdir
+        subdir="${completions[1]}"
+        if [ -n "$subdir" ]; then
+            __sing-box_debug "Listing directories in $subdir"
+            pushd "${subdir}" >/dev/null 2>&1
+        else
+            __sing-box_debug "Listing directories in ."
+        fi
+
+        local result
+        _arguments '*:dirname:_files -/'" ${flagPrefix}"
+        result=$?
+        if [ -n "$subdir" ]; then
+            popd >/dev/null 2>&1
+        fi
+        return $result
+    else
+        __sing-box_debug "Calling _describe"
+        if eval _describe $keepOrder "completions" completions $flagPrefix $noSpace; then
+            __sing-box_debug "_describe found some completions"
+
+            # Return the success of having called _describe
+            return 0
+        else
+            __sing-box_debug "_describe did not find completions."
+            __sing-box_debug "Checking if we should do file completion."
+            if [ $((directive & shellCompDirectiveNoFileComp)) -ne 0 ]; then
+                __sing-box_debug "deactivating file completion"
+
+                # We must return an error code here to let zsh know that there were no
+                # completions found by _describe; this is what will trigger other
+                # matching algorithms to attempt to find completions.
+                # For example zsh can match letters in the middle of words.
+                return 1
+            else
+                # Perform file completion
+                __sing-box_debug "Activating file completion"
+
+                # We must return the result of this command, so it must be the
+                # last command, or else we must store its result to return it.
+                _arguments '*:filename:_files'" ${flagPrefix}"
+            fi
+        fi
+    fi
+}
+
+# don't run the completion function when being source-ed or eval-ed
+if [ "$funcstack[1]" = "_sing-box" ]; then
+    _sing-box
+fi

+ 5 - 0
release/config/openwrt.conf

@@ -0,0 +1,5 @@
+config sing-box 'main'
+	option enabled '1'
+	option conffile '/etc/sing-box/config.json'
+	option workdir '/usr/share/sing-box'
+	option log_stderr '1'

+ 31 - 0
release/config/openwrt.init

@@ -0,0 +1,31 @@
+#!/bin/sh /etc/rc.common
+
+PROG="/usr/bin/sing-box"
+
+start_service() {
+  config_load "sing-box"
+
+  local enabled config_file working_directory
+  local log_stdout log_stderr
+  config_get_bool enabled "main" "enabled" "0"
+  [ "$enabled" -eq "1" ] || return 0
+
+  config_get config_file "main" "conffile" "/etc/sing-box/config.json"
+  config_get working_directory "main" "workdir" "/usr/share/sing-box"
+  config_get_bool log_stdout "main" "log_stdout" "1"
+  config_get_bool log_stderr "main" "log_stderr" "1"
+
+  procd_open_instance
+  procd_swet_param command "$PROG" run -c "$conffile" -D "$workdir"
+  procd_set_param file "$conffile"
+  procd_set_param stderr "$log_stderr"
+  procd_set_param limits core="unlimited"
+  sprocd_set_param limits nofile="1000000 1000000"
+  procd_set_param respawn
+
+  procd_close_instance
+}
+
+service_triggers() {
+  procd_add_reload_trigger "sing-box"
+}

+ 18 - 0
release/config/sing-box.initd

@@ -0,0 +1,18 @@
+#!/sbin/openrc-run
+
+name=$RC_SVCNAME
+description="sing-box service"
+supervisor="supervise-daemon"
+command="/usr/bin/sing-box"
+command_args="-D /var/lib/sing-box -C /etc/sing-box run"
+extra_started_commands="reload"
+
+depend() {
+	after net dns
+}
+
+reload() {
+	ebegin "Reloading $RC_SVCNAME"
+        $supervisor "$RC_SVCNAME" --signal HUP
+	eend $?
+}