介紹 shell 腳本如何接收用戶的輸入
更多精彩
- 更多技術博客,請移步 IT人才終生實訓與職業進階平臺 - 實訓在線
導覽
- 運行腳本時可以往腳本中傳入命令行參數,例如
./param.sh 1 2 3
shift
命令可以從右向左移動傳入的參數位置,類似於迭代器,shift n
命令可以指定參數移動的位置數,默認爲 1$#
可以在腳本中直接獲取傳入的參數總數,${!#}
可以在腳本中直接獲取傳入的最後一個參數$*
可以在腳本中直接獲取傳入的所有參數,但獲取到的內容是一整個字符串$@
可以再腳本中直接獲取傳入的所有參數,而且獲取到的內容是可以進行參數遍歷的字符串- 運行腳本時可以往腳本中傳入選項,例如
./option.sh -a -b -c
getopt
命令可以用於指定腳本的參數和選項的傳入規則,在腳本中的基本語法是set -- $(getopt ab:cd "$@")
getopts
命令不僅可以指定腳本的參數和選項的傳入規則,而且還支持帶空格、引號的參數,在腳本中的基本語法是getopts ab:cd opt
read
命令用於接收用戶輸入,可以通過指定變量名來分別接收用戶的單個輸入或多個輸入,只需要輸入內容使用空格進行分隔read -p
命令可以簡化腳本的輸入流程,將輸入提示語句和輸入接收語句合併爲一條語句read -t
命令可以指定輸入超時的時間read -n
命令可以指定輸入允許接收的字符數量,達到字符數量時會自動執行下一步操作read -s
命令可以隱藏用戶輸入的內容
14.1 命令行參數
- 使用命令行參數是向 shell 腳本傳遞數據的最基本方式
14.1.1 讀取參數
- 通過命令行參數傳遞到 shell 腳本中的數據會被標記爲 位置參數( Positional Parameter )
- 位置參數的索引從 1 開始,因爲 0 表示執行 shell 腳本時用的名稱
- 例如
./input.sh 1 2
,那麼在input.sh
中 ,$0
的值是./input.sh
,$1
的值是 1 ,$2
的值是 2
- 例如
- 通過命令行參數傳遞的參數數量最好在 10 個以下,因爲從第 10 個開始就無法直接使用
$1
這樣的格式獲取參數了,而需要使用${10}
這樣的格式才能獲取到參數 - 寫一個簡單的例子演示一下,如下圖
14.1.2 使用 basename 讀取腳本名
- 雖然通過
$0
可以獲取腳本在執行時的名稱,但如果腳本在執行時攜帶了相應的路徑,也會出現在$0
參數中 - 使用
basename
就可以過濾掉$0
參數中腳本名以外的內容,從而得到一個純淨的腳本名稱,如下圖
14.1.3 測試參數
- 因爲命令行參數是可選的,在執行腳本時,可以添加也可以不添加,所以在腳本中使用命令行參數時,最好是先判斷一下,如下圖
- 在使用
test
命令對命令行參數進行條件判斷時,不能直接使用$1
,需要使用"$1"
才能正確獲取到參數
- 在使用
14.2 特殊參數變量
14.2.1 參數統計
- 使用
$#
可以輸出腳本在執行時傳入了多少個參數,如下圖
- 既然
$#
可以返回傳入參數的數量,那麼理論上使用${$#}
就可以獲取到最後一個參數的內容,但其實不是 - 要快速獲取最後一個參數的內容需要通過
${!#}
,如下圖
14.2.2 抓取所有的數據
- 使用
$*
可以獲取通過命令行傳入的所有參數,但這些參數會被作爲一個字符整體被保存- 將傳入的所有參數作爲一個值
- 使用
$@
也可以獲取通過命令行傳入的所有參數,這些參數則會被作爲一個可以循環的字符串被保存- 將傳入的每個參數作爲單獨的值
- 寫一個簡單的例子演示一下,如下圖
- 在對兩者採用了相同遍歷方式的操作之後,通過
$*
得到的是一個參數,而$@
得到的是 5 個參數
- 在對兩者採用了相同遍歷方式的操作之後,通過
14.3 使用 shift 命令移動變量
- 使用
shift
命令時,默認會將每個參數變量向左移動一個位置,也就是參數$3 -> $2
- 類似於 Java 中的迭代器
- 而除了參數
$0
,其他的參數的位置在被後續參數替換後,自身就不復存在了- 因爲
$0
保存着程序名,所以不會改變
- 因爲
- 寫一個例子簡單演示一下,如下圖
- 在
while
循環中每次獲取的參數都是$1
,但獲取到的內容卻能夠發生變化 - 這就是因爲每次循環後都通過
shift
命令把後續的參數向左移動了一位
- 在
14.3.1 使用 shift 命令移動指定數量的變量
- 使用
shift num
命令可以一次移動多個位置的變量,如下圖
14.3.2 使用 shift 命令移動多個位置時,傳入的參數必須是位置數的倍數
- 上從圖可知,使用
shift 2
命令將參數的位置移動兩位,然後腳本在執行的時候傳入了 6 個參數 - 這個時候如果只傳入 5 個參數,那麼腳本就會無限循環,如下圖
14.4 處理選項
- 執行腳本時,除了能往腳本中傳入命令行參數,還能 在單破折號後面跟字母 往腳本中傳遞 選項
14.4.1 查找選項
14.4.1.1 處理簡單選項
- 當腳本在執行時傳入了選項,通過
case
命令進行判斷是最好的處理方式,如下圖- 爲了讓傳入的多個選項能在一次執行過程中都被執行到,需要先使用一個
while
+shift
對選項進行遍歷 - 在每次遍歷時都通過
case
命令對選項的具體參數進行判斷 - 同時還通過
*)
提供一個默認判斷
- 爲了讓傳入的多個選項能在一次執行過程中都被執行到,需要先使用一個
14.4.1.2 分離參數和選項
- 可以通過 雙破折號( – ) 將參數和選項進行分離,這樣才執行腳本時就可以同時傳入兩者
- 這裏的說法 不是指直接將參數和選項進行混排 ,而是 先傳入參數後傳入選項,或者先傳入選項後傳入參數 ,在這兩者之間通過雙破折號進行分離,如下圖
- 才判斷選項的
case
命令中,新增了一個--)
分支,用於判斷分支的存在 - 當從傳入的內容中檢索到這個分支時,會先執行一個
shift
,目的是將雙破這行這個分隔符跳過去 - 之後再執行一次
break
是爲了跳出while
循環,進行後續屬於參數遍歷的for
循環 - 可以看到,當直接執行
./option-param.sh -a -b -e a b e
時,由於沒有檢測到--)
分支,所以後續的參數也被識別成了錯誤的選項 - 但是當使用雙破折號將選項和參數分離後,就能順利遍歷到選項和參數了
- 才判斷選項的
- 從上述這個例子,其實我們可以思考一下,雙破折號真的是作爲一個 強語法的特殊符號 來被 shell 識別的嗎?很顯然不是!
- 我們可以對上述腳本稍作修改,就可以得出下圖的效果
- 將上圖中的
--)
分支判斷部分修改爲下圖中的-e)
分支判斷 - 然後執行上圖中第一次執行導致錯誤結果的語句,卻可以得到和上圖第二次正確結果一致的輸出
- 這說明 雙破折號作爲參數和選項分隔符只是一個規範上的約定,並不是強語法的特殊符號
- 將上圖中的
14.4.1.3 處理帶值的選項
- 最複雜的情況就是參數和選項的混合搭配,但是在 shell 腳本中,當參數和選項混排後,對應的參數會被認爲是前一個選項的值
- 例如
./option.sh -a str1 -b str2 -c
,這裏的str1
就會被認爲是-a
選項的值,需要在-a
選項的分支中進行操作 - 寫一個簡單的例子演示一下,如下圖
-a
後續的參數識別之後會執行一次shift
命令的目的是爲了將參數跳過
14.4.2 使用 getopt 命令
14.4.2.1 基礎語法
getopt
命令用於將通過簡化形式傳入腳本的命令行參數和選項進行規範化- 比如在執行腳本時使用
./option.sh -abc
,如果腳本中存在getopt
命令,就可以被轉換爲-a -b -c
- 這樣就可以被腳本正確的識別,同時用戶的輸入也得到了簡化
- 比如在執行腳本時使用
- 在腳本執行時,會先將這些傳入腳本的命令行參數和選項自動轉換爲適當的格式,讓腳本在使用這些參數和選項時更加方便
- 比如它可以定義哪些字母選項是有效的,哪些字母選項需要指定參數值
getopt
命令可以在命令行執行執行,雖然這樣做沒有什麼實際意義,但是可以幫助我們熟練一下語法,如下圖ab:cd
就是通過getopt
命令指定的參數選項規則,表示當前可以傳入abcd
選項,而且選項b
後面存在一個參數-ab test -cd
就是實際傳入的參數內容,第二行的輸出結果就是對這些傳入的內容進行格式化後的結果
- 默認如果傳入的內容和規則不匹配,會拋出錯誤,可以通過
-q
選項忽略錯誤,如下圖
14.4.2.2 在腳本中使用
getopt
命令自然是要在腳本中使用纔有意義,而且在腳本中使用的語法基本都是固定的getopt
命令是用來規範參數和選項的,所以肯定是要放在腳本的最前端,這個也是毋庸置疑- 一般只需要在腳本前端加上
set -- $(getopt -q ab:cd "$@")
即可ab:cd
是可變化的部分,指的是具體參數和選項的匹配方式
- 寫一個簡單的例子演示一下,如下圖
- 這個例子是在 centOS 環境下運行的,因爲 macOS 中的
set
命令無法按預期執行出效果
- 這個例子是在 centOS 環境下運行的,因爲 macOS 中的
- 上圖中
case
命令的--)
分支判斷是必須添加的,否則在執行命令時最後一個分支會被多判斷一次 - 至於具體是爲什麼多判斷一次,可以在執行腳本前添加
sh -x
來對腳本進行 DEBUG ,如下圖- 首先在原來的腳本中註釋掉
--)
分支 - 然後在與之前相同的腳本執行語句之前加上
sh -x
,之後就可以很清楚的看到腳本的執行過程 - 在執行過程的第一處可以看到,傳入的
-ab hello -cd
被解析後,最後多出來一個--
- 於是在腳本的最後,這個
--
就被作爲傳入的選項執行了 - 這就是爲什麼如果必須
case
命令添加--)
分支,並且通過shift
+break
命令跳過的原因
- 首先在原來的腳本中註釋掉
14.4.3 使用更高級的 getopts
getopts
命令是getopt
命令的複數版本( 其實說是升級版更貼切 )- 關鍵在於
getopts
可以處理 帶空格和引號的參數 ,而getopt
不行
- 關鍵在於
getopts
比getopt
更優秀的地方在於,每次調用時,只會處理一個參數,並且在處理完所有參數後會返回一個非 0 的退出狀態碼- 而
getopt
是將參數和選項生成爲一個完整的輸出
- 而
getopts
也可以忽略錯誤信息,只需要在參數選項規則之前加上一個冒號即可- 如果某個選項後面需要跟一個參數,這個參數會被存儲在
OPTARG
變量中 - 還有一個變量是
OPTIND
,用於存放getopts
當前正在處理的參數位置 - 寫一個簡單的例子統一演示一下,如下圖
- 可以對比一下兩個命令在處理相同內容時的區別,如下圖
- 首先,
getopts
在處理參數時可以直接加入到while
的條件判斷中,這讓語法變的更簡單易懂 - 其次,
getopts
的case
判斷也更簡單,每個分支前不需要使用-a)
,而是直接a)
即可 - 用於跳過
--
選項的分支判斷在getopts
中也不需要了 - 最後最關鍵的是,在
while
的每次循環結束之前,不需要使用shift
命令來對參數進行位置移動
- 首先,
- 如果在參數中添加空格,
getopts
也可以很好的處理,如下圖
- 如果在參數選項中傳入了與定義的規範不匹配的選項,會被輸出一個問號,如下圖
- 可以看到,
-d
選項因爲沒有找到對應的分支判斷,所以被識別爲其他選項,但輸出內容中可以正確的顯示出來 - 但是
-e
選項因爲不在getopts
定義的規範中,所以最後只輸出來一個問號
- 可以看到,
- 如果說選項處理完畢後,還需要處理單獨的參數列表,則需要在處理參數之前,使用
shift $[ $OPTIND - 1 ]
來跳過參數之前的所有選項,如下圖shift $[ $OPTIND - 1 ]
的意思如果不理解的話,這裏簡單解釋一下- 首先,
$[ $OPTIND - 1 ]
的意思是用getopts
當前執行的選項的位置數減一,爲什麼要減一,因爲命令行參數的位置從 0 開始的 - 所以就相當於是使用
shift
將傳入的參數內容中被getopts
執行過的選項全部跳過
- 如果還是不理解,可以將上述腳本的
shift $[ $OPTIND - 1 ]
語句註釋後再使用相同命令執行,如下圖- 可以看到,所有的選項都被當做參數輸出了
- 可以看到,所有的選項都被當做參數輸出了
14.5 將選項標準化
- 腳本的選項雖然沒有一個強制的語法規範,但有不少建議性規範,如果能遵守這些規範,會讓你編寫的腳本更通用,其他人在上手使用你的腳本時,學習門檻也更低
- 下圖中提供一些選項的常用含義
14.6 獲得用戶輸入
14.6.1 使用 read 命令進行基本的讀取
- 使用
read
命令可以接收用戶輸入,輸入的內容會被放置在後續的變量中,如下圖echo -n
表示輸入內容後不換行- 可以看到,通過鍵盤輸入的 asing1elife 被存入到變量 name 中
- 使用
read -p
命令可以實現合併輸入前提醒語句的效果,如下圖- 少了
echo -n
語句,卻可以達到一樣的輸出效果
- 少了
- 如果想講輸入的內容分配給多個變量,只需要將內容使用空格進行分隔,並且在
read
命令後使用多個變量進行接收,如下圖
- 如果在輸入的時候使用空格分隔了輸入內容,但是在
read
命令後沒有使用數量與之對應的變量分別接收,那麼剩餘的輸入內容都會被直接存放到最後一個變量中,如下圖- 可以看到,在腳本
read-p.sh
中,只有一個變量 name 用於接收輸入內容 - 那麼所有的輸入內容都被存放到這個變量中
- 在腳本
read-multi.sh
中,輸入內容通過逗號分隔後有 6 個,但是變量只有 3 個,所以從第 3 個輸入開始,之後的所有內容都被存放到了第三個變量中
- 可以看到,在腳本
- 如果不想在輸入命令的最後顯式的指定一個變量用於內容接收,也可以直接使用特殊環境變量
REPLY
來獲取輸入內容,如下圖REPLY
變量其實就相當於一個默認的接收變量被隱式的放在了read
命令的最後,所以會默認接收所有的輸入內容
14.6.2 超時
- 爲了防止用戶一直不輸入,而導致腳本的執行流行被阻斷,可以使用
read -t second
來指定一個定時器 - 當輸入時間超過指定的時間後,
read
命令會返回一個非 0 的退出狀態碼,如下圖
14.6.2.1 字數控制
- 使用
read -nNum
可以控制輸入的字符數,例如read -n1
就是當輸入 1 個字符時就開始判斷,如下圖- 當輸入一個字符時,不管輸入的是什麼,也不需要按回車鍵,腳本都會直接開始分支判斷
- 當輸入一個字符時,不管輸入的是什麼,也不需要按回車鍵,腳本都會直接開始分支判斷
14.6.3 隱藏方式讀取
- 使用
read -s
命令可以避免輸入的內容出現在顯示器上,最典型的例子就是輸入密碼,如下圖- 實際上,數據還是會顯示,只是文本的顏色被設置成和終端的背景色一致,所以看不出來
- 實際上,數據還是會顯示,只是文本的顏色被設置成和終端的背景色一致,所以看不出來
14.6.4 從文件中讀取
- 使用
read
命令可以讀取文件中的數據,每次調用都會讀取一行文本,當文件中沒有剩餘內容時,會返回非 0 的退出狀態碼,如下圖- 首先使用
cat read-s.sh
讀取文件內容 - 然後通過管道將數據直接傳遞給
read
命令 read
再將每次讀取的行爲通過while
進行循環
- 首先使用