在命令行按下tab鍵之後, 發生了什麼?

来源:https://www.cnblogs.com/wbourne/archive/2023/06/24/17447471.html
-Advertisement-
Play Games

當我們輸入ls 再按下TAB時, 會自動列出當前路徑下所有的文件; 當我們輸入ls a 再按下TAB時, 會自動列出當前路徑下所有以a開頭的文件; 若只有一個以a開頭的文件, 將會自動補全; 這是怎麼做到的? 本文將帶你一探究竟 ...




1. 引言

當我們輸入ls 再按下TAB時, 會自動列出當前路徑下所有的文件;

當我們輸入ls a再按下TAB時, 會自動列出當前路徑下所有以a開頭的文件; 若只有一個以a開頭的文件, 將會自動補全;

當我們輸入type 再按下TAB時, 會自動列出全所有可執行的命令;

當我們輸入docker rmi 再按下TAB時, 會自動列出所有鏡像名;

一個顯示文件, 一個顯示命令, 一個顯示容器名, 這是怎麼做到的?

本文將帶你一探究竟, 並以docker為例, 實現一個簡單的docker自動補全規則

2. complete命令

上述功能, 是 Bash 2.05 版本新增的功能, 叫做自動補全. 自動補全允許我們對命令和選項設置補全規則, 按下TAB之後, 會根據我們設置的規則返回補全列表, 當補全列表只有一個元素時, 就會自動補全.

bash自動補全用到最主要的命令就是complete, 這是一個Bash的內置命令(builtin), 用於指定某個命令的補全規則, complete語法如下:

complete [-abcdefgjksuv] [-o comp-option] [-DEI] [-A action] [-G globpat] [-W wordlist] [-F function] [-C command] [-X filterpat] [-P prefix] [-S suffix] [name …]
complete -pr [-DEI] [name …]

選項:
    -o comp-option
        定義一些補全的行為, 可以使用的行為如下:
        nospace    補全後不在最後添加空格
        nosort     對於補全列表不要按字母排序

    -A action
        使用預設的補全規則, 可使用的補全規則如下:
        alias        補全列表設置為所有已定義的別名. 等同於-a
        builtin      補全列表設置為所有shell內置命令. 等同於-b
        command      補全列表設置為所有可執行命令. 等同於-c
        directory    補全列表設置為當前路徑下所有目錄. 等同於-d,
                     也就是說 complete -d xxx 與 complete -A directory xxx 等價, 只是寫法不一樣
        export       補全列表設置為所有環境變數名. 等同於-e
        file         補全列表設置為當前路徑下所有文件. 等同於-f
        function     補全列表設置為所有函數名
        signal       補全列表設置為所有信號名
        user         補全列表設置為所有用戶名. 等同於-u
        variable     補全列表設置為所有變數名. 等同於-v

    -F function
        用函數來定義補全規則, 函數運行後 COMPREPLY 變數做為補全列表

    -W wordlist
        用一個字元串來做為補全列表

    -p name
        顯示某個命令的補全規則, 如果 name 為空的話則顯示所有命令的補全規則

    -r
        移除某個命令的補全規則

ls命令預設的補全列表是當前路徑下所有文件, 現在, 我們改變其補全規則, 讓其補全列表變為所有可執行命令

$ cd /

# 先測試下 ls 預設的補全規則
$ ls<TAB>
bin/    boot/   dev/    etc/    home/   lib/    lib32/  lib64/  libx32/ media/  mnt/    opt/    proc/   root/   run/    sbin/   srv/    sys/    tmp/    usr/    var/

# 修改 ls 的補全規則, 讓所有可執行命令作為其補全列表
$ complete -c ls

# 測試修改補全規則後的 ls
$ ls who<TAB>
who                   whoami                whoopsie              whoopsie-preferences

提示: 上述改變的補全規則只在當前shell有效, 即不會影響到其他用戶, 重新登錄後也會失效. 所以想要恢復ls命令的補全規則的話, 只需要退出再重新登錄伺服器就好了. 至於如何永久改變補全規則, 請看後文.


我們再來看下type命令預設的補全規則, 發現type命令設置的補全列表是所有可執行命令

$ complete -p type
complete -c type

至此, 我們應該知道引言中所提出的問題, 為什麼ls命令會文件而type命令會列出命令

3. 自定義補全列表

儘管Bash預設了很多補全規則, 但是很明顯, 如果我們自己想給docker命令寫補全規則的話, 預設的補全規則顯然是不能滿足我們需求的. 所以, 我們可以用-W選項來自定義補全列表.

假設我們自己寫了個mydocker命令, 可以使用的功能有mydocker rm, mydocker rmi, mydocker stop, mydocker start, 顯然, mydocker的補全列表為rm rmi stop start, 我們可以使用下麵的命令來設置補全規則

# 將 rm rmi stop start 設置為 mydocker 的補全列表
$ complete -W 'rm rmi stop start' mydocker

$ mydocker <TAB>
rm     rmi    start  stop

$ mydocker st<TAB>
start  stop

註意: mydocker命令本身沒有自動補全, 需要手動完整輸入


到這一步, 我們已經能給相當一部分的命令來定義補全規則了. 但是, 上述的'-W'選項, 是靜態的補全規則, 不會隨著某些條件的改變而變化; docker rmi <TAB>所有顯示的鏡像名, 會隨著鏡像的增刪而改變; docker rm <TAB>所有顯示的容器名, 會隨著容器的增刪而改變; 是動態的補全規則, 這是如何做到的呢?

我們直接通過-p選項來查看docker預設的補全規則就好了, 發現docker命令是通過-F _docker來指定補全規則; 再通過type _docker來查看_docker是什麼玩意, 發現_docker是一個非常複雜的函數

$ complete -p docker
complete -F _docker docker

$ type _docker
_docker is a function
_docker ()
{
    ......
}

接下來, 我們來好好聊一聊-F這個選項

4. 動態補全列表

-F選項會指定一個函數做為補全規則, 每次按下TAB時, 就會調用這個函數, 並且將COMPREPLY的值做為補全列表, 所以我們需要在函數中處理COMPREPLY變數

除了COMPREPLY變數外, Bash還提供了一些變數來方便我們獲取當前的輸入

變數名 類型 說明
COMP_LINE 字元串 當前的命令行輸入的所有內容
COMP_WORDS 數組 當前的命令行輸入的所有內容, 和COMP_LINE不同的是, 這個變數是一個數組
COMP_CWORD 整數 當前的命令行輸入的內容位於COMP_WORDS數組中的索引
COMPREPLY 數組 補全列表

接下來我們編寫一個補全腳本來測試這些變數, 腳本名字可以隨便取, 暫且叫做 test.sh, 文件內容如下:

_complete_test() {
    echo
    echo "COMP_LINE: $COMP_LINE"                # 當前的命令行輸入的所有內容(字元串)
    echo "COMP_WORDS: ${COMP_WORDS[@]}"         # 當前的命令行輸入的所有內容(數組)
    echo "COMP_CWORD: $COMP_CWORD"              # 數組的索引
    echo "last_word: ${COMP_WORDS[COMP_CWORD]}" # 最後一個輸入的單詞
    echo "COMPREPLY: $COMPREPLY"                # 補全列表
}

complete -F _complete_test mydocker

我們通過執行source test.sh來使腳本生效, 然後來測試腳本

$ source test.sh

$ mydocker <TAB>
COMP_LINE: mydocker    # 當前的命令行輸入的所有內容(字元串)
COMP_WORDS: mydocker   # 當前的命令行輸入的所有內容(數組)
COMP_CWORD: 1          # 數組的索引
last_word:             # 最後一個輸入的單詞
COMPREPLY:             # 補全列表

$ mydocker xy<TAB>
COMP_LINE: mydocker xy    # 當前的命令行輸入的所有內容(字元串)
COMP_WORDS: mydocker xy   # 當前的命令行輸入的所有內容(數組)
COMP_CWORD: 1             # 數組的索引
last_word: xy             # 最後一個輸入的單詞
COMPREPLY:                # 補全列表

我們理解了上述的變數之後, 我們是不是可以這樣做: 獲取當前輸入的內容, 如果是mydocker的話, 將補全列表設置為rm rmi stop start; 如果是mydocker rm的話, 查詢出所有的容器名, 並將補全列表設置為所有的容器名, startstop同理; 如果是mydocker rmi的話, 補全列表設置為所有的鏡像名. 因為每次自動補全都會執行我們的函數, 所以我們的補全列表就是動態的了

在修改test.sh腳本之前, 我們造一點測試數據, 拉取兩個鏡像並運行這兩個鏡像

$ docker pull redis
$ docker pull redmine

接下來將test.sh腳本修改為如下內容:

_complete_mydocker() {
    local prev
    prev="${COMP_WORDS[COMP_CWORD-1]}"

    case "${prev}" in
        rm) COMPREPLY=( $(docker ps -a | tail -n +2 | awk '{print $NF}') ) ;;
        rmi) COMPREPLY=( $(docker images | tail -n +2 | awk '{print $1}') );;
        mydocker) COMPREPLY=( rm rmi stop start ) ;;
    esac
}

complete -F _complete_mydocker mydocker

註意: case語句中判斷的是倒數第二個輸入的單詞, 因為當我們運行mydocker r<TAB>時, 最後一個單詞是r, 倒數第二個單詞是mydocker, 顯然此時我們需要的是mydocker的補全列表


修改完腳本後, 要再次執行source test.sh才能使腳本生效. 然後來測試腳本

$ mydocker <TAB>
rm     rmi    start  stop

# 貌似有點問題?
$ mydocker rm<TAB>
rm     rmi    start  stop


$ mydocker rmi <TAB>
redis             redmine

# 貌似又有問題?
$ mydocker rmi redi<TAB>
redis            redmine

目前的補全腳本還是存在一些問題, 其實也很容易發現問題, 無論我們輸入mydocker rmi re還是mydocker rmi redi, 都會匹配到補全腳本中的rmi) COMPREPLY=( $(docker images | tail -n +2 | awk '{print $1}') );;, 我們返回的補全列表COMPLETE都是同樣的結果, 補全列表並沒有變, 補全列表返回的都是redis redmine. 然而, 我們想要的是, 輸入mydocker rmi re返回redis redmine, 輸入mydocker rmi redi返回redis, 這就需要compgen命令出場了

Tips: 可能有些讀者會有疑問, 為什麼設置同樣的候選列表, 使用-W就和預期一樣而使用-F就會出現上述問題, 因為-W已經幫我們實現了類似compgen的功能, 而-F需要我們手動處理才行

5. compgen命令

compgen也是一個Bash內置命令, 其選項幾乎和complete是通用的, 其作用就是篩選, 看幾個例子大家就明白怎麼用了

# -W指定補全列表, 並返回與st相匹配的值
$ compgen -W 'rm    rmi    start    stop' -- st
start
stop

# -W指定補全列表, 並返回與sto相匹配的值
$ compgen -W 'rm    rmi    start    stop' -- sto
stop

# -b指定補全列表為Bash內置命令, 並返回與c相匹配的值
$ compgen -b -- c
caller
cd
command
compgen
complete
compopt
continue

學會了compgen命令, 我們再來修改腳本, 將COMPREPLY=( rm rmi stop start )修改為COMPREPLY=( $(compgen -W "rm rmi stop start" -- 最後一個單詞) )就可以動態修改補全列表了

最後將腳本修改如下:

_complete_mydocker() {
    local cur prev mydocker_opts images contains
    cur="${COMP_WORDS[COMP_CWORD]}"
    prev="${COMP_WORDS[COMP_CWORD-1]}"
    mydocker_opts="rm rmi stop start"
    images=$(docker images | tail -n +2 | awk '{print $1}')
    contains=$(docker ps -a | tail -n +2 | awk '{print $NF}')

    case "${prev}" in
        rm) COMPREPLY=( $(compgen -W "${contains}" -- ${cur}) ) ;;
        rmi) COMPREPLY=( $(compgen -W "${images}" -- ${cur}) );;
        mydocker) COMPREPLY=( $(compgen -W "${mydocker_opts}" -- ${cur}) ) ;;
    esac
}

complete -F _complete_mydocker mydocker

執行腳本後再次測試腳本, 已經能達到我們想要的效果了

$ mydocker <TAB>
rm     rmi    start  stop

$ mydocker rm<TAB>
rm   rmi 

$ mydocker rmi <TAB>
redis             redmine

$ mydocker rmi re<TAB>
redis             redmine

# 這裡就會自動補全了
$ mydocker rmi redi<TAB>

6. 別名的自動補全

筆者用docker相關的命令用的比較多, 不想每次敲這麼長, 所以直接執行alias d=dockerd設置為docker的別名, 設置後方是方便了很多, 但是用不了自動補全

沒關係, 既然docker有自動補全, 那麼d也必須有自動補全. 通過執行complete命令發現, docker的補全規則是_docker函數提供的

$ complete -p docker
complete -F _docker docker

那我們只需要執行complete -F _docker d, 將d的補全規則設置為_docker, 這樣d也可使用自動補全了

$ d <TAB>
build      cp         events     help       images     inspect    login      network    plugin     pull       restart    run        secret     start      swarm      top        version    
commit     create     exec       history    import     kill       logout     node       port       push       rm         save       service    stats      system     unpause    volume     
container  diff       export     image      info       load       logs       pause      ps         rename     rmi        search     stack      stop       tag        update     wait   

7. 補全規則永久生效

上述例子中, 我們執行補全規則腳本, 使用的是. completion_script或者source completion_script的形式來執行, 而不是通過./completion_scriptbash completion_script的形式來執行, 是因為: 前者的作用範圍是當前shell; 而後者會在子shell中執行, 不會影響到當前shell, 看起來就和沒執行一樣. 子shell是另外一個很重要的概念, 感興趣的讀者可自行瞭解.

由於source completion_script的作用範圍是當前shell, 所以我們設置的補全規則不會影響到其他用戶, 同時也會在重新登錄後失效. 要使補全規則永久生效, 我們將source completion_script本添加到 ~/.bashrc 或者 ~/.profile 文件中即可. 因為這兩個文件是Bash的初始化文件, 每次登錄Bash都會執行初始化文件, 所以就可以達到永久生效的效果.

8. 自動載入

最後提一下自動補全腳本是如何自動載入的. 入口是 /etc/bash.bashrc 這個文件, 其會調用 /usr/share/bash-completion/bash_completion/etc/bash_completion

$ cat /etc/bash.bashrc
......
......
if ! shopt -oq posix; then
  if [ -f /usr/share/bash-completion/bash_completion ]; then
    . /usr/share/bash-completion/bash_completion
  elif [ -f /etc/bash_completion ]; then
    . /etc/bash_completion
  fi
fi

查看 /etc/bash_completion 得知, 無論調用哪個文件, 最後實際上調用的都是 /usr/share/bash-completion/bash_completion

$ cat /etc/bash_completion
. /usr/share/bash-completion/bash_completion

打開 /usr/share/bash-completion/bash_completion 文件, 在2151行左右, 有以下一段代碼, 大概意思就是會執行 /etc/bash_completion.d 中的每個文件, 所以, 我們將自動補全腳本放在這個路徑下, 並設置好讀許可權, 每次登錄系統就會自動載入, 也可以達到永久生效的效果.

$ cat /usr/share/bash-completion/bash_completion
......
......
compat_dir=${BASH_COMPLETION_COMPAT_DIR:-/etc/bash_completion.d}
if [[ -d $compat_dir && -r $compat_dir && -x $compat_dir ]]; then
    for i in "$compat_dir"/*; do
        [[ ${i##*/} != @($_backup_glob|Makefile*|$_blacklist_glob) \
            && -f $i && -r $i ]] && . "$i"
    done
fi

實際上, Ubuntu中一般的自動補全腳本一般放在 /usr/share/bash-completion/completions/, 也會自動載入, 入口是 /etc/bash_completion.d 的2132行左右寫道了complete -D -F _completion_loader, 這裡就不展開講了.

9. 參考

  1. https://www.gnu.org/software/bash/manual/bash.html#Programmable-Completion-Builtins

您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 在日常的工作生產中,經常會有將將生產數據或者一些信息主動推送給相關的管理人員,我們公司在開發WMS系統時,為了倉庫的儲存安全,需要在危廢品庫存達到一定的儲量時,自動通知倉管員去處理危廢品,所以就需要程式自動的通過企業微信告知倉管員,這個時候就需要用到企業微信的機器人了。 現在我所知道的企業微信機器人 ...
  • > 下午寫了一個操作`XML`文件的類庫,後來不用了,~~水篇~~文章存個檔📋 ## 整體功能 `XMLHelper.cs`主要提供以下功能: 1. 載入XML文件:從文件路徑或字元串中載入XML文檔,並返回`XmlDocument`對象。 2. 保存XML文件:將XmlDocument對象保存為 ...
  • [TOC] 上個月換工作,新項目又要重新搭建基礎框架,把日誌實現部分單獨記錄下來方便以後參考。 # 自定義日誌類 代碼大部分使用ChatGPT生成,人工進行了測試和優化,主要特點: * 線程安全,日誌非同步寫入文件不影響業務邏輯 * 支持過期文件自動清理,也可自定義清理邏輯 * 緩存隊列有記憶體上限防呆 ...
  • # 個人博客-給推薦文章添加排序欄位 # 前言 前篇文章優化了推薦文章的載入,但是呢,還是不太滿意,之前是按照文章的發佈日期去排序的,既然是推薦文章,還是得用一個欄位去專門管理順序。 設計思路: 給推薦文章表添加一個排序欄位,然後寫一個修改方法即可。 # 資料庫欄位 這裡的數據類型以sqlite3為 ...
  • 大家好,我是沙漠盡頭的狼。 在 [Dotnet9](https://dotnet9.com) 上線線上小工具和小游戲後,伺服器的壓力感覺挺大的,打開25個頁面,記憶體占用170MB左右,CPU保持在60~70%,看來Server真不適合搞這類交互較多的程式(伺服器配置:2核4G記憶體),所以站長加急上線 ...
  • 支持.Net/.Net Core/.Net Framework,可以部署在Docker, Windows, Linux, Mac。 Redis作為一款主流的緩存工具在業內已廣受歡迎。本文將會介紹操作Redis的一種最簡單的方法。 本文假定你身邊已有安裝好的Redis應用,該應用的網路地址為(ip+p ...
  • 問題:在 Linux 下的 vim 編輯過程中,由於某種原因異常退出正在編輯的文件,再次編輯該文件時,會出現如下提示:[O]pen Read-Only, (E)dit anyway, ®ecover, (D)elete it, (Q)uit, (A)bort: 原因:使用vim編輯文件實際是先cop ...
  • MRS相同功能代碼管理應用筆記 使用 MounRiver(以下簡稱 MRS )進行 RISC-V 單片機開發時,工程目錄下往往存在多個文件夾與文件,我們只需要著重關註截圖中紅框所示的部分,它們自上而下分別是內核、調試、鏈接、外設、啟動與用戶文件夾。除此之外的文件夾與文件,均是由編譯器自動創建,開發過 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...