08.30 如何編寫bash自動補全腳本

如何編寫bash自動補全腳本

作者 | Lazarus Lazaridis

譯者 | 金靈傑

最近我在為一個項目編寫 bash 自動補全腳本,我非常喜歡這個功能。本文我會盡可能讓讀者熟悉編寫一個 bash 自動補全腳本的流程。

什麼是 bash 的自動補全

Bash 自動補全是為了幫助用戶能夠更快、更容易輸入命令的一項功能。它能夠在用戶輸入命令時敲擊tab鍵後,提供可能的選項。

$ git

git git-receive-pack git-upload-archive

gitk git-shell git-upload-pack

$ git-s

$ git-shell

工作原理

Bash 補全腳本是一段使用 bash 內置命令 command 的代碼,用於定義哪些補全建議可以對特定的可執行程序顯示。這些補全建議既可以是簡單的靜態內容,也可以是高度複雜的。

為什麼要使用

自動補全功能能夠為用戶提供以下便利:

  • 當可以自動完成時,幫助用戶減少文本輸入;
  • 讓用戶知道輸入的命令後續可以有哪些可選的參數;
  • 避免輸入錯誤,同時通過用戶已經輸入的內容隱藏或者展示可選項以提高用戶體驗。

開始上手

下面我們將開始一個演示。

首選,我們將會創建一個名為dothis的模擬可執行腳本。該腳本接受一個參數,表示用戶執行歷史中的序號,並執行序號對應的歷史命令。例如,以下命令將會執行用戶歷史命令中序號為235的命令(我電腦上對應的是ls -a命令):

dothis 235

然後,我們將創建一個 bash 自動補全腳本,用以展示用戶歷史命令信息,並和dothis命令“綁定”起來。

$ dothis

215 ls

216 ls -la

217 cd ~

218 man history

219 git status

220 history | cut -c 8-

如何編寫bash自動補全腳本

讀者可以在位於 GitHub 上的本教程代碼倉庫中看見 gif 演示動圖:https://github.com/iridakos/bash-completion-tutorial

現在讓我們開始吧。

創建可執行腳本

在工作目錄中創建名為dothis的文件,並添加以下代碼:

if [ -z "$1" ]; then

echo "No command number passed"

exit 2

fi

exists=$(fc -l -1000 | grep ^$1 -- 2>/dev/null)

if [ -n "$exists" ]; then

fc -s -- "$1"

else

echo "Command with number $1 was not found in recent history"

exit 2

fi

注意:

  • 腳本首先檢查調用時是否跟隨這一個參數。
  • 檢查輸入的數字是否在最近 1000 個命令中:
  • 如果存在則使用fc命令執行對應的命令;
  • 如果不存在則顯示錯誤信息。

使用以下命令給腳本添加可執行權限:

chmod +x ./dothis

由於在後面的教程中將多次執行這個腳本,因此我建議將其放到系統 PATH 環境變量 (http://www.linfo.org/path_env_var.html) 所指定的目錄中,這樣我們就能夠直接輸入dothis來執行它。

我將這個腳本安裝到了我的 $HOME/bin 目錄中:

install ./dothis ~/bin/dothis

如果您的系統中 ~/bin 目錄也在 PATH 環境變量中,也可以用這種方式安裝。

現在讓我們來驗證腳本:

dothis

我們應該可以看見這樣的輸出:

$ dothis

No command number passed

搞定。

創建自動補全腳本

創建一個名為dothis-completion.bash的文件,為了方便描述,從現在開始稱該文件為自動補全腳本。

一旦在該文件中添加了一些代碼,我們都需要source它以生效。注意,後面每次修改文件 之後,都需要source這個文件。

後續我們將討論如何讓這個自動補全腳本在 bash 每次打開時自動生效。

靜態補全

假設dothis應用支持一系列子命令,例如:

  • now
  • tomorrow
  • never

我們可以使用 bash 內置的complete命令來註冊這個補全列表。用專業術語來說,我們通過complete命令為我們的應用定義了一個補全規範(completion specification,compspec)。

將以下內容添加到自動補全腳本中:

#/usr/bin/env bash

complete -W "now tomorrow never" dothis

上述內容使用complete命令定義了:

  • 通過-W參數提供了補全詞列表;
  • 指定該補全詞列表適用的應用程序(這裡作為dothis命令參數)。

前面提到過,每次編輯補全腳本後,都需要 source 該文件:

source ./dothis-completion.bash

現在讓我們嘗試在命令行中敲擊兩次 tab 鍵:

$ dothis

never now tomorrow

再來試下輸入字母 n 之後的效果:

$ dothis n

never now

神奇!補全列表自動過濾出了只以字母 n 開頭的選項。

注意:補全參數列表顯示的順序和我們在補全腳本中定義的順序不同,它們已經經過自動排序。

除了這裡使用的-W參數之外,command 命令還有許多其他參數。大部分參數都以固定的方式生成補全列表,這意味著我們無法動態干預過濾它們的輸出結果。

例如,如果我們想將當前目錄下的子目錄名作為dothis應用程序的補全列表,可以將 complete 命令做如下修改:

complete -A directory dothis

此時,在 dothis 命令之後敲 tab 鍵,我們可以獲取當前目錄下子目錄的列表:

$ dothis

dir1/ dir2/ dir3/

更多關於complete命令的參數參見這裡: https://www.gnu.org/software/bash/manual/html_node/Programmable-Completion-Builtins.html#Programmable-Completion-Builtins

動態補全

本小節中,我們將實現帶有以下邏輯的dothis可執行程序的自動補全:

  • 如果用戶在命令後面直接按 tab 鍵,將顯示用戶執行歷史中的最近 50 個命令。
  • 如果用戶在輸入一個能夠從執行歷史中匹配到多個命令的數字後按 tab 鍵,將顯示這些命令以及它們的序號。
  • 如果用戶在輸入一個從執行歷史中只能匹配到一個命令的數字後按 tab 鍵,將自動補全這個數字,而不顯示命令內容(如果這個描述有些迷糊,看了後面的內容會能夠有更好的理解,放心)。

讓我們從定義一個每次dothis命令補全時都會調用的函數。將補全腳本改成這樣:

#/usr/bin/env bash

_dothis_completions()

{

COMPREPLY+=("now")

COMPREPLY+=("tomorrow")

COMPREPLY+=("never")

}

complete -F _dothis_completions dothis

對該腳本的一些說明:

  • 我們使用complete命令的-F參數定義 _dothis_completions函數為dothis命令提供補全功能。
  • COMPREPLY是一個存儲補全列表的數組,自動補全機制使用該變量來顯示補全內容。

現在讓我們重新 source 下補全腳本,驗證下補全功能:

$ dothis

never now tomorrow

完美,補全腳本能夠輸出和之前一樣的補全詞列表。等等,好像不是?再來試下:

$ dothis nev

never now tomorrow

我們可以看到,雖然我們在輸入了 nev 字母后再觸發了自動補全,顯示的補全列表和之前的一樣並沒有做自動過濾,這是為什麼呢?

  • COMPREPLY變量的內容總是會顯示,補全函數需要自己處理其中的內容。
  • 如果COMPREPLY變量中只有一個元素,那麼這個詞會自動補全到命令之後。由於目前的實現總是返回相同的三個詞,不會觸發這個功能。

使用compgen命令:它是一個用於生成補全列表的內置命令,支持complete命令的大部分參數(例如-W參數指定補全詞列表,-d參數補全目錄),並能夠基於用戶已經輸入的內容進行過濾。

如果有些迷惑也不用著急,下面通過一些命令及其輸出來展示它的使用:

$ compgen -W "now tomorrow never"

now

tomorrow

never

$ compgen -W "now tomorrow never" n

now

never

$ compgen -W "now tomorrow never" t

tomorrow

通過這些示例,我們已經可以使用該命令了,不過在此之前,還需要了解為獲取dothis命令已經輸入的內容。bash 自動補全功能提供了相關變量以支撐這個自動補全。這裡是一些比較重要的變量:

  • COMP_WORDS:當前命令行中已經輸入的詞數組。
  • COMP_CWORD:當前光標所處詞位於 COMP_WORDS 數組中的索引值。既當按下 tab 鍵時光標所處詞的索引。
  • COMP_LINE:當前命令行。

為了獲取dothis命令後面的詞,我們可以使用COMP_WORDS[1]的值。

再次修改自動補全腳本:

#/usr/bin/env bash

_dothis_completions()

{

COMPREPLY=($(compgen -W "now tomorrow never" "${COMP_WORDS[1]}"))

}

complete -F _dothis_completions dothis

source 該文件查看效果:

$ dothis

never now tomorrow

$ dothis n

never now

現在,讓我們拋開 now、never、tomorrow 這些詞,從命令執行歷史中抓取真實的數字。

fc -l命令後面增加一個負數 -n 可以顯示最近執行過的 n 條命令。因此我們將會使用:

fc -l -50

命令來顯示執行歷史中的最近 50 條命令以及它們的序號。這裡我們唯一需要處理的是將原始命令輸出的製表符替換成空格,以便於更好的展示。這個工作由sed來完成。

將自動補全腳本做如下改動:

#/usr/bin/env bash

_dothis_completions()

{

COMPREPLY=($(compgen -W "$(fc -l -50 | sed 's/\\t//')" -- "${COMP_WORDS[1]}"))

}

complete -F _dothis_completions dothis

在控制檯中 source 該腳本並驗證:

$ dothis

632 source dothis-completion.bash 649 source dothis-completion.bash 666 cat ~/.bash_profile

633 clear 650 clear 667 cat ~/.bashrc

634 source dothis-completion.bash 651 source dothis-completion.bash 668 clear

635 source dothis-completion.bash 652 source dothis-completion.bash 669 install ./dothis ~/bin/dothis

636 clear 653 source dothis-completion.bash 670 dothis

637 source dothis-completion.bash 654 clear 671 dothis 6546545646

638 clear 655 dothis 654 672 clear

639 source dothis-completion.bash 656 dothis 631 673 dothis

640 source dothis-completion.bash 657 dothis 150 674 dothis 651

641 source dothis-completion.bash 658 dothis 675 source dothis-completion.bash

642 clear 659 clear 676 dothis 651

643 dothis 623 ls -la 660 dothis 677 dothis 659

644 clear 661 install ./dothis ~/bin/dothis 678 clear

645 source dothis-completion.bash 662 dothis 679 dothis 665

646 clear 663 install ./dothis ~/bin/dothis 680 clear

647 source dothis-completion.bash 664 dothis 681 clear

648 clear 665 cat ~/.bashrc

效果不錯。但是還存在一個問題,當我們輸入一個數字之後再按 tab 鍵,會出現:

$ dothis 623

$ dothis 623 ls 623 ls -la

...

$ dothis 623 ls 623 ls 623 ls 623 ls 623 ls -la

出現這個問題是因為在自動補全腳本中,我們使用了${COMP_WORDS[1]}來獲取dothis命令之後的第一個詞(在上述代碼片段中為623)。因此當 tab 鍵按下時,相同的自動補全列表會一再出現。

要修復這個問題,我們將在已經輸入了至少一個參數之後,不再允許繼續進行自動補全。因此需要在函數中增加對COMP_WORDS數組大小的前置判斷:

#/usr/bin/env bash

_dothis_completions()

{

if [ "${#COMP_WORDS[@]}" != "2" ]; then

return

fi

COMPREPLY=($(compgen -W "$(fc -l -50 | sed 's/\\t//')" -- "${COMP_WORDS[1]}"))

}

complete -F _dothis_completions dothis

source 腳本並重試:

$ dothis 623

$ dothis 623 ls -la # 成功:此時沒有觸發自動補全

當前腳本還有一個不盡如人意的地方。我們希望展示歷史記錄序號給用戶的同時展示對應的命令,以幫助用戶決定選擇哪個歷史命令。但是當補全建議中有且只有一個時候,應該能夠通過自動補全機制自動選擇,而 不要追加命令文本

因為dothis命令實際只接受一個表示執行歷史序號的參數,並且沒有對多餘參數進行校驗。當我們的自動補全函數計算出只有一個結果時,應該去除序號後面的命令文本,只返回命令序號。

為了實現這個功能,我們需要將compgen命令的返回值保存到數組變量中,並且檢查當其大小,當大小為 1 時,去除這個唯一的值數字後面跟隨的文本;否則直接返回這個數組。

將自動補全腳本修改成:

#/usr/bin/env bash

_dothis_completions()

{

if [ "${#COMP_WORDS[@]}" != "2" ]; then

return

fi

# keep the suggestions in a local variable

local suggestions=($(compgen -W "$(fc -l -50 | sed 's/\\t/ /')" -- "${COMP_WORDS[1]}"))

if [ "${#suggestions[@]}" == "1" ]; then

# if there's only one match, we remove the command literal

# to proceed with the automatic completion of the number

local number=$(echo ${suggestions[0]/%\\ */})

COMPREPLY=("$number")

else

# more than one suggestions resolved,

# respond with the suggestions intact

COMPREPLY=("${suggestions[@]}")

fi

}

complete -F _dothis_completions dothis

註冊自動補全腳本

如果我們希望將自動補全腳本應用到個人賬戶,可以在.bashrc文件中 source 這個腳本:

source <path-to-your-script>/dothis-completion.bash/<path-to-your-script>

如果我們需要為機器上的所有用戶啟動這個自動補全腳本,可以將該腳本複製到/etc/bash_completion.d/目錄中,這樣 bash 會自動加載。

最後調優

為了有更好的展示效果,額外增加幾個步驟:)

在新行中展示每個條目

在我實際工作中編寫的 bash 自動補全腳本中,補全建議也由兩部分組成。我希望能夠將第一部分用默認顏色展示,而第二部分用灰色展示,以告知用戶這僅僅是幫助文本。以本教程為例,應該把數字用默認顏色展示,而命令文本用另一個不那麼花哨的顏色展示。

不幸的是,目前為止這個功能還無法實現,因為自動補全項僅僅以純文本方式展示,而不會處理其中的顏色指令(例如:\\e[34mBlue)。

因此這裡我們對於提升用戶體驗(也有可能沒有提升:D)的方法是將每一個補全項換行顯示。這個方案實現起來也沒有那麼方便,因為我們無法簡單的通過在每個COMPREPLY項後追加換行符來實現。為了實現這個功能,這裡採用了 hach 的方式 (https://unix.stackexchange.com/questions/166908/is-there-anyway-to-get-compreply-to-be-output-as-a-vertical-list-of-words-instea) 將補全建議文本填充到控制檯的寬度。

通過printf命令可以實現將字符串填充到指定長度。如果需要這項功能,將自動補全腳本做如下修改:

#/usr/bin/env bash

_dothis_completions()

{

if [ "${#COMP_WORDS[@]}" != "2" ]; then

return

fi

local IFS=$'\\n'

local suggestions=($(compgen -W "$(fc -l -50 | sed 's/\\t//')" -- "${COMP_WORDS[1]}"))

if [ "${#suggestions[@]}" == "1" ]; then

local number="${suggestions[0]/%\\ */}"

COMPREPLY=("$number")

else

for i in "${!suggestions[@]}"; do

suggestions[$i]="$(printf '%*s' "-$COLUMNS" "${suggestions[$i]}")"

done

COMPREPLY=("${suggestions[@]}")

fi

}

complete -F _dothis_completions dothis

source 並驗證:

dothis

...

499 source dothis-completion.bash

500 clear

...

503 dothis 500

可定製行為

在我們的之前的自動補全腳本中,將補全項數量寫死了最後 50 個執行歷史。這在實際使用中不太友好。我們應該讓每個用戶能夠有自己的選擇餘地,如果他們沒有選擇,再使用默認值 50。

為了實現這個功能,我們將檢查是否設置了環境變量DOTHIS_COMPLETION_COMMANDS_NUMBER。

最後一次修改自動補全腳本:

#/usr/bin/env bash

_dothis_completions()

{

if [ "${#COMP_WORDS[@]}" != "2" ]; then

return

fi

local commands_number=${DOTHIS_COMPLETION_COMMANDS_NUMBER:-50}

local IFS=$'\\n'

local suggestions=($(compgen -W "$(fc -l -$commands_number | sed 's/\\t//')" -- "${COMP_WORDS[1]}"))

if [ "${#suggestions[@]}" == "1" ]; then

local number="${suggestions[0]/%\\ */}"

COMPREPLY=("$number")

else

for i in "${!suggestions[@]}"; do

suggestions[$i]="$(printf '%*s' "-$COLUMNS" "${suggestions[$i]}")"

done

COMPREPLY=("${suggestions[@]}")

fi

}

complete -F _dothis_completions dothis

source 並驗證:

export DOTHIS_COMPLETION_COMMANDS_NUMBER=5

$ dothis

505 clear

506 source ./dothis-completion.bash

507 dothis clear

508 clear

509 export DOTHIS_COMPLETION_COMMANDS_NUMBER=5

一些有用的鏈接:

Git 的自動補全腳本:https://github.com/git/git/blob/master/contrib/completion/git-completion.bash

我自己編寫的腳本 goto 的補全腳本:https://github.com/iridakos/goto/blob/master/goto.sh

Bash 參考手冊:可編程自動補全:https://www.gnu.org/software/bash/manual/html_node/Programmable-Completion.html#Programmable-Completion

Bash 參考手冊:可編程自動補全內置支持:https://www.gnu.org/software/bash/manual/html_node/Programmable-Completion-Builtins.html#Programmable-Completion-Builtins

Bash 參考手冊:可編程自動補全示例:https://www.gnu.org/software/bash/manual/html_node/A-Programmable-Completion-Example.html#A-Programmable-Completion-Example

Bash 參考手冊:Bash 變量:https://www.gnu.org/software/bash/manual/html_node/Bash-Variables.html#Bash-Variables

源碼和評論

本教程源碼位於 GitHub。任何反饋、評論、勘誤請在代碼倉庫中提交 issue:https://github.com/iridakos/bash-completion-tutorial

結尾,上貓照

讓我來介紹下我的調試器。

如何編寫bash自動補全腳本

(譯者注:原作者特意囑咐我們別忘了上貓照 ^_^)


分享到:


相關文章: