Shell 函數
1. Shell 函數概述
1.1 Shell 函數簡介
Shell 和其他語言一樣,也有函數,其本質就是一段可以復用的代碼。將數據進行抽離處理,傳遞不同的值輸出不同的結果,在指定的地方進行調用即可。
1.2 為什么要用函數
如果我們要重復執行一批相同的操作,不想重復去寫,可以將這一系列操作抽象為一個函數,后期可以利用變量傳值調用該函數,從而大大減少重復性勞動,提升效率減少代碼量,使得 Shell 腳本更加的靈活和通用。
2. Shell 函數操作
2.1 函數語法
2.1.1 標準語法
function fnname() {
statements
return value
}
對各個部分的說明:
function
是 Shell 中的關鍵字,專門用來定義函數;fname
是函數名;statements
是函數要執行的代碼,也就是一組語句;return value
表示函數的返回值,其中 return 是 Shell 關鍵字,專門用在函數中返回一個值,這一部分可以寫也可以不寫。
由大括號包圍的部分稱為函數體,調用一個函數,實際上就是執行函數體中的代碼。
例如:
function checkuser() {
echo "當前用戶為:$USER"
return 0
}
如上就定義了一個 checkuser
函數,其輸出當前登錄系統的用戶名,返回值為 0。
2.1.2 簡化語法
- 函數名后無括號
#簡化寫法1
function fname{
statements
return n
}
- 函數不寫 function
#簡化寫法2:
fname() {
statements
return n
}
上述兩種定義函數的方法都可以,但是還是建議在編寫 Shell 的時候,也不要浪費這點時間,建議大家都用完整函數定義,寫 function 關鍵字,在為函數定義帶上 (),這樣更加規范,而且便于別人閱讀修改你的腳本。
Tips:在函數定義中需要函數名稱后可以有多個空格,括號內也可以有多個空格,如果函數體寫在一行,需要在語句末尾加上
;
。
2.2 函數調用
當函數定義好了,在需要使用函數的地方,調用其函數名稱即可,注意函數調用需要在函數定義之后。
2.2.1 無參數調用
當函數沒有參數的時候,調用非常簡單,直接寫函數名稱即可,調用函數就是在特定地方執行函數體內的操作。
// 定義函數
function fnname() {
statements
return value
}
// 調用函數
fname
2.2.2 傳遞參數調用
- 語法
我們之前在變量一章節介紹了 Shell 腳本的參數,知道了參數的重要性質及其各種類特征,與 Shell 腳本傳遞參數一樣,函數也可以傳遞參數,例如:
// 定義函數
function fnname() {
statements
return value
}
// 調用函數
fname param1 param2 param3
如上所示,在調用函數 fname
的時候,我們傳遞了三個參數,參數之間利用空格分割,和 Shell 腳本傳遞參數一樣,但需要注意 Shell 腳本中,函數在定義時候不能指定參數,在調用的時候傳遞參數即可,并且在調用函數時傳遞什么參數函數就接受什么參數。
2.3 函數參數
上述我們了解了函數的定義,在其中無參函數調用即調用函數名即可,對于有參函數,需要傳遞一定的參數來執行對應的操作,函數的參數和腳本的參數類型及用法一致,在此我們簡單回顧下,看參數在函數中都有哪些分類,及該如何使用。
2.3.1 位置參數
位置參數顧名思義,就是傳遞給函數參數的位置,例如給一個函數傳遞一個參數,我們可以在執行 Shell 腳本獲取對應位置的參數,獲取參數的格式為:$n。n 代表一個數字,在此需要注意與腳本傳遞參數不一樣,$0
為依舊為腳本的名稱,在函數參數傳遞中,例如傳遞給函數的第一個參數獲取就為 $1
,第 2 個參數就為 $2
, 以此類推……,需要其 $0
為該函數的名稱。
例如:
[root@master func]# cat f1.sh
#!/bin/bash
function f1() {
echo "函數的第一個參數為: ${1}"
echo "函數的第二個參數為: ${2}"
echo "函數的第三個參數為: ${3}"
}
# 調用函數
f1 shell linux python go
[root@master func]# bash f1.sh
函數的第一個參數為: shell
函數的第二個參數為: linux
函數的第三個參數為: python
我們可以看到傳遞給 f1
函數共 4 個位置參數,在結果輸出中可以看到由于函數體內部只對三個參數進行了處理,后續的參數也就不再處理了。
2.3.2 特殊參數
在 Shell 中也存在特殊含義的參數如下表:
變量 | 含義 |
---|---|
$# | 傳遞給函數的參數個數總和 |
$* | 傳遞給腳本或函數的所有參數,當被雙引號 " " 包含時,所有的位置參數被看做一個字符串 |
$@ | 傳遞給腳本或函數的所有參數,當被雙引號 " " 包含時,每個位置參數被看做獨立的字符串 |
$? | $? 表示函數的退出狀態,返回為 0 為執行成功,非 0 則為執行失敗 |
示例:
[root@master func]# cat f1.sh
#!/bin/bash
function fsum() {
echo "函數第一個參數為: ${1}"
echo "函數第二個參數為: ${2}"
echo "函數第三個參數為: ${3}"
echo "函數的參數總數為: ${#}"
echo "函數的參數總數為: ${@}"
local sum=0
for num in ${@};
do
let sum=${sum}+${num}
done
echo "計算的總和為: ${sum}"
return 0
}
# 調用函數
fsum 10 20 1 2
echo $?
[root@master func]# bash f1.sh
函數第一個參數為: 10
函數第二個參數為: 20
函數第三個參數為: 1
函數的參數總數為: 4
函數的參數總數為: 10 20 1 2
計算的總和為: 33
0
如上可以看到特殊參數與 Shell 腳本傳遞參數一樣。
Tips:局部變量需要特別聲明在函數內部利用
local
關鍵字來聲明。
2.4 函數返回值
函數返回值利用 $?
來接收,在上述示例中我們將計算的結果利用 echo 命令打印出來,如果我們在后續的腳本中需要利用此函數計算的結果,就需要得到這個返回值,此刻就需要將計算的結果不僅僅是打印而是返回了,函數中返回利用 return
關鍵字,在函數調用完成后,我們利用 $?
來接受函數的返回值,例如將我們上面的示例改造成返回結構的函數。
注意:shell 函數的返回值,只能是整形,并且在 0-257 之間,不能是字符串或其他形式。并且在調用方法和取得返回值之間,不能有任何操作,不然取不到 return 的值。
[root@master func]# cat f1.sh
#!/bin/bash
function fsum() {
echo "函數第一個參數為: ${1}"
echo "函數第二個參數為: ${2}"
echo "函數第三個參數為: ${3}"
echo "函數的參數總數為: ${#}"
echo "函數的參數總數為: ${@}"
local sum=0
for num in ${@};
do
let sum=${sum}+${num}
done
return $sum
}
fsum 10 20 1 2
echo $?
[root@master func]# bash f1.sh
函數第一個參數為: 10
函數第二個參數為: 20
函數第三個參數為: 1
函數的參數總數為: 4
函數的參數總數為: 10 20 1 2
33
可以看到我們將在函數內部計算的數組之和,利用 return 作為返回,此刻在函數調用的時候,利用 $?
就可以拿到函數返回的值進一步處理。
2.5 遞歸函數
Shell 支持遞歸函數,遞歸函數也就是自己調用自己,即在函數體內部又一次調用函數自己,例如:
[root@master func]# cat recursion.sh
#!/bin/bash
function myecho() {
echo "$(date)"
sleep 1
myecho inner
}
myecho
[root@master func]# bash recursion.sh
Sat Mar 28 13:14:38 CST 2020
Sat Mar 28 13:14:39 CST 2020
Sat Mar 28 13:14:40 CST 2020
Sat Mar 28 13:14:41 CST 2020
Sat Mar 28 13:14:42 CST 2020
...
如上就是一個遞歸函數,在函數體內部又調用了函數 myecho
,在執行的時候就會陷入無限循環。
3. 實例
3.1 需求
系統經常在執行定時腳本期間會將 Linux 系統 CPU 利用率跑滿,導致其他服務受到影響,故查閱資料發現有大神寫的 CPU 利用率限制程序。
地址:CPU Usage Limiter for Linux
利用此工具可以,配合定時任務放置在服務器上,達到限制程序 CPU 情況,可根據自己系統 CPU 核心數進行參數配置,會記錄 CPU 超過閥值的日志,可供后期進行查看分析。
3.2 思路
利用函數編寫安裝 cpulimit 工具的函數,如果系統不存在該命令則安裝并執行 CPU 限制,如果存在則執行 cpulimit 函數對超過指定 CPU 的進程進行限制,最后執行總體 main 函數。
3.3 實現
- 實現腳本
#!/bin/bash
# Description: count file scripts
# Auth: kaliarch
# Email: [email protected]
# function: count file
# Date: 2020-03-29 14:00
# Version: 1.0
[ $(id -u) -gt 0 ] && exit 1
# cpu使用超過百分之多少進行限制
PEC_CPU=80
# 限制進程使用百分之多少,如果程序為多線程,單個cpu限制為85,如果為多核心,就需要按照比例寫,例如cpu為2c,像限制多線程占比80%,就寫170
LIMIT_CPU=85
# 日志
LOG_DIR=/var/log/cpulimit/
# 超過閥值進程pid
PIDARG=$(ps -aux |awk -v CPU=${PEC_CPU} '{if($3 > CPU) print $2}')
# 安裝cpulimit 函數
install_cpulimit() {
[ ! -d /tmp ] && mkdir /tmp || cd /tmp
wget -c https://github.com/opsengine/cpulimit/archive/v0.2.tar.gz
tar -zxf v0.2.tar.gz
cd cpulimit-0.2 && make
[ $? -eq 0 ] && cp src/cpulimit /usr/bin/
}
# 執行cpulimit
do_cpulimit() {
[ ! -d ${LOG_DIR} ] && mkdir -p ${LOG_DIR}
for i in ${PIDARG};
do
CPULIMITCMD=$(which cpulimit)
MSG=$(ps -aux |awk -v pid=$i '{if($2 == pid) print $0}')
echo ${MSG}
[ ! -d /tmp ] && mkdir /tmp || cd /tmp
nohup ${CPULIMITCMD} -p $i -l ${LIMIT_CPU} &
echo "$(date) -- ${MSG}" >> ${LOG_DIR}$(date +%F).log
done
}
# 主函數
main() {
hash cpulimit
if [ $? -eq 0 ];then
# 調用函數
do_cpulimit
else
install_cpulimit && do_cpulimit
fi
}
main
- 測試
需編寫 CPU 壓力測試 python 腳本,后期可以放入計劃任務來監控并限制 CPU 使用率。
#!/bin/env python
import math
import random
a=10000
b=10000
c=10000
sum=0
for i in range(0,a):
for j in range(0,b):
randomfloat=random.uniform(1,10)
randompow=random.uniform(1,10)
sum+=math.pow(randomfloat, randompow)
print "sum is %s" % sum
當我們執行 python 的 CPU 壓力測試腳本后,發現單核服務 CPU 跑到了 100%。

之后運行我們的 CPU 限制腳本,可以看到 CPU 被成功限制。

在此案例中,我們著重來看 Shell 函數,在實例中我們編寫了安裝與執行 CPU 限制兩個函數,最后編寫主函數,在主函數中調用其他函數,配合定時任務就能達到限制某進程的 CPU。
4. 注意事項
- 函數定義建議使用見名知意并需要有一定原則,不僅為了美觀更是為了規范,使得其他人更好理解與閱讀你的 Shell 腳本,增強腳本可維護性;
- 對于函數調用,必須在函數定義之后,Shell 運行為順序運行,沒有定義函數則調用函數會有異常;
- 函數定義不能指定參數,在調用的時候傳遞參數即可,并且調用函數傳遞什么參數,函數就接受什么參數;
- 函數傳遞參數與 Shell 腳本傳遞參數的類型及用法一致,在此可以融會貫通,對比理解記憶;
- shell 函數的返回值,只能是整形,并且在 0-257 之間,不能為字符串或其他形式。
5. 小結
我們編寫 Shell 就是為了實現簡化操作,通常將數據和程序分離,我們將一組實現具體業務的邏輯編寫為一個函數,也可以稱為一段代碼塊,將其模塊化,賦予函數名稱,利用函數可以達到代碼復用,使得數據與邏輯分離,腳本更加便于維護和管理。