Linux 命令行與 shell 腳本編程大全 14 處理用戶輸入

介紹 shell 腳本如何接收用戶的輸入

更多精彩

導覽

  1. 運行腳本時可以往腳本中傳入命令行參數,例如 ./param.sh 1 2 3
  2. shift 命令可以從右向左移動傳入的參數位置,類似於迭代器,shift n 命令可以指定參數移動的位置數,默認爲 1
  3. $# 可以在腳本中直接獲取傳入的參數總數,${!#} 可以在腳本中直接獲取傳入的最後一個參數
  4. $* 可以在腳本中直接獲取傳入的所有參數,但獲取到的內容是一整個字符串
  5. $@ 可以再腳本中直接獲取傳入的所有參數,而且獲取到的內容是可以進行參數遍歷的字符串
  6. 運行腳本時可以往腳本中傳入選項,例如 ./option.sh -a -b -c
  7. getopt 命令可以用於指定腳本的參數和選項的傳入規則,在腳本中的基本語法是 set -- $(getopt ab:cd "$@")
  8. getopts 命令不僅可以指定腳本的參數和選項的傳入規則,而且還支持帶空格、引號的參數,在腳本中的基本語法是 getopts ab:cd opt
  9. read 命令用於接收用戶輸入,可以通過指定變量名來分別接收用戶的單個輸入或多個輸入,只需要輸入內容使用空格進行分隔
  10. read -p 命令可以簡化腳本的輸入流程,將輸入提示語句和輸入接收語句合併爲一條語句
  11. read -t 命令可以指定輸入超時的時間
  12. read -n 命令可以指定輸入允許接收的字符數量,達到字符數量時會自動執行下一步操作
  13. read -s 命令可以隱藏用戶輸入的內容

14.1 命令行參數

  1. 使用命令行參數是向 shell 腳本傳遞數據的最基本方式

14.1.1 讀取參數

  1. 通過命令行參數傳遞到 shell 腳本中的數據會被標記爲 位置參數( Positional Parameter )
  2. 位置參數的索引從 1 開始,因爲 0 表示執行 shell 腳本時用的名稱
    • 例如 ./input.sh 1 2 ,那麼在 input.sh 中 ,$0 的值是 ./input.sh$1 的值是 1 ,$2 的值是 2
  3. 通過命令行參數傳遞的參數數量最好在 10 個以下,因爲從第 10 個開始就無法直接使用 $1 這樣的格式獲取參數了,而需要使用 ${10} 這樣的格式才能獲取到參數
  4. 寫一個簡單的例子演示一下,如下圖

14.1.2 使用 basename 讀取腳本名

  1. 雖然通過 $0 可以獲取腳本在執行時的名稱,但如果腳本在執行時攜帶了相應的路徑,也會出現在 $0 參數中
  2. 使用 basename 就可以過濾掉 $0 參數中腳本名以外的內容,從而得到一個純淨的腳本名稱,如下圖

14.1.3 測試參數

  1. 因爲命令行參數是可選的,在執行腳本時,可以添加也可以不添加,所以在腳本中使用命令行參數時,最好是先判斷一下,如下圖
    • 在使用 test 命令對命令行參數進行條件判斷時,不能直接使用 $1 ,需要使用 "$1" 才能正確獲取到參數

14.2 特殊參數變量

14.2.1 參數統計

  1. 使用 $# 可以輸出腳本在執行時傳入了多少個參數,如下圖
  2. 既然 $# 可以返回傳入參數的數量,那麼理論上使用 ${$#} 就可以獲取到最後一個參數的內容,但其實不是
  3. 要快速獲取最後一個參數的內容需要通過 ${!#} ,如下圖

14.2.2 抓取所有的數據

  1. 使用 $* 可以獲取通過命令行傳入的所有參數,但這些參數會被作爲一個字符整體被保存
    • 將傳入的所有參數作爲一個值
  2. 使用 $@ 也可以獲取通過命令行傳入的所有參數,這些參數則會被作爲一個可以循環的字符串被保存
    • 將傳入的每個參數作爲單獨的值
  3. 寫一個簡單的例子演示一下,如下圖
    • 在對兩者採用了相同遍歷方式的操作之後,通過 $* 得到的是一個參數,而 $@ 得到的是 5 個參數

14.3 使用 shift 命令移動變量

  1. 使用 shift 命令時,默認會將每個參數變量向左移動一個位置,也就是參數 $3 -> $2
    • 類似於 Java 中的迭代器
  2. 而除了參數 $0 ,其他的參數的位置在被後續參數替換後,自身就不復存在了
    • 因爲 $0 保存着程序名,所以不會改變
  3. 寫一個例子簡單演示一下,如下圖
    • while 循環中每次獲取的參數都是 $1 ,但獲取到的內容卻能夠發生變化
    • 這就是因爲每次循環後都通過 shift 命令把後續的參數向左移動了一位

14.3.1 使用 shift 命令移動指定數量的變量

  1. 使用 shift num 命令可以一次移動多個位置的變量,如下圖

14.3.2 使用 shift 命令移動多個位置時,傳入的參數必須是位置數的倍數

  1. 上從圖可知,使用 shift 2 命令將參數的位置移動兩位,然後腳本在執行的時候傳入了 6 個參數
  2. 這個時候如果只傳入 5 個參數,那麼腳本就會無限循環,如下圖

14.4 處理選項

  1. 執行腳本時,除了能往腳本中傳入命令行參數,還能 在單破折號後面跟字母 往腳本中傳遞 選項

14.4.1 查找選項

14.4.1.1 處理簡單選項

  1. 當腳本在執行時傳入了選項,通過 case 命令進行判斷是最好的處理方式,如下圖
    • 爲了讓傳入的多個選項能在一次執行過程中都被執行到,需要先使用一個 while + shift 對選項進行遍歷
    • 在每次遍歷時都通過 case 命令對選項的具體參數進行判斷
    • 同時還通過 *) 提供一個默認判斷

14.4.1.2 分離參數和選項

  1. 可以通過 雙破折號( – ) 將參數和選項進行分離,這樣才執行腳本時就可以同時傳入兩者
  2. 這裏的說法 不是指直接將參數和選項進行混排 ,而是 先傳入參數後傳入選項,或者先傳入選項後傳入參數 ,在這兩者之間通過雙破折號進行分離,如下圖
    • 才判斷選項的 case 命令中,新增了一個 --) 分支,用於判斷分支的存在
    • 當從傳入的內容中檢索到這個分支時,會先執行一個 shift ,目的是將雙破這行這個分隔符跳過去
    • 之後再執行一次 break 是爲了跳出 while 循環,進行後續屬於參數遍歷的 for 循環
    • 可以看到,當直接執行 ./option-param.sh -a -b -e a b e 時,由於沒有檢測到 --) 分支,所以後續的參數也被識別成了錯誤的選項
    • 但是當使用雙破折號將選項和參數分離後,就能順利遍歷到選項和參數了
  3. 從上述這個例子,其實我們可以思考一下,雙破折號真的是作爲一個 強語法的特殊符號 來被 shell 識別的嗎?很顯然不是!
  4. 我們可以對上述腳本稍作修改,就可以得出下圖的效果
    • 將上圖中的 --) 分支判斷部分修改爲下圖中的 -e) 分支判斷
    • 然後執行上圖中第一次執行導致錯誤結果的語句,卻可以得到和上圖第二次正確結果一致的輸出
    • 這說明 雙破折號作爲參數和選項分隔符只是一個規範上的約定,並不是強語法的特殊符號

14.4.1.3 處理帶值的選項

  1. 最複雜的情況就是參數和選項的混合搭配,但是在 shell 腳本中,當參數和選項混排後,對應的參數會被認爲是前一個選項的值
  2. 例如 ./option.sh -a str1 -b str2 -c ,這裏的 str1 就會被認爲是 -a 選項的值,需要在 -a 選項的分支中進行操作
  3. 寫一個簡單的例子演示一下,如下圖
    • -a 後續的參數識別之後會執行一次 shift 命令的目的是爲了將參數跳過

14.4.2 使用 getopt 命令

14.4.2.1 基礎語法

  1. getopt 命令用於將通過簡化形式傳入腳本的命令行參數和選項進行規範化
    • 比如在執行腳本時使用 ./option.sh -abc ,如果腳本中存在 getopt 命令,就可以被轉換爲 -a -b -c
    • 這樣就可以被腳本正確的識別,同時用戶的輸入也得到了簡化
  2. 在腳本執行時,會先將這些傳入腳本的命令行參數和選項自動轉換爲適當的格式,讓腳本在使用這些參數和選項時更加方便
    • 比如它可以定義哪些字母選項是有效的,哪些字母選項需要指定參數值
  3. getopt 命令可以在命令行執行執行,雖然這樣做沒有什麼實際意義,但是可以幫助我們熟練一下語法,如下圖
    • ab:cd 就是通過 getopt 命令指定的參數選項規則,表示當前可以傳入 abcd 選項,而且選項 b 後面存在一個參數
    • -ab test -cd 就是實際傳入的參數內容,第二行的輸出結果就是對這些傳入的內容進行格式化後的結果
  4. 默認如果傳入的內容和規則不匹配,會拋出錯誤,可以通過 -q 選項忽略錯誤,如下圖

14.4.2.2 在腳本中使用

  1. getopt 命令自然是要在腳本中使用纔有意義,而且在腳本中使用的語法基本都是固定的
  2. getopt 命令是用來規範參數和選項的,所以肯定是要放在腳本的最前端,這個也是毋庸置疑
  3. 一般只需要在腳本前端加上 set -- $(getopt -q ab:cd "$@") 即可
    • ab:cd 是可變化的部分,指的是具體參數和選項的匹配方式
  4. 寫一個簡單的例子演示一下,如下圖
    • 這個例子是在 centOS 環境下運行的,因爲 macOS 中的 set 命令無法按預期執行出效果
  5. 上圖中 case 命令的 --) 分支判斷是必須添加的,否則在執行命令時最後一個分支會被多判斷一次
  6. 至於具體是爲什麼多判斷一次,可以在執行腳本前添加 sh -x 來對腳本進行 DEBUG ,如下圖
    • 首先在原來的腳本中註釋掉 --) 分支
    • 然後在與之前相同的腳本執行語句之前加上 sh -x ,之後就可以很清楚的看到腳本的執行過程
    • 在執行過程的第一處可以看到,傳入的 -ab hello -cd 被解析後,最後多出來一個 --
    • 於是在腳本的最後,這個 -- 就被作爲傳入的選項執行了
    • 這就是爲什麼如果必須 case 命令添加 --) 分支,並且通過 shift + break 命令跳過的原因

14.4.3 使用更高級的 getopts

  1. getopts 命令是 getopt 命令的複數版本( 其實說是升級版更貼切 )
    • 關鍵在於 getopts 可以處理 帶空格和引號的參數 ,而 getopt 不行
  2. getoptsgetopt 更優秀的地方在於,每次調用時,只會處理一個參數,並且在處理完所有參數後會返回一個非 0 的退出狀態碼
    • getopt 是將參數和選項生成爲一個完整的輸出
  3. getopts 也可以忽略錯誤信息,只需要在參數選項規則之前加上一個冒號即可
  4. 如果某個選項後面需要跟一個參數,這個參數會被存儲在 OPTARG 變量中
  5. 還有一個變量是 OPTIND ,用於存放 getopts 當前正在處理的參數位置
  6. 寫一個簡單的例子統一演示一下,如下圖
  7. 可以對比一下兩個命令在處理相同內容時的區別,如下圖
    • 首先,getopts 在處理參數時可以直接加入到 while 的條件判斷中,這讓語法變的更簡單易懂
    • 其次,getoptscase 判斷也更簡單,每個分支前不需要使用 -a) ,而是直接 a) 即可
    • 用於跳過 -- 選項的分支判斷在 getopts 中也不需要了
    • 最後最關鍵的是,在 while 的每次循環結束之前,不需要使用 shift 命令來對參數進行位置移動
  8. 如果在參數中添加空格,getopts 也可以很好的處理,如下圖
  9. 如果在參數選項中傳入了與定義的規範不匹配的選項,會被輸出一個問號,如下圖
    • 可以看到,-d 選項因爲沒有找到對應的分支判斷,所以被識別爲其他選項,但輸出內容中可以正確的顯示出來
    • 但是 -e 選項因爲不在 getopts 定義的規範中,所以最後只輸出來一個問號
  10. 如果說選項處理完畢後,還需要處理單獨的參數列表,則需要在處理參數之前,使用 shift $[ $OPTIND - 1 ] 來跳過參數之前的所有選項,如下圖
    • shift $[ $OPTIND - 1 ] 的意思如果不理解的話,這裏簡單解釋一下
    • 首先,$[ $OPTIND - 1 ] 的意思是用 getopts 當前執行的選項的位置數減一,爲什麼要減一,因爲命令行參數的位置從 0 開始的
    • 所以就相當於是使用 shift 將傳入的參數內容中被 getopts 執行過的選項全部跳過
  11. 如果還是不理解,可以將上述腳本的 shift $[ $OPTIND - 1 ] 語句註釋後再使用相同命令執行,如下圖
    • 可以看到,所有的選項都被當做參數輸出了

14.5 將選項標準化

  1. 腳本的選項雖然沒有一個強制的語法規範,但有不少建議性規範,如果能遵守這些規範,會讓你編寫的腳本更通用,其他人在上手使用你的腳本時,學習門檻也更低
  2. 下圖中提供一些選項的常用含義

14.6 獲得用戶輸入

14.6.1 使用 read 命令進行基本的讀取

  1. 使用 read 命令可以接收用戶輸入,輸入的內容會被放置在後續的變量中,如下圖
    • echo -n 表示輸入內容後不換行
    • 可以看到,通過鍵盤輸入的 asing1elife 被存入到變量 name 中
  2. 使用 read -p 命令可以實現合併輸入前提醒語句的效果,如下圖
    • 少了 echo -n 語句,卻可以達到一樣的輸出效果
  3. 如果想講輸入的內容分配給多個變量,只需要將內容使用空格進行分隔,並且在 read 命令後使用多個變量進行接收,如下圖
  4. 如果在輸入的時候使用空格分隔了輸入內容,但是在 read 命令後沒有使用數量與之對應的變量分別接收,那麼剩餘的輸入內容都會被直接存放到最後一個變量中,如下圖
    • 可以看到,在腳本 read-p.sh 中,只有一個變量 name 用於接收輸入內容
    • 那麼所有的輸入內容都被存放到這個變量中
    • 在腳本 read-multi.sh 中,輸入內容通過逗號分隔後有 6 個,但是變量只有 3 個,所以從第 3 個輸入開始,之後的所有內容都被存放到了第三個變量中

  5. 如果不想在輸入命令的最後顯式的指定一個變量用於內容接收,也可以直接使用特殊環境變量 REPLY 來獲取輸入內容,如下圖
    • REPLY 變量其實就相當於一個默認的接收變量被隱式的放在了 read 命令的最後,所以會默認接收所有的輸入內容

14.6.2 超時

  1. 爲了防止用戶一直不輸入,而導致腳本的執行流行被阻斷,可以使用 read -t second 來指定一個定時器
  2. 當輸入時間超過指定的時間後,read 命令會返回一個非 0 的退出狀態碼,如下圖

14.6.2.1 字數控制

  1. 使用 read -nNum 可以控制輸入的字符數,例如 read -n1 就是當輸入 1 個字符時就開始判斷,如下圖
    • 當輸入一個字符時,不管輸入的是什麼,也不需要按回車鍵,腳本都會直接開始分支判斷

14.6.3 隱藏方式讀取

  1. 使用 read -s 命令可以避免輸入的內容出現在顯示器上,最典型的例子就是輸入密碼,如下圖
    • 實際上,數據還是會顯示,只是文本的顏色被設置成和終端的背景色一致,所以看不出來

14.6.4 從文件中讀取

  1. 使用 read 命令可以讀取文件中的數據,每次調用都會讀取一行文本,當文件中沒有剩餘內容時,會返回非 0 的退出狀態碼,如下圖
    • 首先使用 cat read-s.sh 讀取文件內容
    • 然後通過管道將數據直接傳遞給 read 命令
    • read 再將每次讀取的行爲通過 while 進行循環
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章