定義函數的兩種格式
在 bash 中,定義函數時,function 關鍵字是可選的,查看man bash手冊,里面提到定義函數的兩種格式如下:
name () compound-command [redirection]
function name [()] compound-command [redirection]
從中可以看到,當不寫 function 關鍵字時,函數名后面一定要跟著小括號(),而寫了 function 關鍵字時,小括號是可選的。
關于 compound-command 的說明,同樣可以查看 man bash 手冊,里面提到下面幾種形式:
A compound command is one of the following:
(list) list is executed in a subshell environment. Variable assignments and builtin commands that affect the shell's environment do not remain in effect after the command completes. The return status is the exit status of list.
{ list; } list is simply executed in the current shell environment. list must be terminated with a newline or semicolon. The return status is the exit status of list.
常見的是 { list; } 這種形式,但是寫為 (list) 也可以。
舉例說明如下:
$ testpwd() (pwd)
$ testpwd
/home/sample/
$ testpwd() ( pwd )
$ testpwd
/home/sample/
$ testpwd() (pwd;)
$ testpwd
/home/sample/
這里定義了一個 testpwd 函數,它自身的代碼是用小括號()括起來的 pwd 命令,這個命令跟 () 之間可以有空格,也可以沒有空格。
在命令后可以加分號,也可以不加分號。
注意:使用 { list; } 這個寫法時,在 list 后面一定要跟著分號';',否則會報錯。而且 list; 和左邊的大括號 { 之間要有空格。
如果寫為 {list;} 會報錯,而 { list;} 不會報錯,建議還是寫為 { list; } 的形式。
舉例說明如下:
$ lsfunc() {ls}
-bash: syntax error near unexpected token `{ls}'
$ function lsfunc() {ls;}
-bash: syntax error near unexpected token `{ls'
$ lsfunc() { ls;}
$ lsfunc
hello.c
調用 bash shell 的函數時,不需要寫小括號(),只寫函數名即可。
例如執行上面的 lsfunc() 函數,直接寫 lsfunc 就可以。
如果寫成 lsfunc() 反而變成重新定義這個函數。
在函數名后面可以提供要傳入的參數列表,不同參數之間用空格隔開。
如果某個參數需要帶有空格,要用引號把該參數括起來。
從函數中返回內容
Bash 要求函數的返回值必須為一個整數,不能用 return 語句返回字符串變量。
一般來說,該整數返回值為 0,表示函數執行成功,非0 表示執行失敗。
在自定義的函數里面,執行 return 語句會退出函數,不會退出整個腳本。
在函數里面執行 exit 語句則會退出整個腳本,而不是只退出函數。
由于在函數內部用 return 返回,只能返回整數。
如果想從函數內部把字符串傳遞到函數之外,可以用 echo 命令來實現,就是在函數內部打印字符串,然后調用者獲取標準輸出獲取到打印的字符串。
具體舉例如下:
$ function foo() { echo "foo"; return 0; }
$ var=$(foo)
$ echo ${var}, $?
foo, 0
可以看到,打印結果是 "foo, 0"。
此時看起來,這個函數像是返回了兩個值,一個是通過 $(foo) 獲取 foo 函數的標準輸出,另一個是 $? 會獲取函數通過 return 語句返回的 0。
如果在函數中寫為 return 1,那么上面的 $? 打印的出來的值是 1。
下面再舉例說明如下:
$ foo() { echo "foo"; }
$ bar() { foo; }
$ foobar() { a=$(foo); }
$ var=$(foo); echo first: ${var}
first: foo
$ var=$(bar); echo second: ${var}
second: foo
$ var=$(foobar); echo third: ${var}
third:
可以看到, foo 函數將字符串寫到標準輸出,var=$(foo); 語句把 foo 函數的標準輸出賦值給 var 變量,打印 var 變量的值是 "foo"。
bar() 函數調用了 foo 函數,但是沒有讀取 foo 函數打印的標準輸出,則這個標準輸出會被 bar 函數繼承,就好象這個標準輸出是由 bar 函數輸出一樣,var=$(bar); 語句也會把 var 變量賦值為 "foo"。
而 foobar 函數讀取了 foo 函數的標準輸出,foobar 函數自身沒有用 echo 命令來輸出內容,此時再通過 $(foobar) 來獲取該函數的輸出,會獲取到空,因為 foo 函數中的標準輸出給 foobar 讀走了。
注意:這種在函數內部通過 echo 命令輸出字符串的做法有個缺陷,就是不能再在函數里面執行 echo 語句來打印調試信息,這些調試信息會被函數外的語句一起讀取到,有用的結果和調試信息都混在一起,如果函數外的語句沒有打印這些結果,就會看不到調試信息。
執行某個函數后,可以使用 $? 表達式來獲取函數的 return 返回值,但是要注意下面的一種情況:
var=$(foo)
if [ "$?" == "0" ]; then
echo success
fi
此時,不要在 var=$(foo) 和 if [ "$?" == "0" ]; then 之間添加任何語句。
否則,$? 獲取到將不是 $(foo) 的 return 值,判斷就有問題,特別是不要添加 echo 調試語句。
換句話來說,這種先執行一個語句,再判斷 $? 的方法不是很可靠,會受到各種影響,要特別注意代碼語句的順序。
使用函數名作為函數指針
在 bash 中,可以通過如下的方式來達到類似C語言函數指針的效果。
假設有一個 test.sh 腳本,內容如下:
#!/bin/bash
# 注意$1后面有一個分號';', 少這個分號會報錯
echo_a() { echo aaaa $1; }
echo_b() { echo bbbb $1; }
if [ "$1" == "-a" ]; then
# 這里的 echo_a 沒有加雙引號
echo_common=echo_a
elif [ "$1" == "-b" ]; then
# 上面的echo_a沒加雙引號, 這里加了.
# 實際上, 可加可不加, 都可以正確執行.
echo_common="echo_b"
else
echo ERROR; exit 1
fi
${echo_common} common
這個腳本通過 echo_common 變量來保存函數名,相當于是函數指針,再通過 ${echo_common} 來調用它保存的函數。
在 bash shell 中執行 ./test.sh -a 命令,會輸出 "aaaa common";執行 ./test.sh -b 命令,會輸出 "bbbb common"。
函數內執行cd命令的影響
在函數里面執行 cd 命令,切換到某個目錄后,函數退出時,當前工作目錄還是會保持在那個目錄,而不會自動恢復為原先的工作目錄,需要手動執行 cd - 命令再切換回去。
假設有一個 testcd.sh 腳本,里面的內容如下:
#!/bin/bash
echo "now, the pwd is: $(pwd)"
cd_to_root() { cd /usr/; }
cd_to_root
echo "after execute the cd_to_root, pwd is: $(pwd)"
這個函數先打印出執行腳本時工作目錄路徑,然后執行自定義的 cd_to_root 函數,在函數內部切換工作目錄到 "/usr/",最后在 cd_to_root 函數外面打印工作目錄路徑。
執行 ./testcd.sh 腳本,會輸出下面的內容:
[~/sample]$ ./testcd.sh
now, the pwd is: /home/sample
after execute the cd_to_root, pwd is: /usr
[~/sample]$ pwd
/home/sample
可以看到,如果在函數里面執行過 cd 命令,函數退出后,當前工作目錄還是 cd 后的目錄。
但是腳本執行結束后,當前 shell 的工作目錄還是之前的工作目錄,不是腳本里面 cd 后的目錄。
在每個 shell 下面,當前工作目錄 (working directory ) 是全局的狀態,一旦改變,在整個 shell 里面都會改變。
而 bash 執行腳本時,是啟動一個新的子 shell 來執行,所以腳本內部執行 cd 命令,會影響運行這個腳本的子 shell 的工作目錄,但不影響原先父 shell 的工作目錄。
聲明函數內的變量為局部變量
在 bash 中,沒有使用 local 命令來聲明的變量都是全局變量,在函數內部定義的變量也是全局變量。
如果沒有注意到這一點,在函數內操作變量可能會影響到的外部同名變量,造成不預期的結果。
為了避免對外部同名變量造成影響,函數內的變量最好聲明為局部變量,使用 local 命令來聲明。
查看 man bash 里面對 local 命令的說明如下:
local [option] [name[=value] ...]
For each argument, a local variable named name is created, and assigned value. The option can be any of the options accepted by declare.
When local is used within a function, it causes the variable name to have a visible scope restricted to that function and its children. With no operands, local writes a list of local variables to the standard output.
It is an error to use local when not within a function.
The return status is 0 unless local is used outside a function, an invalid name is supplied, or name is a readonly variable.
即,在函數內,使用 local 命令聲明的變量是局部變量,這些變量在函數外不可見。
在函數外,不能使用 local 命令聲明變量,否則會報錯。
注意:如上面說明,local 命令本身會返回一個值,正常的返回值是 0。
假設有個 testlocal.sh 腳本,內容如下:
#!/bin/bash
foo() { return 1; }
bar() {
ret=$(foo); echo first: $?
local var=$(foo); echo second: $?
}
foobar() { return 0; }
bar
local out=$(foobar); echo third: $?
則執行 ./testlocal.sh 腳本,會輸出:
first: 1
second: 0
./testlocal.sh: 第 x 行:local: 只能在函數中使用
third: 1
可以看到,ret=$(foo); 語句沒有使用 local 命令來定義 ret 變量,執行 foo 函數,該函數的返回值是 1,所以得到的 $? 是 1。
而 local var=$(foo); 語句使用 local 命令來定義 var 變量,執行 foo 函數,得到的 $? 卻是 0,而foo 函數明明返回的是 1。
原因就是該語句先通過 $(foo) 執行 foo 函數,然后用 local var 來定義 var 變量為局部變量,所以該語句對應的 $? 是 local 命令的返回值,而不是 foo 函數的返回值。
當在函數外執行 local 命令時,它會報錯,可以看到雖然 foobar 函數返回是 0,但是第三個 echo 語句打印的 $?是 1,正好是 local 命令執行出錯時的返回值。
即,對于 local var=$(func); 這種語句來說,它對應的 $? 不是所調用函數的返回值,而是local 命令的返回值。
所以執行函數后,想用 $? 來判斷函數的 return 返回值時,注意不要用這種寫法。
為了避免這種情況,最好在函數開頭就用 local 命令聲明所有局部變量。
使用 local 聲明多個變量時,變量之間用空格隔開,不要加逗號。
舉例說明如下:
local a b c;
獲取傳入函數的所有參數
在 bash 中,可以使用 $1、$2、$3、...、$n 的方式來引用傳入函數的參數,$1 對應第一個參數值,$2 對應第二個參數值,依次類推。
如果 n 的值大于 9,那么需要用大括號{}把 n 括起來。
例如,${10} 表示獲取第 10 個參數的值,寫為 $10 獲取不到第 10 個參數的值。
實際上,$10 相當于 ${1}0,也就是先獲取 $1 的值,后面再跟上 0,如果 $1 的值是 "first",則 $10 的值是 "first0"。
下面通過一個 testparams.sh 腳本來舉例說明,該腳本的內容如下:
#!/bin/bash
function show_params()
{
echo $1 , $2 , $3 , $4 , $5 , $6 , $7 , $8 , $9 , $10 , ${10} , ${11}
}
show_params $@
這個腳本把傳入自身的參數傳給 show_params 函數,該函數再打印出各個參數,使用 $10、${10} 這兩個形式來說明它們的區別。
執行 ./testparams.sh 腳本的結果如下:
$ ./testparams.sh 1a 2b 3c 4d 5e 6f 7g 8h 9i 10j 11k
1a , 2b , 3c , 4d , 5e , 6f , 7g , 8h , 9i , 1a0 , 10j , 11k
可以看到,傳入的第 10 個參數值是 "10j",而 $10 打印出來的結果是 "1a0",也就是第一個參數 "1a" 后面再跟上 0。
${10} 打印的結果才是第 10 個參數的值。
相應地,${11} 也能正確打印第 11 個參數的值。
$1、$2 這種寫法在 bash 文檔里面稱之為 positional parameter,中文直譯過來是 “位置參數”。
查看 man bash 里面的說明如下:
Positional Parameters
A positional parameter is a parameter denoted by one or more digits, other than the single digit 0. Positional parameters are assigned from the shell's arguments when it is invoked, and may be reassigned using the set builtin command.
Positional parameters may not be assigned to with assignment statements. The positional parameters are temporarily replaced when a shell function is executed.
When a positional parameter consisting of more than a single digit is expanded, it must be enclosed in braces.
${parameter}
The value of parameter is substituted.
The braces are required when parameter is a positional parameter with more than one digit, or when parameter is followed by a character which is not to be interpreted as part of its name.
The parameter is a shell parameter or an array reference (Arrays).
可以看到,這里面提到了需要用大括號{}把大于9的數字括起來,{} 的作用是限定大括號里面的字符串是一個整體。
例如,有一個 var 變量值是 "Test",現在想打印這個變量值,并跟著打印 "Hello" 字符串,也就是打印出來 "TestHello" 字符串,那么獲取 var 變量值的語句和 "Hello" 字符串中間就不能有空格,否則 echo 命令會把這個空格一起打印出來,但是寫為 $varHello 達不到想要的效果。
具體舉例如下:
$ var="Test"
$ echo $var Hello
Test Hello
$ echo $varHello
$ echo ${var}Hello
TestHello
可以看到,$var Hello 這種寫法打印出來的 "Test" 和 "Hello" 中間有空格,不是想要的結果。
而 $varHello 打印為空,這其實是獲取 varHello 變量的值,這個變量沒有定義過,默認值是空。
${var}Hello 打印出了想要的結果,用大括號 {} 把 var 括起來,明確指定要獲取的變量名是 var,避免混淆。
上面貼出的 testparams.sh 腳本代碼里面還用到了一個 $@ 特殊參數,它會擴展為 positional parameter 自身的列表。
查看 man bash 的說明如下:
Special Parameters
@ Expands to the positional parameters, starting from one. When the expansion occurs within double quotes, each parameter expands to a separate word.
That is, "$@" is equivalent to "$1" "$2" ...
注意:$@ 和 "$@" 這兩種寫法得到的結果可能會有所不同。$@ 是擴展為 $1 $2 ...,而 "$@" 是擴展為 "$1" "$2" ...
修改上面的 testparams.sh 腳本來舉例說明 $@ 和 "$@" 的區別:
#!/bin/bash
function show_params()
{
echo $1 , $2 , $3
}
show_params $@
show_params "$@"
執行 testparams.sh 腳本,輸出結果如下:
$ ./testparams.sh 1a 2b 3c
1a , 2b , 3c
1a , 2b , 3c
$ ./testparams.sh "1 a" "2 b" "3 c"
1 , a , 2
1 a , 2 b , 3 c
$ ./testparams.sh 1a 2b
1a , 2b ,
1a , 2b ,
可以看到,當傳入腳本的參數值不帶空格時,$@ 和 "$@" 得到的結果相同。
當傳入腳本的參數值自身帶有空格時,$@ 得到的參數個數會變多,"$@" 可以保持參數個數不變。
上面的 $1 是 "1 a",$@ 會拆分成 "1" "2" 兩個參數,然后傳給 show_params 函數;"$@" 會保持為 "1 a" 不變,然后傳給 show_params 函數。
即,"1 a" "2 b" "3 c" 這三個參數,經過 $@ 處理后,得到的是 "1" "a" "2" "b" "3" "c" 六個參數。
經過 "$@" 處理后,得到的還是 "1 a" "2 b" "3 c" 三個參數。
同時也看到,即使只傳入兩個參數,引用 $3 也不會報錯,只是會獲取為空。