--- title: '建構腳本 - 互動輸入' disqus: kyleAlien --- 建構腳本 - 互動輸入 === ## Overview of Content 建議先閱讀 [**Shell 變量**](https://hackmd.io/tJkxCeTpQuK7TqVmn1PHVw?view) 章節 :::success * 如果喜歡讀更好看一點的網頁版本,可以到我新做的網站 [**DevTech Ascendancy Hub**](https://devtechascendancy.com/) 本篇文章對應的是 [**Shell 掌握參數處理與用戶輸入技巧 | Shell 結構化 &輸入互動**](https://devtechascendancy.com/shell-script-options-params_user-input/) ::: [TOC] ## 參數常用命令 以下介紹如何透過特定的命來來取得輸入給 shell 的參數;參數輸入給 Shell script 的方式如下所示 ```shell= # 腳本名為「hello_script.sh」 # 參數為 「10 Apple 20 Banana」 ./hello_script.sh 10 Apple 20 Banana ``` 而接下來我們就要去獲取輸入給 `hello_script.sh` 腳本的參數 ### basename 命令:取得腳本名、基礎名 * 我們知道腳本名參數是 `$0`,可以透過它取得腳本名稱;當使用者傳入完整路徑時,我們 **可以用 `basename` 命令取出 不包含路徑的腳本名** 也就是 **`basename` 命令可以單取出腳本、參數的基礎名稱** > 以下範例腳本名為 `./read_cmd_2.sh`,參數為 `$PWD/read_cmd_2.sh` ```shell= #!/bin/bash # 包含相對路徑來取得腳本名 echo "The script name: $0" # 不包含相對路徑,只輸出腳本名稱 echo "The script name, and use 'basename': $( basename $0 )" # 為輸入 $1 參數 name=$( basename $1 ) # 使用 basename 取出參數的腳本名 echo "The total path($1), and use 'basename': $name" ``` > ![](https://i.imgur.com/XIehrvX.png) * `basename` 也可以 **用來消除副檔名**,使用方式如下 > 以下範例直接使用 `basename` 命令,並對其輸入兩個參數 `hello.sh`、`.sh` ```shell= basename hello.sh .sh ``` > ![](https://hackmd.io/_uploads/Bk2QSFLih.png) 更近一步使用的範例:使用它來轉換所有 `gif` 成為 `png` 檔 ```shell= #!/bin/sh for file in *.gif; do if [ ! -f $file]; then echo "The ($file) is not a file." fi fileName=$(basename $file .git) echo "Converting $fileName.git to $fileName.png" giftopnm $fileName.git | pnmtopng > $fileName.png done ``` ### shift 命令:移動輸入變量 * **shift 命令的功能**: 將除了腳本名 (`$0`) 之外的參數全部向左移動一個位置;這個命令最常用在 **當我們不知道使用者對腳本輸入多少參數時,我們可以一個參數一個參數處理、判斷** > shift 預設的偏移量為 1,但它也可以一次性偏移多個位置 ```shell= shift [偏移量=1] ``` :::warning * **shift 移除的注意事項**: **被移除的參數就不存在,無法再次取得**,如果有顧慮的話可以先用副本參數本存(可以存為另一個參數) ::: * **`shift` 命令的使用範例**: 以下範例,一次移動輸入參數的兩個偏移量 ```shell= #!/bin/bash varCount=0 while [ -n "$1" ] ; do (( varCount++ )) echo "Parameter #$varCount = $1" # 移動兩個參數 shift 2 done ``` 從下圖結果中,我們也可以看到,對腳本輸入的參數為 `1 2 3 a b c`,如果一次移動兩個偏移量時,就會輸出為 `1 3 b` > ![](https://i.imgur.com/5keQ00t.png) ## 處理 options 指令 在使用其他命令時常常會有 `options` 功能(可選功能),腳本接到 options 功能後會依照設定去處理 > e.g: ls -l > > 其中 ls 是命令,而 `-l` 就是 options ### 標準 Options * Linux 每個命令的指令 options 的意義都不同,但它有一些大部分 options 代表的意義(不代表全部),我們可以透過這些標準 options 來猜測這個命令的使用 | options | 說明 | | - | - | | -a | 顯示所有對象 | | -c | 生成一個計數 | | -d | 指定一個目錄 | | -e | 擴展一個對象 | | -f | 指定讀入數據的文件 | | -h | 顯示命令的幫助信息 | | -i | 忽略文本大小寫 | | -l | 產生輸出的長格式版本 | | -n | 使用非交互模式(批處理) | | -o | 將所有輸出重定向到的指定的輸出文件 | | -q | 以安靜模式運行 | | -r | 遞歸地處理目錄和文件 | | -x | 排除某個對象 | | -y | 對所有問題回答 yes | :::info 命令完整的 options 建議使用 `man` or `--help` 查詢 ::: ### 手動處理 options:使用 while、case、shift * 使用 ^1.^ `while` 語句、^2.^ `case-in` 語句、^3.^ `shift` 命令,組成不斷接收判斷 options 的腳本 以下範例會取出所有對腳本的輸入,並且 `case` 先指定處理 `-a`、`-b` 這兩個 options,其他的 options 則輸出 `illegal option` ```shell= #!/bin/bash # 參數不為空的狀況,就持續執行 while # `-n` 是 字串長度非 0 while [ -n "$1" ] ; do case "$1" in -a) echo "Get option of -a" ;; -b) echo "Get option of -b" ;; *) echo "$1 is not illegal option" ;; esac # 位移輸入參數 shift done ``` 從下圖中,我們可以看到輸入的參數為 `-a -b -c -d` 這些 options,而處理的則只有 `-a -b` 兩個 options > ![](https://i.imgur.com/UyEviLJ.png) ### 雙破折號:分離 options & 參數 * Linux Shell 中的雙破折號(`--`)功能: **用來表示 options 列表已經結束**,之後的皆是參數,概念程式如下所示… ```shell= # 命令 `add` # option `-a` # params `1`、`2` add -a -- 1 2 ``` * 添加了判斷雙破折(`--`)符號,並在判斷是雙破折號後就 break 迴圈,代表已經處理完全的 options,接下來要處理的就是參數;範例如下… > 以下範例會分為「Options」、「參數」兩個分開管理 ```shell= #!/bin/bash # 處理 Options while [ -n "$1" ] ; do case "$1" in -a) echo "Get option of -a" ;; -b) echo "Get option of -b" ;; # 處理完 Options --) shift break;; *) echo "$1 is not illegal option" ;; esac # 位移,移除處理完畢的 option shift done # 開始處理參數,透過 $@ 取得剩餘的所有參數 for param in $@ ; do echo "param: $param" done ``` 下圖中,我們對腳本 `handle_options_2.sh` 輸入… Options `-a -b -c`、參數 `11 22 33`,並且中間使用 `--` 號隔開 > ![](https://i.imgur.com/EWAy7Ri.png) ### 處理 options 指定的參數 * 當 option 之後有指定的參數,那在讀取完 options、參數之後,記得使用 `shift` 命令位移 以下範例,我們會對 `handle_options_3.sh` 腳本輸入幾個 Option,而我們就是要取得指定 Option 之後的參數,指定 Option 如下 * **Option `-a` 之後 1 個參數** * **Option `-b` 之後 2 個參數為** ```shell= #!/bin/bash # `handle_options_3.sh` 腳本 while [ -n "$1" ] ; do case "$1" in -a) echo "Get option of -a, param=($2)" # 取得 -a 之後的參數後,使用 shift 移除參數 shift ;; -b) echo "Get option of -b, param=($2, $3)" # 取得 -b 之後的兩個參數後,使用 shift 移除兩個參數 shift 2 ;; --) shift break ;; *) echo "$1 is not illegal option" ;; esac # 移除處理完畢的 option shift done for param in $@ ; do echo "param: $param" done ``` 下圖中,我們對`handle_options_3.sh` 腳本輸入,Option `-a` 之後的參數為 `27`、Option `-b` 之後的參數為 `34 "Hello"` > ![](https://i.imgur.com/bASJzzr.png) ### getopt 命令:格式化輸入 * `getopt` 命令是一種 **格式化字串的命令(將字串依照命令格式進行格式化)**;`getopt` 命令格式如下 opstring 在可以指定需要的 options 格式 ```shell= getopt opstring parameters ``` 接下來解釋如何使用 1. **`getopt` 的 `opstring` 用來格式化輸入**,也就是指定如處理輸入 ```shell= # 規定 options 有:`a`、`b`、`c`、`d` 四個 # # `b:` 代表 b 後面有一個參數,而 `a`、`c`、`d` 選項後沒有參數 getopt ab:cd -a -b test1 -cde test2 test3 ``` > ![](https://i.imgur.com/OZZJskn.png) 2. **`getopt` 添加 `-q`**:可以省略錯誤資訊 ```shell= getopt ab:cd -a -b test1 -cde test2 test3 ``` > ![](https://i.imgur.com/OWLXV9q.png) :::success * 如果要使用長指令 getopt 命令要修改為 `getopt -q -a --long-option-name` * **`set` 替換輸入命令**: set 命令中有一個選項(options)就是是雙破折號(`--`),它會將命令行參數替換成 set 命令的命令行值 > 也就是說使用 `set -- <替換數值>`,這個「替換數值」就會替代原來的輸入數據;使用 `set --` 的範例如下 ```shell= #!/bin/bash echo "param: $1" set -- "Hello world" echo "after set, param: $1" ``` 下圖中,我們對 `set_cmd.sh` 腳本輸入 `123` 參數,在經過命令替換後,我們可以看到輸入命令被改為 "Hello world" > ![](https://i.imgur.com/O1ff5Vu.png) ::: * **`getopt` 命令的使用範例**: 在這個腳本中,我們先使用 `set --` 命令替換,替換使用者的輸入為 `getopt -q a:b: "$@"`(**這個動作就是在做輸入參數格式化**) 格式化輸入後就可以用來上面的 while、case、shift 命令來處理輸入 ```shell= #!/bin/bash # 格式化輸入 set -- $( getopt -q a:b: "$@" ) echo "After getopt: $*" # 處理 Options while [ -n "$1" ] ; do case "$1" in -a) echo "Get option of -a, param=($2)" shift ;; -b) echo "Get option of -b, param=($2)" shift ;; --) shift break ;; *) echo "$1 is not illegal option" ;; esac shift done # 處理剩餘的參數 for param in $@ ; do echo "param: $param" done ``` :::danger getopt 無法讀取超過一個參數,讀取超過一個參數會出錯! ::: > ![](https://i.imgur.com/mGumIoe.png) ### getopts 命令:加強 getopt 命令、自動處理輸入 :::warning 請注意這個命令與 `getopt` 命令不同,它多了 `s`! ::: * **`getopts` 命令,它有幾個特點**… 首先先來看看 `getopts` 命令的格式 ```shell= getopt opstring parameters ``` 1. 變數的處理 **不再透過 `set --` 處理**,**`getopts` 命令會自動將參數一個一個處理**,參數處理結果成功會產生一個退出碼 0 > `getopts` 會使用「**`opt` 關鍵字**」來處理使用者輸入的字串 :::info **使用 `getopts` 命令後不需要使用 `shift` 移動** ::: 2. **使用環境變量 `$OPTIND`**:`$OPTIND` 表示當前處理到哪個變數的 index > `$OPTIND` 從腳本名 (`$0`) 從 1 開始算;算到 參數結束 3. **使用環境變量 `$OPTARG`**:`$OPTARG` 表示當前正在處理的變數 > 只有有 option 時 `$OPTARG` 才會有值 4. 如果要省略錯誤只需要在 `opstring` 前,添加 `:` 符號 * **`getopts` 命令的使用範例**: ```swift= #!/bin/bash while getopts :ab: opt ; do echo "Current index: $OPTIND, arg: $OPTARG" case "$opt" in a) echo "Get the -a options" ;; b) echo "Get the -b options, param: $OPTARG";; *) echo "Other options: $OPTARG" ;; esac echo done ``` > ![](https://i.imgur.com/wY75j7X.png) :::warning * `getopts` 仍無法取得一個 options 多個參數的狀況 ::: :::success * **`getopts` 自動分離 options 與 參數**: getopts 在分析 options、參數的時候,**options 與 參數 不需要空格** > e.g: 我可以下命令 `./getopts.sh -a -b2`,同樣可以分析出 options `-b` 之後接的參數是 2 ::: ## 取得用戶輸入 在交互式版本的腳本中常常需要使用者輸入資料,這時我們就需要取得用戶輸入,再分析使用者輸入的數據 ### read 命令:取得鍵盤輸入 * **`read` 命令**: 該命令可以主動反應到 Shell 要求使用者輸入資料,並讀取到腳本中,其格式如下 ```shell= read options local_params ``` 常用 Options 如下表 | Options | 說明 | | - | - | | -p | 添加輸入提示 | | -t | 以秒為單位,計時限制使用者的輸入時間;超時後返回一個非零狀退出態碼 | | -n<字符數量> | 讀取指定的字符數量 | | -s | 安靜模式輸入(不會顯示使用者輸入的資訊),同常使用在輸入密碼 | * **`read` 命令使用範例**: 1. **read 基礎使用** ```shell= #!/bin/bash echo -n "Enter target file: " read file_name echo "Your target file is $file_name" ``` > ![](https://i.imgur.com/mXHRjKZ.png) 2. **使用輸入提示 (`-p`)、限制輸入時間 (`-t`)** ```shell= #!/bin/bash echo -n "Enter target file: " read -t 5 -p "(In 5s): " file_name if [ -z $file_name ] ; then echo echo "Input data too slow." else echo "Your target file is $file_name" fi ``` > ![](https://i.imgur.com/6rqzm7G.png) 3. **讀取指定字符數量 (`-n`)** ```shell= #!/bin/bash read -n1 -p "Accept(Y/N): " res echo case $res in Y|y) echo "Ok, start doing." ;; N|n) echo "Quit!" exit ;; *) echo "Unknow options($res)" ;; esac echo "Finish script." ``` > ![](https://i.imgur.com/RHIGqDO.png) 4. **安靜模式輸入 (`-s`)** ```shell= #!/bin/bash read -s -p "Input passwd: " pw echo echo "Input passwd: $pw" ``` > ![](https://i.imgur.com/lP4F7lo.png) ### read 命令:從 Pipe 讀取輸入 * Read 可以讀取 Pipe 文本直到換行(以換行做為分隔),透過這個特性就可以逐行讀取文件文本,範例如下: 以下範例透過 `cat` 來讀取文件,並輸入給 `read` 命令 ```shell= #!/bin/bash count=1 cat testFile | while read lineTxt ; do echo "Line $count: $lineTxt" (( count++ )) done ``` > ![](https://i.imgur.com/u8wqAey.png) ## Appendix & FAQ :::info ::: ###### tags: `Linux Shell`