Shell 函數的知識與實踐
文章目錄
- Shell 函數的知識與實踐
- Shell 函數介紹
- Shell 函數的語法
- Shell 函數的執行
- 1. 不帶參數的函數執行
- 2. 帶參數的函數執行
- Shell 函數的基礎實踐
- 示例 1:簡單的 hello 函數(驗證 “先定義后調用”)
- 示例 2:調用外部文件中的函數
- 示例 3:帶參數的函數(根據輸入輸出彩色文字)
- 示例 4:函數參數與腳本參數的關系
- 企業級 URL 檢測腳本
- 函數的遞歸調用(函數調用自身)
- 示例 1:遞歸求 1+2+...+n 的和
- 示例 2:遞歸求 n 的階乘(1*2*...*n)
- 示例 3:fork 炸彈(危險!僅作原理了解)
- 總結
Shell 函數介紹
在學習 Shell 函數之前,我們先回憶一下 Linux 中的 alias
(別名)功能。比如我們常用 ll
代替 ls -l --color=auto
,就是通過別名實現的:
# 直接執行詳細列表命令,顯示/home目錄內容(--color=auto自動為文件著色)
[bq@controller shell 14:15:42]$ ls -l --color=auto /home
總用量 0
drwx------. 5 bq bq 124 8月 23 14:15 bq# 創建別名:用ll代替ls -l --color=auto
[bq@shell ~]$ alias ll='ls -l --color=auto'# 用別名執行,效果和原命令完全一致
[bq@controller shell 14:25:23]$ ll /home/
總用量 0
drwx------. 5 bq bq 124 8月 23 14:15 bq
Shell 函數和別名類似,都能簡化操作,但功能更強大。簡單來說,函數就是把一段重復使用的代碼 “打包”,起一個名字。之后想使用這段代碼時,直接調用這個名字就行。如果需要修改這段代碼,只改 “打包” 好的那一份,所有調用的地方都會同步更新。我們也可以把函數存到單獨的文件里,需要時再加載使用。
使用 Shell 函數的好處:
- 減少重復代碼:一段代碼多次用,定義成函數后不用反復寫,提高開發效率。
- 增強可讀性:用有意義的函數名代替一堆命令,代碼更易懂、易維護。
- 實現模塊化:把功能拆分成函數,讓腳本更通用,方便移植到其他場景。
小貼士:Linux 系統中的近 2000 個命令,其實都可以理解為 Shell 的 “內置函數”,可見函數在 Shell 中的重要性。
Shell 函數的語法
Shell 函數有多種定義方式,核心都是 “函數名 + 代碼塊”,以下是常見格式:
標準寫法:
function 函數名 () {指令... # 函數要執行的代碼return n # 可選,返回一個狀態值(0-255,0表示成功)
}
簡化寫法 1:省略小括號 ()
function 函數名 { # 去掉了函數名后的(),其他和標準寫法一致指令...return n
}
簡化寫法 2:省略 function
關鍵字
函數名 () { # 去掉了function,保留()和代碼塊指令...return n
}
三種寫法功能完全一樣,實際使用中可以根據習慣選擇。
Shell 函數的執行
Shell 函數分為 “不帶參數” 和 “帶參數” 兩種,執行方式略有不同,下面詳細說明:
1. 不帶參數的函數執行
直接輸入函數名即可(注意:函數名后不要加小括號),格式:
函數名 # 直接調用函數
重要說明(必看):
- 調用函數時,不要加
function
關鍵字,也不要加小括號(比如定義了hello()
,調用時直接寫hello
)。 - 函數必須 “先定義,后調用”:如果在調用之后才定義函數,Shell 會提示 “命令未找到”。
- 執行優先級:Shell 執行程序的順序是「系統別名 → 函數 → 系統命令 → 可執行文件」。比如如果有一個別名
ll
、一個函數ll
和系統命令ll
,執行ll
時會先執行別名。 - 變量共享:函數和調用它的腳本會共用變量,但可以用
local
定義 “局部變量”(僅在函數內有效,函數結束后消失)。 return
和exit
的區別:return
是退出函數,返回一個狀態值給調用者;exit
是直接退出整個腳本,返回狀態值給當前 Shell。- 外部函數加載:如果函數存放在單獨的文件中,需要用
source 文件名
或. 文件名
加載后才能調用(source
和.
作用相同)。
2. 帶參數的函數執行
調用時在函數名后直接跟參數,格式:
函數名 參數1 參數2 # 參數之間用空格分隔
參數說明:
- 函數內部用 “位置參數” 接收參數:
$1
表示第 1 個參數,$2
表示第 2 個參數,$#
表示參數總數,$*
或$@
表示所有參數,$?
表示上一條命令的返回值。 - 臨時覆蓋父腳本參數:函數執行時,父腳本的參數會被函數參數臨時 “掩蓋”,函數執行完后,父腳本參數恢復正常。
$0
特殊:始終表示父腳本的文件名,不會被函數參數影響。
Shell 函數的基礎實踐
示例 1:簡單的 hello 函數(驗證 “先定義后調用”)
實驗流程:
- 編寫腳本,先定義
hello
函數(輸出 “Hello World !”),再調用函數,觀察執行結果。 - 編寫另一個腳本,先調用
hello
函數,再定義函數,觀察錯誤結果。
詳細步驟:
# 腳本1:先定義函數,再調用
[bq@shell ~]$ cat fun1.sh
#!/bin/bash
# 定義hello函數:輸出Hello World !
function hello () {echo "Hello World !"
}
# 調用hello函數(直接寫函數名)
hello# 執行腳本,成功輸出結果
[bq@shell ~]$ bash fun1.sh
Hello World !# 腳本2:先調用函數,再定義(錯誤示范)
[bq@shell ~]$ cat fun2.sh
#!/bin/bash
# 先調用hello函數(此時函數還未定義)
hello
# 后定義hello函數
function hello () {echo "Hello World !"
}# 執行腳本,提示“hello: 命令未找到”(因為調用時函數還不存在)
[bq@shell ~]$ bash fun2.sh
fun2.sh: line 2: hello: command not found
結論:函數必須先定義,后調用,否則會報錯。
示例 2:調用外部文件中的函數
實驗流程:
- 創建一個存放函數的文件
mylib
(定義hello
函數)。 - 編寫腳本
fun3.sh
,通過source
加載mylib
中的函數,然后調用。 - 執行腳本,驗證能否成功調用外部函數。
詳細步驟:
# 1. 創建存放函數的文件mylib
[bq@shell ~]$ cat >> mylib << 'EOF' # 用here document寫入內容,'EOF'表示內容中的變量不解析
function hello () {echo "Hello World !" # 函數功能:輸出Hello World !
}
EOF# 2. 編寫調用腳本fun3.sh
[bq@shell ~]$ cat fun3.sh
#!/bin/bash
# 檢查mylib文件是否存在且可讀(-r選項:判斷文件存在且可讀)
if [ -r mylib ];thensource mylib # 加載mylib文件中的函數(source等同于. mylib)
elseecho "mylib文件不存在或不可讀" # 如果文件不存在,提示錯誤exit 1 # 退出腳本,返回狀態碼1(表示執行失敗)
fi
hello # 調用加載的hello函數# 3. 執行腳本,成功調用外部函數
[bq@shell ~]$ bash fun3.sh
Hello World !
結論:通過 source
可以加載外部文件中的函數,實現代碼復用。
示例 3:帶參數的函數(根據輸入輸出彩色文字)
實驗流程:
- 定義
print
函數,接收參數PASS
/FAIL
/DONE
,分別輸出綠色、紅色、紫色文字;其他參數提示用法。 - 腳本中通過
read
命令獲取用戶輸入,傳給print
函數。 - 測試不同輸入(
PASS
/FAIL
/DONE
/ 其他文字),觀察輸出結果。
詳細步驟:
# 編寫腳本fun4.sh
[bq@shell ~]$ cat fun4.sh
#!/bin/bash
# 定義print函數:根據參數輸出彩色文字
function print () {# 判斷參數是否為PASS:輸出綠色文字(\033[1;32m是綠色高亮,\033[0;39m恢復默認顏色)if [ "$1" == "PASS" ];thenecho -e '\033[1;32mPASS\033[0;39m' # -e選項:解析轉義字符(如顏色控制符)# 判斷參數是否為FAIL:輸出紅色文字elif [ "$1" == "FAIL" ];thenecho -e '\033[1;31mFAIL\033[0;39m'# 判斷參數是否為DONE:輸出紫色文字elif [ "$1" == "DONE" ];thenecho -e '\033[1;35mDONE\033[0;39m'# 其他參數:提示用法elseecho "Usage: print PASS|FAIL|DONE"fi
}
# 交互式讀取用戶輸入,-p選項:顯示提示文字
read -p "請輸入你想要打印的內容:" str
# 調用print函數,傳入用戶輸入的參數
print $str# 測試1:輸入PASS(輸出綠色PASS)
[bq@shell ~]$ bash fun4.sh
請輸入你想要打印的內容:PASS
PASS # 實際顯示為綠色高亮# 測試2:輸入FAIL(輸出紅色FAIL)
[bq@shell ~]$ bash fun4.sh
請輸入你想要打印的內容:FAIL
FAIL # 實際顯示為紅色高亮# 測試3:輸入DONE(輸出紫色DONE)
[bq@shell ~]$ bash fun4.sh
請輸入你想要打印的內容:DONE
DONE # 實際顯示為紫色高亮# 測試4:輸入其他文字(提示用法)
[bq@shell ~]$ bash fun4.sh
請輸入你想要打印的內容:hello
Usage: print PASS|FAIL|DONE
補充說明:
read -p "提示信息" 變量名
:用于交互式獲取用戶輸入,-p
顯示提示文字。- 顏色控制符:
\033[1;32m
中,1
表示高亮,32
表示綠色(31 = 紅,35 = 紫),\033[0;39m
用于恢復默認顏色,避免后續文字也變色。
示例 4:函數參數與腳本參數的關系
實驗流程:
- 編寫腳本
fun5.sh
,定義print
函數(邏輯同示例 3),腳本中通過$2
獲取第二個參數傳給函數。 - 測試腳本傳入不同參數,觀察函數是否使用腳本的參數,以及
$0
的值(腳本名還是函數名)。
詳細步驟:
# 編寫腳本fun5.sh
[bq@shell ~]$ cat fun5.sh
#!/bin/bash
function print () {if [ "$1" == "PASS" ];thenecho -e '\033[1;32mPASS\033[0;39m'elif [ "$1" == "FAIL" ];thenecho -e '\033[1;31mFAIL\033[0;39m'elif [ "$1" == "DONE" ];thenecho -e '\033[1;35mDONE\033[0;39m'else# $0始終表示腳本名(而非函數名)echo "Usage: $0 PASS|FAIL|DONE"fi
}
str=$2 # 腳本的第二個參數賦值給變量str
print $str # 函數使用腳本的第二個參數# 測試1:腳本傳入兩個參數(PASS和FAIL)
[bq@shell ~]$ bash fun5.sh PASS FAIL
FAIL # 函數接收的是腳本的第二個參數(FAIL),輸出紅色FAIL# 測試2:腳本不傳入參數(觸發else分支)
[bq@shell ~]$ bash fun5.sh
Usage: fun5.sh PASS|FAIL|DONE # $0顯示為腳本名fun5.sh,而非函數名print
結論:
- 函數參數需要顯式從腳本參數中傳遞(如腳本的
$2
傳給函數的$1
)。 $0
始終表示當前腳本的文件名,和函數名無關。
企業級 URL 檢測腳本
實驗流程:
- 定義
usage
函數:提示腳本用法(如參數錯誤時調用)。 - 定義
check_url
函數:用wget
檢測 URL 是否可訪問。 - 定義
main
函數:檢查參數數量,調用check_url
函數。 - 執行腳本,測試有效 URL(如百度)和無效 URL,觀察結果。
詳細步驟:
# 編寫檢測腳本check_url.sh
[bq@shell ~]$ cat check_url.sh
#!/bin/bash
# 用法提示函數:當參數錯誤時調用
function usage () {echo "usage: $0 url" # 提示正確用法:腳本名 + URLexit 1 # 退出腳本,狀態碼1表示錯誤
}# URL檢測函數:接收URL參數,判斷是否可訪問
function check_url () {# wget選項說明:# --spider:模擬爬蟲(只檢查URL是否存在,不下載內容)# -q:安靜模式(不輸出日志)# -o /dev/null:將日志輸出到“黑洞”(徹底不顯示)# --tries=1:嘗試1次(失敗不重試)# -T 5:超時時間5秒wget --spider -q -o /dev/null --tries=1 -T 5 $1# 判斷上一條命令的返回值($?):0表示成功,非0表示失敗[ $? -eq 0 ] && echo "$1 is accessable" || echo "$1 is not accessable"
}# 主函數:處理參數,調用檢測函數
function main () {[ $# -ne 1 ] && usage # 如果參數數量不是1,調用usage函數提示用法check_url $1 # 調用檢測函數,傳入URL參數
}# 執行主函數,$*表示所有參數(傳給main函數)
main $*# 測試1:檢測有效URL(百度)
[bq@shell ~]$ bash check_url.sh www.baidu.com
www.baidu.com is accessable # 可訪問# 測試2:檢測無效URL(不存在的域名)
[bq@shell ~]$ bash check_url.sh www.bq.com
www.bq.com is not accessable # 不可訪問# 測試3:參數錯誤(不傳參數)
[bq@shell ~]$ bash check_url.sh
usage: check_url.sh url # 調用usage函數提示用法
實戰價值:可批量檢測網站可用性,結合定時任務(crontab
)實現自動監控。
函數的遞歸調用(函數調用自身)
遞歸是指函數自己調用自己,適用于有明確終止條件的問題(如求和、階乘)。
示例 1:遞歸求 1+2+…+n 的和
實驗流程:
- 定義
sum_out
函數:如果n=1
,返回 1;否則返回n + sum_out(n-1)
(自身調用)。 - 腳本接收用戶輸入的整數
n
,調用函數計算和并輸出。 - 測試輸入
10
(預期結果 55),驗證正確性。
詳細步驟:
# 編寫求和腳本sum.sh
[bq@shell ~]$ cat sum.sh
#!/bin/bash
# 遞歸求和函數:計算1+2+...+$1的和
function sum_out() {# 終止條件:當參數為1時,和為1if [ $1 -eq 1 ];thensum=1else# 遞歸調用:n的和 = n + (n-1)的和(通過$(sum_out $[ $1 - 1 ])獲取n-1的和)sum=$[ $1 + $(sum_out $[ $1 - 1 ]) ]fiecho $sum # 輸出計算結果
}
# 讀取用戶輸入的整數
read -p "輸入一個你想計算和的整數:" num
# 調用遞歸函數,傳入用戶輸入的數字
sum_out $num# 測試:輸入10(1+2+...+10=55)
[bq@shell ~]$ bash sum.sh
輸入一個你想計算和的整數:10
55 # 結果正確
示例 2:遞歸求 n 的階乘(12…*n)
實驗流程:
- 定義
fact_out
函數:如果n=1
,返回 1;否則返回n * fact_out(n-1)
。 - 腳本接收用戶輸入的整數
n
,調用函數計算階乘并輸出。 - 測試輸入
10
(預期結果 3628800),驗證正確性。
詳細步驟:
# 編寫階乘腳本fact.sh
[bq@shell ~]$ cat fact.sh
#!/bin/bash
# 遞歸階乘函數:計算1*2*...*$1的積
function fact_out() {# 終止條件:當參數為1時,階乘為1if [ $1 -eq 1 ];thensum=1else# 遞歸調用:n的階乘 = n * (n-1)的階乘sum=$[ $1 * $(fact_out $[ $1 - 1 ]) ]fiecho $sum # 輸出計算結果
}
# 讀取用戶輸入的整數
read -p "輸入一個你想計算階乘的整數:" num
# 調用遞歸函數,傳入用戶輸入的數字
fact_out $num# 測試:輸入10(10! = 3628800)
[bq@shell ~]$ bash fact.sh
輸入一個你想計算階乘的整數:10
3628800 # 結果正確
示例 3:fork 炸彈(危險!僅作原理了解)
fork 炸彈是一種通過遞歸創建大量進程耗盡系統資源的惡意代碼,原理是函數不斷自我復制并后臺運行。
代碼解析:
:(){ :|:& };: # fork炸彈核心代碼
逐部分解釋:
-
:()
:定義一個函數,函數名是:
(冒號)。 -
{ :|:& }
:函數體內容:
:
:調用函數自身(遞歸)。|
:管道符,將左邊的輸出作為右邊的輸入,同時觸發兩次函數調用。&
:將進程放入后臺運行,允許同時創建更多子進程。
-
;
:結束函數定義。 -
:
:調用函數,觸發 “爆炸”。
危害:函數會無限制創建子進程,很快耗盡 CPU、內存等資源,導致系統卡死。
防御措施:限制用戶可創建的最大進程數(臨時生效):
# 限制當前用戶最多創建100個進程(ulimit -u 限制用戶最大進程數)
[bq@shell ~]$ ulimit -u 100
警告:切勿在生產環境中執行 fork 炸彈代碼!
總結
Shell 函數通過 “封裝重復代碼” 提升了腳本的簡潔性和可維護性,掌握函數的定義、參數傳遞、遞歸調用等技巧,能大幅提高 Shell 腳本開發效率。實際使用中,建議將通用函數整理到單獨的文件中,通過 source
加載復用,形成自己的 “函數庫”。