定義函數(shù)的兩種格式
在 bash 中,定義函數(shù)時,function 關(guān)鍵字是可選的,查看man bash手冊,里面提到定義函數(shù)的兩種格式如下:
name () compound-command [redirection]
function name [()] compound-command [redirection]
從中可以看到,當(dāng)不寫 function 關(guān)鍵字時,函數(shù)名后面一定要跟著小括號(),而寫了 function 關(guān)鍵字時,小括號是可選的。
關(guān)于 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 函數(shù),它自身的代碼是用小括號()括起來的 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
調(diào)用 bash shell 的函數(shù)時,不需要寫小括號(),只寫函數(shù)名即可。
例如執(zhí)行上面的 lsfunc() 函數(shù),直接寫 lsfunc 就可以。
如果寫成 lsfunc() 反而變成重新定義這個函數(shù)。
在函數(shù)名后面可以提供要傳入的參數(shù)列表,不同參數(shù)之間用空格隔開。
如果某個參數(shù)需要帶有空格,要用引號把該參數(shù)括起來。
從函數(shù)中返回內(nèi)容
Bash 要求函數(shù)的返回值必須為一個整數(shù),不能用 return 語句返回字符串變量。
一般來說,該整數(shù)返回值為 0,表示函數(shù)執(zhí)行成功,非0 表示執(zhí)行失敗。
在自定義的函數(shù)里面,執(zhí)行 return 語句會退出函數(shù),不會退出整個腳本。
在函數(shù)里面執(zhí)行 exit 語句則會退出整個腳本,而不是只退出函數(shù)。
由于在函數(shù)內(nèi)部用 return 返回,只能返回整數(shù)。
如果想從函數(shù)內(nèi)部把字符串傳遞到函數(shù)之外,可以用 echo 命令來實現(xiàn),就是在函數(shù)內(nèi)部打印字符串,然后調(diào)用者獲取標(biāo)準(zhǔn)輸出獲取到打印的字符串。
具體舉例如下:
$ function foo() { echo "foo"; return 0; }
$ var=$(foo)
$ echo ${var}, $?
foo, 0
可以看到,打印結(jié)果是 "foo, 0"。
此時看起來,這個函數(shù)像是返回了兩個值,一個是通過 $(foo) 獲取 foo 函數(shù)的標(biāo)準(zhǔn)輸出,另一個是 $? 會獲取函數(shù)通過 return 語句返回的 0。
如果在函數(shù)中寫為 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 函數(shù)將字符串寫到標(biāo)準(zhǔn)輸出,var=$(foo); 語句把 foo 函數(shù)的標(biāo)準(zhǔn)輸出賦值給 var 變量,打印 var 變量的值是 "foo"。
bar() 函數(shù)調(diào)用了 foo 函數(shù),但是沒有讀取 foo 函數(shù)打印的標(biāo)準(zhǔn)輸出,則這個標(biāo)準(zhǔn)輸出會被 bar 函數(shù)繼承,就好象這個標(biāo)準(zhǔn)輸出是由 bar 函數(shù)輸出一樣,var=$(bar); 語句也會把 var 變量賦值為 "foo"。
而 foobar 函數(shù)讀取了 foo 函數(shù)的標(biāo)準(zhǔn)輸出,foobar 函數(shù)自身沒有用 echo 命令來輸出內(nèi)容,此時再通過 $(foobar) 來獲取該函數(shù)的輸出,會獲取到空,因為 foo 函數(shù)中的標(biāo)準(zhǔn)輸出給 foobar 讀走了。
注意:這種在函數(shù)內(nèi)部通過 echo 命令輸出字符串的做法有個缺陷,就是不能再在函數(shù)里面執(zhí)行 echo 語句來打印調(diào)試信息,這些調(diào)試信息會被函數(shù)外的語句一起讀取到,有用的結(jié)果和調(diào)試信息都混在一起,如果函數(shù)外的語句沒有打印這些結(jié)果,就會看不到調(diào)試信息。
執(zhí)行某個函數(shù)后,可以使用 $? 表達式來獲取函數(shù)的 return 返回值,但是要注意下面的一種情況:
var=$(foo)
if [ "$?" == "0" ]; then
echo success
fi
此時,不要在 var=$(foo) 和 if [ "$?" == "0" ]; then 之間添加任何語句。
否則,$? 獲取到將不是 $(foo) 的 return 值,判斷就有問題,特別是不要添加 echo 調(diào)試語句。
換句話來說,這種先執(zhí)行一個語句,再判斷 $? 的方法不是很可靠,會受到各種影響,要特別注意代碼語句的順序。
使用函數(shù)名作為函數(shù)指針
在 bash 中,可以通過如下的方式來達到類似C語言函數(shù)指針的效果。
假設(shè)有一個 test.sh 腳本,內(nèi)容如下:
#!/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沒加雙引號, 這里加了.
# 實際上, 可加可不加, 都可以正確執(zhí)行.
echo_common="echo_b"
else
echo ERROR; exit 1
fi
${echo_common} common
這個腳本通過 echo_common 變量來保存函數(shù)名,相當(dāng)于是函數(shù)指針,再通過 ${echo_common} 來調(diào)用它保存的函數(shù)。
在 bash shell 中執(zhí)行 ./test.sh -a 命令,會輸出 "aaaa common";執(zhí)行 ./test.sh -b 命令,會輸出 "bbbb common"。
函數(shù)內(nèi)執(zhí)行cd命令的影響
在函數(shù)里面執(zhí)行 cd 命令,切換到某個目錄后,函數(shù)退出時,當(dāng)前工作目錄還是會保持在那個目錄,而不會自動恢復(fù)為原先的工作目錄,需要手動執(zhí)行 cd - 命令再切換回去。
假設(shè)有一個 testcd.sh 腳本,里面的內(nèi)容如下:
#!/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)"
這個函數(shù)先打印出執(zhí)行腳本時工作目錄路徑,然后執(zhí)行自定義的 cd_to_root 函數(shù),在函數(shù)內(nèi)部切換工作目錄到 "/usr/",最后在 cd_to_root 函數(shù)外面打印工作目錄路徑。
執(zhí)行 ./testcd.sh 腳本,會輸出下面的內(nèi)容:
[~/sample]$ ./testcd.sh
now, the pwd is: /home/sample
after execute the cd_to_root, pwd is: /usr
[~/sample]$ pwd
/home/sample
可以看到,如果在函數(shù)里面執(zhí)行過 cd 命令,函數(shù)退出后,當(dāng)前工作目錄還是 cd 后的目錄。
但是腳本執(zhí)行結(jié)束后,當(dāng)前 shell 的工作目錄還是之前的工作目錄,不是腳本里面 cd 后的目錄。
在每個 shell 下面,當(dāng)前工作目錄 (working directory ) 是全局的狀態(tài),一旦改變,在整個 shell 里面都會改變。
而 bash 執(zhí)行腳本時,是啟動一個新的子 shell 來執(zhí)行,所以腳本內(nèi)部執(zhí)行 cd 命令,會影響運行這個腳本的子 shell 的工作目錄,但不影響原先父 shell 的工作目錄。
聲明函數(shù)內(nèi)的變量為局部變量
在 bash 中,沒有使用 local 命令來聲明的變量都是全局變量,在函數(shù)內(nèi)部定義的變量也是全局變量。
如果沒有注意到這一點,在函數(shù)內(nèi)操作變量可能會影響到的外部同名變量,造成不預(yù)期的結(jié)果。
為了避免對外部同名變量造成影響,函數(shù)內(nèi)的變量最好聲明為局部變量,使用 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.
即,在函數(shù)內(nèi),使用 local 命令聲明的變量是局部變量,這些變量在函數(shù)外不可見。
在函數(shù)外,不能使用 local 命令聲明變量,否則會報錯。
注意:如上面說明,local 命令本身會返回一個值,正常的返回值是 0。
假設(shè)有個 testlocal.sh 腳本,內(nèi)容如下:
#!/bin/bash
foo() { return 1; }
bar() {
ret=$(foo); echo first: $?
local var=$(foo); echo second: $?
}
foobar() { return 0; }
bar
local out=$(foobar); echo third: $?
則執(zhí)行 ./testlocal.sh 腳本,會輸出:
first: 1
second: 0
./testlocal.sh: 第 x 行:local: 只能在函數(shù)中使用
third: 1
可以看到,ret=$(foo); 語句沒有使用 local 命令來定義 ret 變量,執(zhí)行 foo 函數(shù),該函數(shù)的返回值是 1,所以得到的 $? 是 1。
而 local var=$(foo); 語句使用 local 命令來定義 var 變量,執(zhí)行 foo 函數(shù),得到的 $? 卻是 0,而foo 函數(shù)明明返回的是 1。
原因就是該語句先通過 $(foo) 執(zhí)行 foo 函數(shù),然后用 local var 來定義 var 變量為局部變量,所以該語句對應(yīng)的 $? 是 local 命令的返回值,而不是 foo 函數(shù)的返回值。
當(dāng)在函數(shù)外執(zhí)行 local 命令時,它會報錯,可以看到雖然 foobar 函數(shù)返回是 0,但是第三個 echo 語句打印的 $?是 1,正好是 local 命令執(zhí)行出錯時的返回值。
即,對于 local var=$(func); 這種語句來說,它對應(yīng)的 $? 不是所調(diào)用函數(shù)的返回值,而是local 命令的返回值。
所以執(zhí)行函數(shù)后,想用 $? 來判斷函數(shù)的 return 返回值時,注意不要用這種寫法。
為了避免這種情況,最好在函數(shù)開頭就用 local 命令聲明所有局部變量。
使用 local 聲明多個變量時,變量之間用空格隔開,不要加逗號。
舉例說明如下:
local a b c;
獲取傳入函數(shù)的所有參數(shù)
在 bash 中,可以使用 $1、$2、$3、...、$n 的方式來引用傳入函數(shù)的參數(shù),$1 對應(yīng)第一個參數(shù)值,$2 對應(yīng)第二個參數(shù)值,依次類推。
如果 n 的值大于 9,那么需要用大括號{}把 n 括起來。
例如,${10} 表示獲取第 10 個參數(shù)的值,寫為 $10 獲取不到第 10 個參數(shù)的值。
實際上,$10 相當(dāng)于 ${1}0,也就是先獲取 $1 的值,后面再跟上 0,如果 $1 的值是 "first",則 $10 的值是 "first0"。
下面通過一個 testparams.sh 腳本來舉例說明,該腳本的內(nèi)容如下:
#!/bin/bash
function show_params()
{
echo $1 , $2 , $3 , $4 , $5 , $6 , $7 , $8 , $9 , $10 , ${10} , ${11}
}
show_params $@
這個腳本把傳入自身的參數(shù)傳給 show_params 函數(shù),該函數(shù)再打印出各個參數(shù),使用 $10、${10} 這兩個形式來說明它們的區(qū)別。
執(zhí)行 ./testparams.sh 腳本的結(jié)果如下:
$ ./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 個參數(shù)值是 "10j",而 $10 打印出來的結(jié)果是 "1a0",也就是第一個參數(shù) "1a" 后面再跟上 0。
${10} 打印的結(jié)果才是第 10 個參數(shù)的值。
相應(yīng)地,${11} 也能正確打印第 11 個參數(shù)的值。
$1、$2 這種寫法在 bash 文檔里面稱之為 positional parameter,中文直譯過來是 “位置參數(shù)”。
查看 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的數(shù)字括起來,{} 的作用是限定大括號里面的字符串是一個整體。
例如,有一個 var 變量值是 "Test",現(xiàn)在想打印這個變量值,并跟著打印 "Hello" 字符串,也就是打印出來 "TestHello" 字符串,那么獲取 var 變量值的語句和 "Hello" 字符串中間就不能有空格,否則 echo 命令會把這個空格一起打印出來,但是寫為 $varHello 達不到想要的效果。
具體舉例如下:
$ var="Test"
$ echo $var Hello
Test Hello
$ echo $varHello
$ echo ${var}Hello
TestHello
可以看到,$var Hello 這種寫法打印出來的 "Test" 和 "Hello" 中間有空格,不是想要的結(jié)果。
而 $varHello 打印為空,這其實是獲取 varHello 變量的值,這個變量沒有定義過,默認(rèn)值是空。
${var}Hello 打印出了想要的結(jié)果,用大括號 {} 把 var 括起來,明確指定要獲取的變量名是 var,避免混淆。
上面貼出的 testparams.sh 腳本代碼里面還用到了一個 $@ 特殊參數(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" ...
注意:$@ 和 "$@" 這兩種寫法得到的結(jié)果可能會有所不同。$@ 是擴展為 $1 $2 ...,而 "$@" 是擴展為 "$1" "$2" ...
修改上面的 testparams.sh 腳本來舉例說明 $@ 和 "$@" 的區(qū)別:
#!/bin/bash
function show_params()
{
echo $1 , $2 , $3
}
show_params $@
show_params "$@"
執(zhí)行 testparams.sh 腳本,輸出結(jié)果如下:
$ ./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 ,
可以看到,當(dāng)傳入腳本的參數(shù)值不帶空格時,$@ 和 "$@" 得到的結(jié)果相同。
當(dāng)傳入腳本的參數(shù)值自身帶有空格時,$@ 得到的參數(shù)個數(shù)會變多,"$@" 可以保持參數(shù)個數(shù)不變。
上面的 $1 是 "1 a",$@ 會拆分成 "1" "2" 兩個參數(shù),然后傳給 show_params 函數(shù);"$@" 會保持為 "1 a" 不變,然后傳給 show_params 函數(shù)。
即,"1 a" "2 b" "3 c" 這三個參數(shù),經(jīng)過 $@ 處理后,得到的是 "1" "a" "2" "b" "3" "c" 六個參數(shù)。
經(jīng)過 "$@" 處理后,得到的還是 "1 a" "2 b" "3 c" 三個參數(shù)。
同時也看到,即使只傳入兩個參數(shù),引用 $3 也不會報錯,只是會獲取為空。