shell 命令行參數(基本)

命令行參數

\$0 表示程序名。
\$1 至 \$9則是位置參數。
\$# 表示參數的個數。
\$* 將所有參數當做一個整體來引用
\$@ 把每個參數作爲一個字符串返回,可以使用for循環來遍歷
\$? 最近一個執行的命令的退出狀態。0表示執行成功
\$_ 上一個命令的最後一個參數。使用快捷鍵 ESC+. 也是這個效果

位置參數

位置參數不止9個,更多的參數也是一樣支持的。只是要使用\${10}這樣的形式引用。
\$1 和 \${1}的效果是一樣的。
不用花括號的話,\$10 會被認爲是 \$1 和一個字符 0。

帶空格的參數值
每個參數都是用空格分隔的。要在參數值中包含空格,必須要使用引號(單引號或雙引號都可)。

將文本字符串作爲參數傳遞時,引號並非數據的一部分。它們只是表明數據的起止位置。

獲取腳本名

\$0 表示腳本名,但是不同的調用方法返回的結果也是不同的。下面的腳本就是簡單的打印\$0的值:

$ cat filename.sh 
#!/bin/bash
echo $0
$ ./ filename.sh
-bash: ./: 是一個目錄
$ cat filename.sh 
#!/bin/bash
echo $0
$ ./filename.sh
./filename.sh
$ bash filename.sh 
filename.sh
$ bash /root/filename.sh 
/root/filename.sh
$ 

使用 basename 命令
如果要使用腳本名稱來進行判斷,可以先用命令 basename 把路徑的信息給過濾掉。命令的效果如下:

$ basename /var/log/messages 
messages
$ 

所以上面的腳本可以修改成這樣:

$ cat filename.sh 
#!/bin/bash
echo $(basename $0)
$ ./filename.sh
filename.sh
$ bash filename.sh 
filename.sh
$ bash /root/filename.sh 
filename.sh
$ 

測試參數

在腳本中使用參數要確保參數存在,否則運行時有可能會報錯:

$ cat add.sh 
#!/bin/bash
echo $1 + $2 = $[ $1 + $2 ]
$ ./add.sh 1 2
1 + 2 = 3
$ ./add.sh 1
./add.sh:行2: 1 +  : 語法錯誤: 期待操作數 (錯誤符號是 "+  ")
$ 

如果只是當做字符串引用,也不會報錯。沒有傳參的參數默認都是空:

$ cat hello.sh 
#!/bin/bash
echo Hello $1 $2.
$ ./hello.sh Tom Jerry
Hello Tom Jerry.
$ ./hello.sh Jerry
Hello Jerry .
$ ./hello.sh
Hello .
$ 

判斷參數是否存在
在 shell 中利用 -n 來判定字符串非空,-z 則正好相反,空即是真。上面已經測試過了,未定義的參數默認是空:

$ cat hello.sh 
#!/bin/bash
if [ -n "$1" ]
then
    echo Hello $1.
else
    echo Hello Nobody.
fi
$ ./hello.sh Tom
Hello Tom.
$ ./hello.sh
Hello Nobody.
$ 

這裏的判斷的 \$1 要加上雙引號,否則會被認爲是字符串。一個字符串當然非空,所以結果會永遠爲真。

判斷參數的個數
上面的例子的腳本也可以通過判斷參數數量是否大於0來實現:

$ cat hello.sh 
#!/bin/bash
echo 參數數量: $#
if [ $# -gt 0 ]
then
    echo Hello $1.
else
    echo Hello Nobody.
fi
$ ./hello.sh
參數數量: 0
Hello Nobody.
$ ./hello.sh Tom
參數數量: 1
Hello Tom.
$ ./hello.sh Tom Jerry
參數數量: 2
Hello Tom.
$ 

這裏 -gt 比較的是前後兩個數字(INT),所以\$#是不加引號的。
用這種方法也能判斷參數是否存在。兩種方法,效果一樣,不同的書上都看到有人使用。

這裏是一樣加法的例子,必須要傳入2個參數:

$ cat add.sh 
#!/bin/bash
if [ $# -eq 2 ]
then
    echo $1 + $2 = $[ $1 + $2 ]
else
    echo 需要參數: 2, 實際參數: $#.
fi
$ ./add.sh 1 2
1 + 2 = 3
$ ./add.sh 1 2 3
需要參數: 2, 實際參數: 3.
$ ./add.sh 1
需要參數: 2, 實際參數: 1.
$ 

如果要表示不相等,就是 if [ $# -ne 2 ]

獲取最後一個參數
這是一個使用 \$# 的小技巧。使用${$#}似乎就是參數的最後一個變量了。
但是其實不然,花括號裏不能這樣用\$,這裏要把裏面的換成感嘆號:

$ cat hello.sh 
#!/bin/bash
if [ $# -gt 0 ]
then
    echo Hello ${!#}.
else
    echo Hello Nobody.
fi
$ ./hello.sh Tom Jerry
Hello Jerry.
$ 

如果沒有任何命令行參數,那麼就是返回\$0,也就是腳本名。

上面感嘆號的問題,效果是引用變量的值而不是變量自身。類似於指針的取值。把#號換成一個有名字的變量來說明比較直觀:

$ cat parameter.sh 
#!/bin/bash
paramater=key
key=value
echo "${paramater}"
echo "${!paramater}"
echo "${key}"
$ ./parameter.sh 
key
value
value
$ 

不加感嘆號,就是直接去該變量的值。加上感嘆號,就是去變量值所對應的變量名的那個變量的值。

獲取所有參數

\$* 和 \$@ 都是表示所有的字符串,但是在遍歷的時候會有區別:

$ cat all.sh 
#!/bin/bash
echo '$* 的效果:'
count=1
for i in "$*"
do
    echo $count: $i
    count=$[ $count + 1 ]
done
echo '$@ 的效果:'
count=1
for i in "$@"
do
    echo $count: $i
    count=$[ $count + 1 ]
done
$ ./all.sh Oliver Barry Kara Sara Kane
$* 的效果:
1: Oliver Barry Kara Sara Kane
$@ 的效果:
1: Oliver
2: Barry
3: Kara
4: Sara
5: Kane
$ 

\$*就一個整體的值,無法遍歷。要遍歷每一個變量要使用\$@。這裏的雙引號很重要。

不加引號的話,就是把 \$* 和 \$@ 的內容(變量解析後就是多個詞)傳遞給for循環遍歷,這樣兩個參數的效果是一樣的。和直接傳不加引號的字符串的效果一樣。
加上引號,引號裏的內容就是一個整體。如果是\$*,這個整體裏的所有內容還是一個詞,不會拆。如果是\$@,這個整體裏會按空格拆分成多個詞。
下面是演示的效果:

$ cat all2.sh 
#!/bin/bash
echo '$* 不加引號的效果:'
count=1
for i in $*
do
    echo $count: $i
    count=$[ $count + 1 ]
done
echo '$@ 不加引號的效果:'
count=1
for i in $@
do
    echo $count: $i
    count=$[ $count + 1 ]
done
echo '直接遍歷不加引號的字符的效果:'
count=1
for i in Oliver Barry Kara Sara Kane
do
    echo $count: $i
    count=$[ $count + 1 ]
done
echo '加引號遍歷的效果:'
count=1
for i in "Oliver Barry Kara Sara Kane"
do
    echo $count: $i
    count=$[ $count + 1 ]
done
$ ./all2.sh Oliver Barry Kara Sara Kane
$* 不加引號的效果:
1: Oliver
2: Barry
3: Kara
4: Sara
5: Kane
$@ 不加引號的效果:
1: Oliver
2: Barry
3: Kara
4: Sara
5: Kane
直接遍歷不加引號的字符的效果:
1: Oliver
2: Barry
3: Kara
4: Sara
5: Kane
加引號遍歷的效果:
1: Oliver Barry Kara Sara Kane
$ 

強調:特殊參數\$@一定要用在雙引號內,效果是每個參數都擴展爲分隔的單詞。在使用for循環遍歷的時候會體現出效果。

移動變量 shift

shift 命令能夠用來操作命令行參數。默認情況下將每個參數向左移動一個位置。被移出的參數就被丟棄了,無法恢復。
先掌握這個命令的使用,使用這個命令可以方便地解析命令行參數。

使用示例

下面是一個簡單的示例:

$ cat pop.sh 
#!/bin/bash
count=1
while [ -n "$1" ]
# while [ $# -ne 0 ]
do
    echo "$count: $1"
    count=$[ $count + 1 ]
    shift
done
$ ./pop.sh Oliver Barry Kara Sara Kane
1: Oliver
2: Barry
3: Kara
4: Sara
5: Kane
$ 

這裏有2中判斷方法來判斷是否還有參數,效果是一樣的。

移動多個位置

帶參數執行shift,指明要移動幾個位置就可以了:

$ cat pop.sh 
#!/bin/bash
count=1
# while [ -n "$1" ]
while [ $# -ne 0 ]
do
    if [ -n "$2" ]
    then
        echo "$count: $1, $2"
        shift 2
    else
        echo "$count: $1"
        shift
    fi
    count=$[ $count + 1 ]
done
$ ./pop.sh Oliver Barry Kara Sara Kane
1: Oliver, Barry
2: Kara, Sara
3: Kane
$ 

簡單修改下上面的腳本,一次輸出2個參數,然後移動2個位置。

處理選項

當shell腳本需要多個命令行參數時,在調用腳本的時候就必須將所有參數按固定的順序。
或者還可以使用選項來指定參數的值。

case 配合 shift

這個例子裏有帶值的選項也有不帶值的選項:

$ cat format.sh 
#!/bin/bash
prefix=""    # 前綴
base="test"  # 默認字符串
suffix=""    # 後綴
upper=off    # 是否大寫
# 解析命令行參數
while [ -n "$1" ]
do
    case "$1" in
        -a) suffix="$2"
            shift ;;
        -b) prefix="$2"
            shift ;;
        -s) base="$2"
            shift ;;
        -u) upper=on ;;
         *) echo "$1 is not an option"
            exit 1 ;;  # 發現未知參數,直接退出
    esac
    shift
done
# 添加前綴和後綴
output="${prefix:+${prefix}_}${base}${suffix:+_${suffix}}"
# 判斷是否要全大寫輸出
if [ $upper = on ]
then
    output=${output^^}
fi
# 輸出結果
echo "$output"
$ ./format.sh -a after
test_after
$ ./format.sh -s hello -b befor
befor_hello
$ ./format.sh -s hello -u -a after -b befor
BEFOR_HELLO_AFTER
$ ./format.sh -s hello -u -a after -b befor -l
-l is not an option
$ 

case語句找到一個選項就處理一個選項。如果還需要在命令行提供其他參數,可以在通用情況的處理部分中處理。而這裏因爲不需要提供任何參數,凡是解析不正確的就報告錯誤並退出(exit 1)。

能解析參數的版本
這個版本匹配所有的參數進行格式化輸出:

$ cat format.sh 
#!/bin/bash
prefix=""    # 前綴
base="test"  # 默認字符串
suffix=""    # 後綴
upper=off    # 是否大寫
# 顯示聲明一下這是個數組變量,其實沒有必要
declare -a names  # 需要格式化輸出的所有原始字符串
# 解析命令行參數
while [ -n "$1" ]
do
    case "$1" in
        -a) suffix="$2"
            shift ;;
        -b) prefix="$2"
            shift ;;
        -s) base="$2"
            shift ;;
        -u) upper=on ;;
         *) names=("${names[@]}" "$1") ;;
    esac
    shift
done
names[0]=${names[0]:-$base}
for name in "${names[@]}"
do
    # 添加前綴和後綴
    output="${prefix:+${prefix}_}${name}${suffix:+_${suffix}}"
    # 判斷是否要全大寫輸出
    if [ $upper = on ]
    then
        output=${output^^}
    fi
    # 輸出結果
    echo "$output"
done
$ 
$ ./format.sh -a after -b befor -u value1 value2 value3
BEFOR_VALUE1_AFTER
BEFOR_VALUE2_AFTER
BEFOR_VALUE3_AFTER
$ ./format.sh -a after after1 -b befor befor1 -u value1 value2 value3
BEFOR_AFTER1_AFTER
BEFOR_BEFOR1_AFTER
BEFOR_VALUE1_AFTER
BEFOR_VALUE2_AFTER
BEFOR_VALUE3_AFTER
$ ./format.sh -a after after1 -b befor befor1 -u -v value1 value2 value3
BEFOR_AFTER1_AFTER
BEFOR_BEFOR1_AFTER
BEFOR_-V_AFTER
BEFOR_VALUE1_AFTER
BEFOR_VALUE2_AFTER
BEFOR_VALUE3_AFTER
$ 

看最後的兩項的結果,提供的命令行參數有問題,但是程序無法發現。
倒數第二項可以認爲提供的參數是對的,但是選項和參數交替出現。
而最後一項提供了一個錯誤的選項,但是無法識別出來。
解決這個問題,需要更加規範的方法來分離參數和選項。下一小節的內容。

數組帶空格的問題
數組添加元素有很多方法,這裏是一種重新創建數組的做法:

array_name=("${array_name[@]}" value1 ... valueN)

可以一次添加多個元素,如果字符串包含空格,就要加上引號。

和命令行參數的\$@與\$*一樣,數組所有的元素也有這兩個類似的符號。最嚴謹的方法是使用 "${names[@]}" 使用帶雙引號的@。
添加元素和取出元素的時候都要注意,否則存在帶空格的元素的時候就會破壞數組原本的元素分隔。
添加元素這裏使用:

names=("${names[@]}" "$1")

不單是數組裏的元素,被添加的元素也要加上雙引號,否則如果有空格,就會按多個元素被添加進數組。

遍歷元素使用:

for name in "${names[@]}"

只有添加的時候正確了,才能正確的遍歷。然後遍歷的時候也要保證正確。
驗證效果:

$ ./format.sh -a after -b befor -u value1 "value2 value3" value4
BEFOR_VALUE1_AFTER
BEFOR_VALUE2 VALUE3_AFTER
BEFOR_VALUE4_AFTER
$ 

完美。

分離參數和選項

這裏的參數就是命令行參數中除了定義的選項之外,其他額外的參數。要同時處理參數和選項,就要用特殊字符(雙破折線--)將二者分開。雙破折線表明選項列表結束,雙破折線後面的都是參數。基於這個邏輯,只要在case語句中加一項判斷就行了。
把上面的腳本做一些修改:

$ cat format.sh 
#!/bin/bash
prefix=""    # 前綴
base="test"  # 默認字符串
suffix=""    # 後綴
upper=off    # 是否大寫
# 顯示聲明一下這是個數組變量,其實沒有必要
declare -a names  # 需要格式化輸出的所有原始字符串
# 解析選項
while [ -n "$1" ]
do
    case "$1" in
        -a) suffix="$2"
            shift ;;
        -b) prefix="$2"
            shift ;;
        -s) base="$2"
            shift ;;
        -u) upper=on ;;
        --) shift
            break ;;
         *) echo "$1 is not an option"
            exit 1 ;;  # 發現未知參數,直接退出
    esac
    shift
done
# 解析參數
while [ -n "$1" ]
do
    names=("${names[@]}" "$1")
    shift
done
names[0]=${names[0]:-$base}
for name in "${names[@]}"
do
    # 添加前綴和後綴
    output="${prefix:+${prefix}_}${name}${suffix:+_${suffix}}"
    # 判斷是否要全大寫輸出
    if [ $upper = on ]
    then
        output=${output^^}
    fi
    # 輸出結果
    echo "$output"
done
$ 

基於這個版本,在使用的時候,需要先輸入選項,然後使用雙破折線隔開,再輸入參數。當腳本遇到雙破折線時,它會停止處理選項,並將剩下的參數都當作參數:

$ ./format.sh -a after -b befor -u value1 value2 value3
value1 is not an option
$ ./format.sh -a after -b befor -u -- value1 value2 value3
BEFOR_VALUE1_AFTER
BEFOR_VALUE2_AFTER
BEFOR_VALUE3_AFTER
$ ./format.sh -a after -b befor -v -u -- value1 value2 value3
-v is not an option
$ 

第一次沒有使用雙破折線,所以報錯。
第二次正確的用雙破折號分隔了參數和選項。
第三次在選項部分出現了未定義的選項,也能發現錯誤。

小結
這一小節的內容也是爲下面的getopt命令做鋪墊。getopt就是可以幫我們完成命令行參數的解析,返回一個用雙破折線隔開選項和參數的規整的參數列表。
另外這裏還不支持選項合併:

$ ls -al

這些問題,用getopt都能解決,而且還支持長選項。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章