Bash用の補完スクリプトを実際に作ってみる(4種)

1. [Bash用の補完スクリプトの作り方](https://qiita.com/nil2/items/8a1544e206928c753a2e)
2. Bash用の補完スクリプトを実際に作ってみる(4種)

Bashの補完スクリプトの作り方をおさえたところで、
早速、4種のコマンドについての補完スクリプトを作成してみましょう。

## vil

https://github.com/nil-two/vil

Vim scriptでテキストを処理するコマンドです。
このコマンドの使い方は次のようになっています。

```
$ vil [OPTION]... {script-only-if-no-other-script} [input-file]...

Options:
  -n, --quiet, --silent
                 suppress automatic printing of buffer
  -e script, --expression=script
                 add the script to the commands to be executed
  -f script-file, --file=script-file
                 add the contents of script-file to the commands to be executed
  --help
                 display this help and exit
  --version
                 display version information and exit
```

オプションと入力ファイルのパスを受け取る、よくある形のコマンドですね。
補完の基本方針としては、引数が-で始まっていればオプションを、そうでなければファイルパスを補完候補とするのがよさそうです。
例外として、-f、--fileの後はファイルパスを補完してほしいので、そのときだけ処理を変えるようにしたほうがよいでしょうか。

まず、ベースとして\_init\_completionを呼び出すだけのもの書きます。
このコマンドはロングオプションを取るため-sオプションを指定しておきます。
ファイル名は`vil.bash`とし、ファイルを作成したら`source vil.bash`として読み込むようにします。

```bash
_vil() {
    local cur prev words cword split
    _init_completion -s || return
}
complete -F _vil vil
```

これでcur、prev、words、cword、splitにアクセスできるようになりました。
次に引数が-で始まっていればオプションを、そうでなければファイルパスを補完するようにします。

```bash
_vil() {
    local cur prev words cword split
    _init_completion -s || return

    case $cur in
        -*)
            COMPREPLY=( $(compgen -W '$(_parse_help vil)' -- "$cur") )
            ;;
        *)
            _filedir
            ;;
    esac
}
complete -F _vil vil
```

これでファイルパスとオプションが補完されるようになりました。
ただ、これでは=で終わるオプションを補完したときに、=の後ろにスペースがついてしまいます。
そのため、補完候補の末尾が=で終わっていたときは、`compopt -o nospace`を使って、スペースがつかないようにします。

```bash
_vil() {
    local cur prev words cword split
    _init_completion -s || return

    case $cur in
        -*)
            COMPREPLY=( $(compgen -W '$(_parse_help vil)' -- "$cur") )
            [[ $COMPREPLY == *= ]] && compopt -o nospace
            ;;
        *)
            _filedir
            ;;
    esac
}
complete -F _vil vil
```

これで=で終わるオプションの後ろにスペースがつかないようになりました。
最後に、-f、--fileの後のファイルパスが補完されるようにします。

```bash
_vil() {
    local cur prev words cword split
    _init_completion -s || return

    case $prev in
        -f|--file)
            _filedir
            return
            ;;
    esac
    $split && return

    case $cur in
        -*)
            COMPREPLY=( $(compgen -W '$(_parse_help vil)' -- "$cur") )
            [[ $COMPREPLY == *= ]] && compopt -o nospace
            ;;
        *)
            _filedir
            ;;
    esac
}
complete -F _vil vil
```

これで完成です。
もっと詰めれば、-fの続きはファイルパスになるだとか、--の後の引数にオプションは来ないだとかを考慮してもいいですが、
bash-completionの公式でもそこまでは作り込んでいないのでこれで十分な品質です。

また、今回に関しては次のようにしてしまっても問題無いと思います。

```bash
complete -F _longopt vil
```

## nano

https://www.nano-editor.org/

ターミナルで動くテキストエディタです。
このコマンドの使い方は次のようになっています。

```
Usage: nano [OPTIONS] [[+LINE[,COLUMN]] FILE]...

To place the cursor on a specific line of a file, put the line number with
a '+' before the filename.  The column number can be added after a comma.
When a filename is '-', nano reads data from standard input.

Option		GNU long option		Meaning
 -A		--smarthome		Enable smart home key
 -B		--backup		Save backups of existing files
 -C <dir>	--backupdir=<dir>	Directory for saving unique backup files
 -D		--boldtext		Use bold instead of reverse video text
 -E		--tabstospaces		Convert typed tabs to spaces
 -F		--multibuffer		Read a file into a new buffer by default
 -G		--locking		Use (vim-style) lock files
 -H		--historylog		Log & read search/replace string history
 -I		--ignorercfiles		Don't look at nanorc files
 -J <number>	--guidestripe=<number>	Show a guiding bar at this column
 -K		--rawsequences		Fix numeric keypad key confusion problem
 -L		--nonewlines		Don't add an automatic newline
 -M		--trimblanks		Trim tail spaces when hard-wrapping
 -N		--noconvert		Don't convert files from DOS/Mac format
 -P		--positionlog		Log & read location of cursor position
 -Q <regex>	--quotestr=<regex>	Regular expression to match quoting
 -R		--restricted		Restricted mode
 -T <#cols>	--tabsize=<#cols>	Set width of a tab to #cols columns
 -U		--quickblank		Do quick statusbar blanking
 -V		--version		Print version information and exit
 -W		--wordbounds		Detect word boundaries more accurately
 -X <str>	--wordchars=<str>	Which other characters are word parts
 -Y <name>	--syntax=<name>		Syntax definition to use for coloring
 -Z		--zap			Let Bsp and Del erase a marked region
 -a		--atblanks		When soft-wrapping, do it at whitespace
 -b		--breaklonglines	Automatically hard-wrap overlong lines
 -c		--constantshow		Constantly show cursor position
 -d		--rebinddelete		Fix Backspace/Delete confusion problem
 -e		--emptyline		Keep the line below the title bar empty
 -g		--showcursor		Show cursor in file browser & help text
 -h		--help			Show this help text and exit
 -i		--autoindent		Automatically indent new lines
 -j		--jumpyscrolling	Scroll per half-screen, not per line
 -k		--cutfromcursor		Cut from cursor to end of line
 -l		--linenumbers		Show line numbers in front of the text
 -m		--mouse			Enable the use of the mouse
 -n		--noread		Do not read the file (only write it)
 -o <dir>	--operatingdir=<dir>	Set operating directory
 -p		--preserve		Preserve XON (^Q) and XOFF (^S) keys
 -r <#cols>	--fill=<#cols>		Set width for hard-wrap and justify
 -s <prog>	--speller=<prog>	Enable alternate speller
 -t		--tempfile		Auto save on exit, don't prompt
 -u		--unix			Save a file by default in Unix format
 -v		--view			View mode (read-only)
 -w		--nowrap		Don't hard-wrap long lines [default]
 -x		--nohelp		Don't show the two help lines
 -y		--afterends		Make Ctrl+Right stop at word ends
 -z		--suspend		Enable suspension
 -$		--softwrap		Enable soft line wrapping
```

結構な分量がありますね。
しかし、オプションと入力ファイルのパスを受け取る、よくある形のコマンドであることに変わりはありません。
今回も補完の基本方針としては、引数が-で始まっていればオプションを、そうでなければファイルパスを補完候補とするのがよさそうです。
例外として、-C、--backupdirの後はディレクトリパスを、-o、--operatingdirの後はディレクトリパスを、-s、--spellerの後はコマンド名を補完してほしいので、そのときだけ処理を変えるようにしたほうがよいでしょうか。
あとは、--syntaxの後に来るシンタックスも補完できるといいのですが、シンタックス一覧を取得する手段が見当たらないので、その点は妥協します。

まず、ベースとして\_init\_completionを呼び出すだけのもの書きます。
このコマンドはロングオプションを取るため-sオプションを指定しておきます。
ファイル名は`nano.bash`とし、ファイルを作成したら`source note.bash`として読み込むようにします。

```bash
_nano() {
    local cur prev words cword split
    _init_completion -s || return
}
complete -F _nano nano
```

これでcur、prev、words、cword、splitにアクセスできるようになりました。
次に引数が-で始まっていればオプションを、そうでなければファイルパスを補完するようにします。

```bash
_nano() {
    local cur prev words cword split
    _init_completion -s || return

    case $cur in
        -*)
            COMPREPLY=( $(compgen -W '--smarthome --backup --backupdir= --boldtext --tabstospaces --multibuffer --locking --historylog --ignorercfiles --guidestripe= --rawsequences --nonewlines --trimblanks --noconvert --positionlog --quotestr= --restricted --tabsize= --quickblank --version --wordbounds --wordchars= --syntax= --zap --atblanks --breaklonglines --constantshow --rebinddelete --emptyline --showcursor --help --autoindent --jumpyscrolling --cutfromcursor --linenumbers --mouse --noread --operatingdir= --preserve --fill= --speller= --tempfile --unix --view --nowrap --nohelp --afterends --suspend --softwrap' -- "$cur") )
            [[ $COMPREPLY == *= ]] && compopt -o nospace
            ;;
        *)
            _filedir
            ;;
    esac
}
complete -F _nano nano
```

これでファイルパスとオプションが補完されるようになりました。
今回は残念ながら\_parse\_helpだけではオプション一覧を抽出できませんでした。そういうこともあります。
最後に-C、--backupdirの後はディレクトリパスを、-o、--operatingdirの後はディレクトリパスを、-s、--spellerの後はコマンド名を補完するようにします。

```bash
_nano() {
    local cur prev words cword split
    _init_completion -s || return

    case $prev in
        -C|--backupdir)
            _filedir -d
            return
            ;;
        -o|--operatingdir)
            _filedir -d
            return
            ;;
        -s|--speller)
            COMPREPLY=( $(compgen -A command -- "$cur") )
            return
            ;;
    esac
    $split && return

    case $cur in
        -*)
            COMPREPLY=( $(compgen -W '--smarthome --backup --backupdir= --boldtext --tabstospaces --multibuffer --locking --historylog --ignorercfiles --guidestripe= --rawsequences --nonewlines --trimblanks --noconvert --positionlog --quotestr= --restricted --tabsize= --quickblank --version --wordbounds --wordchars= --syntax= --zap --atblanks --breaklonglines --constantshow --rebinddelete --emptyline --showcursor --help --autoindent --jumpyscrolling --cutfromcursor --linenumbers --mouse --noread --operatingdir= --preserve --fill= --speller= --tempfile --unix --view --nowrap --nohelp --afterends --suspend --softwrap' -- "$cur") )
            [[ $COMPREPLY == *= ]] && compopt -o nospace
            ;;
        *)
            _filedir
            ;;
    esac
}
complete -F _nano nano
```

これで完成です。

## vack

https://github.com/nil-two/vack

Vimのプラグインを管理するコマンドです。
このコマンドの使い方は次のようになっています。

```
usage: vack <command> [...]
manage Vim plugins.

commands:
  i|install [-os] <repository>...   # install the plugins
  u|update [-a] <plugin>...         # update the plugins
  r|remove <plugin>...              # remove the plugins
  l|list [-aos]                     # list installed plugins
  p|path [-a] <plugin>...           # show the path of the plugins
  e|enable <plugin>...              # move the plugins to start
  d|disable <plugin>...             # move the plugins to opt
  I|init                            # create the plugin directory
  h|help                            # print usage

environment-variables:
  VACKPATH   # the plugin directory (default: $HOME/.vim/pack/vack)
```

サブコマンドを取り、サブコマンドごとに違った引数を取る、これもよくある形のコマンドですね。
補完の基本方針としては、第一引数はサブコマンドを、第二引数以降は引数が-で始まっていればサブコマンドごとのオプションを、そうでなければサブコマンドごとの引数を補完候補とするのがよさそうです。
installの引数はインストール元のリポジトリなので補完できないですが、update、remove、pathはインストール済みのプラグイン、enableはopt配下にあるプラグイン、disableはstart配下にあるプラグインを引数に取るので、これらは引数の補完ができそうです。

まず、ベースとして\_init\_completionを呼び出すだけのもの書きます。
このコマンドはロングオプションを取らず、:を区切り文字として使う引数を取らないので、オプションは何も指定しません。
ファイル名は`vack.bash`とし、ファイルを作成したら`source vack.bash`として読み込むようにします。

```bash
_vack() {
    local cur prev words cword split
    _init_completion || return
}
complete -F _vack vack
```

これでcur、prev、words、cword、splitにアクセスできるようになりました。
次に第一引数のときにサブコマンドを補完するようにします。

```bash
_vack() {
    local cur prev words cword split
    _init_completion || return

    case $cword in
        1)
            COMPREPLY=( $(compgen -W 'install update remove list path enable disable init help' -- "$cur") )
            ;;
    esac
}
complete -F _vack vack
```

これで第一引数のサブコマンドが補完されるようになりました。
次に第二引数以降をサブコマンドの引数として、引数が-で始まっていればサブコマンドごとのオプションを、そうでなければサブコマンドごとの引数を補完するようにします。
今回はロングオプションが無いですが、代わりにショートオプションを補完するようにします。

```bash
_vack() {
    local cur prev words cword split
    _init_completion || return

    case $cword in
        1)
            COMPREPLY=( $(compgen -W 'install update remove list path enable disable init help' -- "$cur") )
            ;;
        *)
            case ${words[1]} in
                i|install)
                    case $cur in
                        -*) COMPREPLY=( $(compgen -W '-o -s --' -- "$cur") ) ;;
                        *)  COMPREPLY=()
                    esac
                    ;;
                u|update)
                    case $cur in
                        -) COMPREPLY=( $(compgen -W '-a --' -- "$cur") ) ;;
                        *) COMPREPLY=( $(compgen -W '$(vack list)' -- "$cur") ) ;;
                    esac
                    ;;
                r|remove)
                    case $cur in
                        -) COMPREPLY=( $(compgen -W '--' -- "$cur") ) ;;
                        *) COMPREPLY=( $(compgen -W '$(vack list)' -- "$cur") ) ;;
                    esac
                    ;;
                l|list)
                    case $cur in
                        -) COMPREPLY=( $(compgen -W '-a -o -s --' -- "$cur") ) ;;
                        *) COMPREPLY=() ;;
                    esac
                    ;;
                p|path)
                    case $cur in
                        -) COMPREPLY=( $(compgen -W '-a --' -- "$cur") ) ;;
                        *) COMPREPLY=( $(compgen -W '$(vack list)' -- "$cur") ) ;;
                    esac
                    ;;
                e|enable)
                    case $cur in
                        -) COMPREPLY=( $(compgen -W '--' -- "$cur") ) ;;
                        *) COMPREPLY=( $(compgen -W '$(vack list -o)' -- "$cur") ) ;;
                    esac
                    ;;
                d|disable)
                    case $cur in
                        -) COMPREPLY=( $(compgen -W '--' -- "$cur") ) ;;
                        *) COMPREPLY=( $(compgen -W '$(vack list -s)' -- "$cur") ) ;;
                    esac
                    ;;
                I|init)
                    COMPREPLY=()
                    ;;
                h|help)
                    COMPREPLY=()
                    ;;
            esac
            ;;
    esac
}
complete -F _vack vack
```

これでサブコマンドの引数が補完されるようになりました。
しかし、vack listの出力をそのまま補完候補としている箇所でIFSの制御を行っていないので、もしvack listの出力に空白が混じっていれば正しく補完がされません。
そのため、IFSを都度制御することによって、空白を含む補完候補を扱えるようにします。

```bash
_vack() {
    local cur prev words cword split
    _init_completion || return

    local defaultIFS=$' \t\n'
    local IFS=$defaultIFS

    case $cword in
        1)
            COMPREPLY=( $(compgen -W 'install update remove list path enable disable init help' -- "$cur") )
            ;;
        *)
            case ${words[1]} in
                i|install)
                    case $cur in
                        -*) COMPREPLY=( $(compgen -W '-o -s --' -- "$cur") ) ;;
                        *)  COMPREPLY=()
                    esac
                    ;;
                u|update)
                    case $cur in
                        -) COMPREPLY=( $(compgen -W '-a --' -- "$cur") ) ;;
                        *) IFS=$'\n'; COMPREPLY=( $(compgen -W '$(vack list)' -- "$cur") ); IFS=$defaultIFS ;;
                    esac
                    ;;
                r|remove)
                    case $cur in
                        -) COMPREPLY=( $(compgen -W '--' -- "$cur") ) ;;
                        *) IFS=$'\n'; COMPREPLY=( $(compgen -W '$(vack list)' -- "$cur") ); IFS=$defaultIFS ;;
                    esac
                    ;;
                l|list)
                    case $cur in
                        -) COMPREPLY=( $(compgen -W '-a -o -s --' -- "$cur") ) ;;
                        *) COMPREPLY=() ;;
                    esac
                    ;;
                p|path)
                    case $cur in
                        -) COMPREPLY=( $(compgen -W '-a --' -- "$cur") ) ;;
                        *) IFS=$'\n'; COMPREPLY=( $(compgen -W '$(vack list)' -- "$cur") ); IFS=$defaultIFS ;;
                    esac
                    ;;
                e|enable)
                    case $cur in
                        -) COMPREPLY=( $(compgen -W '--' -- "$cur") ) ;;
                        *) IFS=$'\n'; COMPREPLY=( $(compgen -W '$(vack list -o)' -- "$cur") ); IFS=$defaultIFS ;;
                    esac
                    ;;
                d|disable)
                    case $cur in
                        -) COMPREPLY=( $(compgen -W '--' -- "$cur") ) ;;
                        *) IFS=$'\n'; COMPREPLY=( $(compgen -W '$(vack list -s)' -- "$cur") ); IFS=$defaultIFS ;;
                    esac
                    ;;
                I|init)
                    COMPREPLY=()
                    ;;
                h|help)
                    COMPREPLY=()
                    ;;
            esac
            ;;
    esac
}
complete -F _vack vack
```

これで完成です。

## memo

https://github.com/mattn/memo

メモを管理するコマンドです。
このコマンドの使い方は次のようになっています。

```
NAME:
   memo - Memo Life For You

USAGE:
   memo [global options] command [command options] [arguments...]

VERSION:
   0.0.13

COMMANDS:
   new, n     create memo
   list, l    list memo
   edit, e    edit memo
   cat, v     view memo
   delete, d  delete memo
   grep, g    grep memo
   config, c  configure
   serve, s   start http server
   help, h    Shows a list of commands or help for one command

GLOBAL OPTIONS:
   --help, -h     show help (default: false)
   --version, -v  print the version (default: false)

------------------------------------------------------------------

NAME:
   memo new - create memo

USAGE:
   memo new [command options] [arguments...]

OPTIONS:
   --help, -h  show help (default: false)
   
------------------------------------------------------------------

NAME:
   memo list - list memo

USAGE:
   memo list [command options] [arguments...]

OPTIONS:
   --fullpath       show file path (default: false)
   --format string  print the result using a Go template string
   --help, -h       show help (default: false)

------------------------------------------------------------------

NAME:
   memo edit - edit memo

USAGE:
   memo edit [command options] [arguments...]

OPTIONS:
   --help, -h  show help (default: false)
   
------------------------------------------------------------------

NAME:
   memo cat - view memo

USAGE:
   memo cat [command options] [arguments...]

OPTIONS:
   --help, -h  show help (default: false)

------------------------------------------------------------------

NAME:
   memo delete - delete memo

USAGE:
   memo delete [command options] [arguments...]

OPTIONS:
   --help, -h  show help (default: false)
   
------------------------------------------------------------------

NAME:
   memo grep - grep memo

USAGE:
   memo grep [command options] [arguments...]

OPTIONS:
   --help, -h  show help (default: false)
   
------------------------------------------------------------------

NAME:
   memo config - configure

USAGE:
   memo config [command options] [arguments...]

OPTIONS:
   --cat       cat the file (default: false)
   --help, -h  show help (default: false)
   
------------------------------------------------------------------

NAME:
   memo serve - start http server

USAGE:
   memo serve [command options] [arguments...]

OPTIONS:
   --addr value  server address (default: ":8080")
   --help, -h    show help (default: false)
   
------------------------------------------------------------------

NAME:
    - Shows a list of commands or help for one command

USAGE:
    [command options] [command]

OPTIONS:
   --help, -h  show help (default: false)
```

サブコマンドを取り、サブコマンドもオプションを取る、複合的なコマンドになっています。
分量がありますが、複雑な引数は取らないので一つ一つ見ていけば大丈夫です。
補完の基本方針としては、第一引数はサブコマンドを、第二引数以降は引数が-で始まっていればサブコマンドごとのオプションを、そうでなければサブコマンドごとの引数を補完候補とするのがよさそうです。

まず、ベースとして\_init\_completionを呼び出すだけのもの書きます。
このコマンドはロングオプションを取るため-sオプションを指定しておきます。
ファイル名は`memo.bash`とし、ファイルを作成したら`source memo.bash`として読み込むようにします。

```bash
_memo() {
    local cur prev words cword split
    _init_completion -s || return
}
complete -F _memo memo
```

これでcur、prev、words、cword、splitにアクセスできるようになりました。
次に第一引数のときに引数が-で始まっていればオプションを、そうでなければサブコマンド補完するようにします。

```bash
_memo() {
    local cur prev words cword split
    _init_completion -s || return

    case $cword in
        1)
            $split && return
            case $cur in
                -*) COMPREPLY=( $(compgen -W '--help --version' -- "$cur") ) ;;
                *)  COMPREPLY=( $(compgen -W 'new list edit cat delete grep config serve help' -- "$cur") ) ;;
            esac
            ;;
    esac
}
complete -F _memo memo

```

これで第一引数が補完されるようになりました。
最後に、第二引数以降としてサブコマンドごとの引数を-で始まっていればサブコマンドごとのオプションを、そうでなければサブコマンドごとの引数を補完するようにします。

```bash
_memo() {
    local cur prev words cword split
    _init_completion -s || return

    local defaultIFS=$' \t\n'
    local IFS=$defaultIFS

    case $cword in
        1)
            $split && return
            case $cur in
                -*) COMPREPLY=( $(compgen -W '--help --version' -- "$cur") ) ;;
                *)  COMPREPLY=( $(compgen -W 'new list edit cat delete grep config serve help' -- "$cur") ) ;;
            esac
            ;;
        *)
            case ${words[1]} in
                n|new)
                    $split && return
                    case $cur in
                        -*) COMPREPLY=( $(compgen -W '--help' -- "$cur") ) ;;
                        *)  COMPREPLY=() ;;
                    esac
                    ;;
                l|list)
                    $split && return
                    case $cur in
                        -*) COMPREPLY=( $(compgen -W '--fullpath --format= --help' -- "$cur") ); [[ $COMPREPLY == *= ]] && compopt -o nospace ;;
                        *)  COMPREPLY=() ;;
                    esac
                    ;;
                e|edit)
                    $split && return
                    case $cur in
                        -*) COMPREPLY=( $(compgen -W '--help' -- "$cur") ) ;;
                        *)  IFS=$'\n'; COMPREPLY=( $(compgen -W '$(memo list)' -- "$cur") ); IFS=$defaultIFS ;;
                    esac
                    ;;
                v|cat)
                    $split && return
                    case $cur in
                        -*) COMPREPLY=( $(compgen -W '--help' -- "$cur") ) ;;
                        *)  IFS=$'\n'; COMPREPLY=( $(compgen -W '$(memo list)' -- "$cur") ); IFS=$defaultIFS ;;
                    esac
                    ;;
                d|delete)
                    $split && return
                    case $cur in
                        -*) COMPREPLY=( $(compgen -W '--help' -- "$cur") ) ;;
                        *)  IFS=$'\n'; COMPREPLY=( $(compgen -W '$(memo list)' -- "$cur") ); IFS=$defaultIFS ;;
                    esac
                    ;;
                g|grep)
                    $split && return
                    case $cur in
                        -*) COMPREPLY=( $(compgen -W '--help' -- "$cur") ) ;;
                        *)  COMPREPLY=() ;;
                    esac
                    ;;
                c|config)
                    $split && return
                    case $cur in
                        -*) COMPREPLY=( $(compgen -W '--cat --help' -- "$cur") ) ;;
                        *)  COMPREPLY=() ;;
                    esac
                    ;;
                s|serve)
                    $split && return
                    case $cur in
                        -*) COMPREPLY=( $(compgen -W '--addr= --help' -- "$cur") ); [[ $COMPREPLY == *= ]] && compopt -o nospace ;;
                        *)  COMPREPLY=() ;;
                    esac
                    ;;
                h|help)
                    $split && return
                    case $cur in
                        -*) COMPREPLY=( $(compgen -W '--help' -- "$cur") ) ;;
                        *)  COMPREPLY=( $(compgen -W 'new list edit cat delete grep config serve help' -- "$cur") ) ;;
                    esac
                    ;;
            esac
            ;;
    esac
}
complete -F _memo memo
```

これで完成です。