---
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`