萬字長文:帶你走進 shell 世界
王志遠,微醫前端技術部。愛好吉他、健身、桌遊,最最關鍵,資深大廠員工(kfc 外賣小哥),trust me,好奇心使生命有趣起來!
可視化 shell 調試?shell 函數庫?智能提示 shell 補全的 vscode 插件?這全都有,還不快到碗裏來
前置信息
當你遇到了服務器內存佔用過多導致卡死的情況,你會怎麼辦?度娘 or 谷歌,你得到了這樣的答案
查看佔用 cpu / 內存 資源最多的進程並殺死
對啊,佔用多了就幹掉啊!
但怎麼幹?習慣圖形化界面的咱們找不到刪除圖標啊!!!沒關係,度娘 or 谷歌,你得到了這樣的答案(下面命令執行後會刪除佔用內存最多的兩個進程)
ps aux|sort -rn -k +3|head -2 | awk '{print $2}' | xargs kill -9
看懂是不可能看懂的,這輩子是不可能看懂的。
抱着【試試就試試,反正不會懷孕】的態度,你登陸了 shell 並且執行了,然後你驚喜的發現,成了!腰也不酸了腿也不疼了,比新蓋中蓋都好使。
至此,你的問題得以解決,好學的同學可能會深入學習一番(關於此命令的詳解咱們放在文末),然後隨着時間推移,短期記憶海馬體突觸消散而告終。此文結束。。。。。慢着 QAQ,問一句,下次再出現這個情況咋辦?再找一遍嗎?我們陷入了循環
顯然,我們需要破除循環。怎麼破除?想想我們熟悉的高級語言怎麼解決這個問題的,封裝呀,以 js 爲例,各種稀奇古怪的操作被封裝在一個又一個的庫裏,工具庫的 lodash、網絡請求的 axios 等等,函數名取的清晰易懂,取來即可。這纔是我們想要的效果。
可 linux 是命令啊,一個個命令用分號或者管道符鏈接執行,咋封裝?而且對於【批量執行命令】這個需求而言,用這兩種方法真的不會感覺很冗長嗎?這就迎來了我們的主角:shell 語言
讓我們來看看對於 shell 而言怎麼實現上面的需求:刪除佔用內存最多的兩個進程
-
創建 shell 文件,作爲函數庫,假定取名爲
shell-libs.sh
-
定義對應函數,假定取名爲
killProcessByMemory
,實現功能爲,傳遞進程數 n,殺死佔用內存排名前 n 位進程 -
引入函數庫文件:
source shell-libs.sh
shell-libs.sh
function arr_includes () {
local name=$1
eval local innerArr=(\${${name}[@]})
local item=$2
if [[ "${innerArr[@]}" =~ "${item}" ]]; then
echo 0
else
echo 1
fi
}
function boolean () {
local bool=1
if [[ $1 == 0 ]]; then
# source ../install/git/git.sh
bool=0
fi
return $bool
}
function killProcessByMemory() {
params=($*)
# 殺死進程的命令
killCmd="kill -9"
# 查詢進程號的命令
memoryInfoCmd='ps aux | sort -rn -k +3 | head -${1} | awk "{print $2}"'
# echo memoryInfo:$memoryInfo
needkill=`arr_includes params kill`
if boolean $needkill; then
echo "需要刪除"
handledCmd="${memoryInfoCmd} | ${killCmd}"
else
echo "只需要查詢"
handledCmd=$memoryInfoCmd
fi
echo '最終需要的命令爲'
echo $handledCmd
}
當做完這幾步初始動作後,我們要實現需求只需要執行下面命令(執行函數)即可
killProcessByMemory 2 # 只需要查詢進程號 最終需要的命令爲 ps aux | sort -rn -k +3 | head -${1} | awk "{print $2}"
killProcessByMemory 2 kill # 需要刪除 最終需要的命令爲 ps aux | sort -rn -k +3 | head -${1} | awk "{print $2}" | kill -9
函數庫很多,但最重要的是,我們的初始動作只需要做一遍,一勞永逸!!
其實不只是做一個常用命令的封裝,我們還可以做到
-
操作系統的初始化:git、nvm、node 等等等的安裝一鍵完成、常見軟件的自動安裝(win、mac 也可以!)
-
遠程服務器配置:遠程服務器免密登陸一鍵完成
-
爬蟲動作的封裝:定時任務不再是執行命令,而是一系列動作的合集
-
等等等
好了,好處顯而易見,我們現在對於 shell 有啥念想?兄弟姐妹們,學它!
整體定義
誘惑了這麼久,還是來給個定義吧:shell 是一個命令行解釋器,它爲用戶提供了一個向 Linux 內核發送請求以便運行程序的界面系統級程序;用戶可以用 Shell 來啓動、掛起、停止或者編寫一些程序;Shell 還是一個功能相當強大的編程語言,易編寫,易調試,靈活性較強,是解釋執行的腳本語言,在 Shell 中可以直接調用 Linux 系統命令。
亂入一下:案例解釋
對於上面【傳遞進程數 n,殺死佔用內存排名前 n 位進程】案例而言
ps aux|sort -rn -k +3|head -2 | awk '{print $2}' | xargs kill -9
ps 命令查找與進程相關的 PID 號:
-
a:顯示現行終端機下的所有程序,包括其他用戶的程序
-
u:以用戶爲主的格式來顯示程序狀況
-
x:顯示所有程序,不以終端機來區分
sort 對內容根據指定列進行排序
-
r:表示是結果倒序排列
-
n:以數值大小排序
-
-k +3:則是針對第 3 列的內容進行排序(第三列是 cpu,第四列是內存)
head 只顯示前指定行的數據,獲取默認前 10 行數據
awk 數據處理神器,這裏用於獲取第二列(PID,進程 id)
亂入結束,讓我們開始叭!
如何學習一門語言
語言大同小異,核心不過【變量、運算、語句、函數、框架】,shell 也是門語言,所以我們也會根據這個脈絡進行學習;
-
變量:定義、賦值、作用域
-
運算:算術運算符, 邏輯運算符
-
語句:測試,順序,分支,循環
-
函數,類,接口,包
-
框架:系統函數庫之上的封裝
-
特殊點:腳本控制、計劃任務、管道、重定向、可視化調試 shell 腳本(vscode 插件 Bash Debug)
但 shell 又有所不同,因爲它的出現強依賴【unix】,unix 的哲學:一條命令只做一件事情;爲了組合命令和多次執行,最先是分號; 用於同行組合,但不利於展示,於是出現了 shell 腳本文件,用來保存需要執行的命令,所以有別於其他語言又會出【環境變量、配置文件等等概念】,也強依賴 unix 的各個命令,這點是需要貫徹整個 shell 學習脈絡的。
注意:本文不會過多的牽扯 linux 的知識,awk、xarg、print 等等等,小夥伴放心,我們學習最核心的語言邏輯,至於工具,多用即可,放心食用
shell 腳本基礎概念
-
什麼是 shell
-
linux 的啓動過程
-
怎麼編寫一個 shell 腳本
-
shell 腳本的執行方式
-
內建命令和外部命令的區別
什麼是 shell
命令解釋器,用於解釋用戶對操作系統的操作,命令解釋器有很多種,具體可見:(默認的是 bash,其中的 a 是指 again,意思是彙總其他 shell 解釋器的優點,重寫實現)
cat /etc/shells
linux 的啓動過程
BIOS - MBR - BootLoader(grub) - kernel - systemd - 系統初始化 - shell
六步驟
1. BIOS 引導,BIOS 系統是基本的輸入輸出系統,功能嵌在主板上,用於選擇使用哪個介質(硬盤、光盤、網絡)
2. MBR(主引導記錄):用於確定硬盤是否可以被引導
3. BootLoader(grub):用於啓動和引導內核(種類和版本)
4. kernel:
5. systemd:1 號進程
shell 腳本的創建與執行
怎麼編寫一個 shell 腳本
unix 的哲學:一條命令只做一件事情;
爲了組合命令和多次執行,於是出現了 shell 腳本文件,用來保存需要執行的命令。
-
指定解釋器:
#!/bin/xxx
,默認是#!/bin/bash
;這其實是後綴,因爲在 Linux 中,文件後綴是沒有意義的,所以操作系統要知道這個腳本文件該用什麼應用來執行,就需要這個註釋來指明,比如 node 就是#!/usr/bin/node
-
爲文件賦予可執行權限,即
chmod u+x [filename]
案例:實現 sh 腳本文件,查看 / var 下所有子目錄空間
- 顯示當前目錄下的所有子目錄所佔空間:du -sh *
cat >> memo.sh << EOF
#!/bin/bash
cd /var
du -sh *
EOF
# 賦予權限 chmod u+x memo.sh
# 執行 ./memo.sh
怎麼執行一個 shell 腳本
存在四種執行方式
-
[解釋器] [文件名]:bash ./filename.sh,不需要賦予可執行權限,新創建進程,採用解釋器進行執行
-
[./ 文件名]:./filename.sh,新創建進程,需要賦予可執行權限,採用 #! 指定解釋器進行執行
-
source [./ 文件名]: source ./filename.sh,在當前進程進行執行
-
.[文件名]:在當前進程進行執行
shell 之變量
-
增刪改查:定義、賦值、引用 + 查詢、刪除
-
作用範圍
-
系統環境變量
-
環境變量配置文件
增刪改查:定義、賦值、引用、刪除、類型處理
定義
即變化的量,字母、數字、下劃線的組合,且不能以數字開頭(shell 變量是弱類型,不區分變量類型);
-
變量必須以字母或下劃線開頭,名字中間只能由字母,數字和下劃線組成
-
變量名的長度不得超過 255 個字符
-
變量名在有效範圍內必須唯一
-
變量默認類型都是字符串(存在類型【字符串、整型、浮點型、日期型】)
$ x=1 $ y=2 $ z=3 $ k=$x+$y+$z $ echo $k 1+2+3
賦值
非交互式賦值存在四種賦值方式
-
變量名 = 變量值(等號左右不能有空格): a=20
-
let 進行賦值:let a=20
-
將命令賦值給變量:l=ls
-
將命令的結果複製給變量(常用):使用或者:(ls -l /etc)
-
變量間的賦值可以設定默認值:
a=${[變量 b]-[默認值]}
,假設默認值是 *,a=${b-*}
注意:變量值如果有空格等特殊字符,可以包含在 ""或''中
引用 + 查詢
在 shell 中存在四種引用:單引號、雙引號、${}、``
-
雙引號:部分引用,使用這種引用時,$、`(反引號)、(轉義符) 這 3 個還是會解析成特殊的意義
-
單引號:完全引用,只原樣輸出
-
反引號 `:命令替換
變量名即對變量的引用,在部分情況下可以縮寫爲變量名(不產生歧義,比如字符串拼接就不能縮寫);
而查詢則可以使用set
命令,其會默認查詢系統中默認所有已經生效的變量,包括系統變量, 也包括自定義變量,結合管道運算符可以精準查詢
set | grep []
舉例:查詢wzy
變量的值
刪除
刪除則是unset
命令,使用方式如下
unset [變量名]
特殊數據結構之數組
-
定義:[變量名]=(a b c)
-
顯示所有元素:echo ${變量名 [@]}
-
顯示數組元素個數:echo ${# 變量名 [@]}
-
顯示數組第一個元素:echo ${變量名 [0]}(如果直接訪問時也會只顯示第一個元素)
colors=( yellow red blue )
echo ${colors[@]}
echo ${#colors[@]}
echo ${colors[0]}
echo $colors
類型處理
shell 變量爲弱類型並且默認是字符串類型,要想設置變量的類型,可以使用declare
命令。
declare 命令用來聲明變量類型
declare [+/-] [選項] 變量名
設定類型
整數型
舉例,將變量設定爲整數型從而實現加和,通過配置項-i
進行設定
數組類型
通過配置項-a
進行設定
定義
取第一個值
取第二個值
輸出所有
取消變量的類型屬性
承接上面,此時變量c
已經是整型了,我們希望實現取消變量c
的類型(即變回字符型),從而實現拼接的效果
設定爲只讀變量
通過配置項-r
進行設定
declare -r [變量名]
查詢變量
此時也可以通過配置項-p
查詢變量類型
declare -p [變量名] #顯示指定變量的被聲明的類型
設置爲環境變量
通過配置項-x
設置變量爲環境變量
declare -x [變量名]
由此也可以看到,之前定義全局環境變量的方式export [變量名]
其實就是declare -x [變量名]
的語法糖
變量的作用範圍
-
默認範圍:只針對當前的終端(shell)生效
-
支持子進程訪問父進程的變量:
export [變量名]=[變量值]
;(取消變量可以使用unset [變量名]
) -
系統環境變量:每個 shell 打開都能獲取到的變量
其中第一條可以使用bash
命令創建一個新的 shell 進行測試;第二條就是 export 和 unset 關鍵字;關鍵是系統環境變量,重點分析:
系統環境變量
環境變量主要根據兩個角度進行劃分:用戶級別是屬於系統還是屬於用戶;shell 級別是屬於當前的 shell 還是所有 shell。而這些是通過配置文件進行區分和記錄的,不同作用範圍和功能的變量分屬不同的配置文件中,主要有四個文件/etc/profile
、~/.bash_profile
、~/.bashrc
、/etc/bashrc
。系統環境變量的查詢:env 和 set
常見變量
環境變量配置文件
-
/etc/profile:
-
/etc/profile.d:目錄
-
~/.bash_profile
-
~/.bashrc
-
/etc/bashrc
需要注意的是
-
etc 下的是所有用戶通用,而~ 則是用戶用戶家目錄,只對指定用戶生效;
-
profile 文件和 bashrc 文件的加載取決於 shell 的種類是 loginShell 還是 noLoginShell(在登陸時採用的是
su - [用戶名]
,這時如果有這個 - 號就是 loginShell,不加則是 noLoginShell) -
noLoginShell 只會加載執行 bashrc
-
loginShell 則會全部加載執行
加載順序爲
/etc/profile - ~/.bash_profile - ~/.bashrc - /etc/bashrc
如果修改了配置文件,是不會立即重新加載的,需要我們重啓終端或者執行 source 命令
source [修改的配置文件地址]
在操作系統加載過程中,主要按如下順序進行加載
而作用範圍如下圖
shell 之運算
-
賦值運算符:用於字符串賦值,賦值使用 =,取消賦值使用 unset(想到於 js 中的 delete)
-
算數運算符:用於算數賦值,+-*/**%,使用方式如下
-
使用 expr 聲明,如 a=
expr 4 + 5
(不支持浮點數) -
數字常量:比 expr 精簡,可以使用 let 進行賦值操作,如 let a=1+2;
-
雙圓括號:是 let 的縮寫,如 ((a=10))、((a++))
其實 expr、let 或者 (()) 都是爲了向 shell 聲明,我目前在做算數賦值的動作,這也就能理解爲什麼要把整個式子都放在雙圓括號中了。
shell 之特殊符號
引號
-
雙引號:部分引用,使用這種引用時,$、`(反引號)、(轉義符) 這 3 個還是會解析成特殊的意義
-
單引號:完全引用,只原樣輸出
-
反引號:執行命令
括號
-
圓括號
-
():單獨使用圓括號會產生一個子 shell(這樣執行的表達式的輸出就不會在當前 shell 中顯示),也可用於數組賦值(colors=(yellow red blue))
-
(()):算數運算賦值,是 let 的縮寫
-
$():將命令賦值給變量
-
方括號
-
[]:單獨使用是測試或數組元素功能
-
[[]]:表示測試表達式
[ 5 -gt 4 ] # 5 是否大於 4
[[ 5 -gt 4 ]] # 5 是否大於 4
echo $? # 測試上條命令執行結果
-
尖括號 <>:重定向符號
-
花括號 {}:
-
輸出範圍:echo {0..9}
-
文件複製
# 下面兩條命令等價
cp /etc/passwd{,.bak}
cp /etc/passwd /etc/passwd.bak
運算符號和邏輯符號
-
算數運算符:+-*/ %
-
比較運算符:><=
-
邏輯運算符:&&||!
(( 5 > 4 ))
echo $?
(( 5 > 4 && 6 < 5))
echo $?
(( 5 > 4 || 6 < 5))
echo $?
(( ! 5 > 4 ))
echo $?
轉義符號
-
\ 轉義某字符
-
普通字符轉義後有不同的功能,如 \ n
-
特殊字符轉義後消除了特殊功能,如
\’
其他特殊字符
# if
if [ "$PS1" ]; then
enif
# case
case $TERM in
xterm*|vte*)
語句 1
;;
screen*)
語句 2
;;
*)
語句 3
;;
esac
運算的優先級
shell 之語句
-
順序
-
測試:退出與退出狀態、測試命令 test
-
分支:if-then
-
循環:
測試命令(學習分支語句的前提)
參考文檔:
- https://www.cnblogs.com/f-ck-need-u/p/7427357.html
狀態:$? 和 exit
根據程序是否正常執行(程序退出的狀態)進行判斷
-
exit:手動退出 shell 的命令
-
exit 10 返回 10 給 shell,返回值非 0 爲不正常退出
-
$? 用於判讀昂當前 shell 前一個進程是否正常退出(非 0 爲不正常退出)
命令:test
tesh 命令可以用於檢查文件或者比較值,可用於如下功能(可以用man test
查看更具體的命令)
-
整數比較測試
-
字符串測試
-
文件測試
test 測試語句可以簡化爲 [] 符號,而 [] 符號還有擴展寫法[[]] ,支持 &&、||、<、>(推薦使用[[]])
字符串測試
兩個字符串是否相等:[str1 = str2] 和 [str1 != str2]
#!/bin/bash
str1='a'
str2='a'
if [ $str1 = $str2 ]; then
echo '等於'
elif [ $str1 != $str2 ]; then
echo '不等於'
fi
讀取字符串長度是否是 0(空字符串):-z
#!/bin/bash
str1='a'
str2=''
if [ -z $str1 ]; then
echo 'str1 爲空字符串'
else
echo 'str1 不爲空字符串'
fi
if [ -z $str2 ]; then
echo 'str2 爲空字符串'
else
echo 'str2 不爲空字符串'
fi
整數比較測試
#!/bin/bash
a=1
b=2
# if [ $a = $b ]; then
if [ $a -eq $b ]; then
echo 'a 等於 b'
elif [ $a -ge $b ]; then
echo 'a 大於等於 b'
elif [ $a -gt $b ]; then
echo 'a 大於 b'
elif [ $a -le $b ]; then
echo 'a 小於等於 b'
elif [ $a -lt $b ]; then
echo 'a 小於 b'
elif [ $a -ne $b ]; then
echo 'a 不等於 b'
fi
if [[ $a = $b ]]; then
echo 'a 等於 b'
# elif [[ $a >= $b ]]; then
# echo 'a 大於等於 b'
elif [[ $a > $b ]]; then
echo 'a 大於 b'
# elif [[ $a <= $b ]]; then
# echo 'a 小於等於 b'
elif [[ $a < $b ]]; then
echo 'a 小於 b'
elif [[ $a != $b ]]; then
echo 'a 不等於 b'
fi
文件測試
分支語句
-
基礎 if
-
if-else
-
if-elif
-
嵌套 if
-
case 語句
基本用法:基礎 if
-
測試語句
-
命令返回值
if [ 測試條件成立 || 命令返回值爲 0 ]; then
相關行爲
fi # 結束
實例
if [ $UID = 0 ]; then
echo " root user "
fi
if [ pwd ]; then
echo " root user "
fi
基本用法:if-else
if [ 測試條件成立 || 命令返回值爲 0 ]; then
相關行爲
else
相關行爲
fi # 結束
實例
#!/bin/bash
if [ $UID = 0 ]; then
echo " root user "
else
echo " $UID user "
fi
基本用法:if-elif-else
if [ 測試條件成立 || 命令返回值爲 0 ]; then
相關行爲
elif [ 測試條件成立 || 命令返回值爲 0 ]; then
相關行爲
else
相關行爲
fi # 結束
實例
#!/bin/bash
if [ $UID = 0 ]; then
echo " root user "
elif [ $UID = 501 ]; then
echo " wangzy user "
else
echo " $UID user "
fi
基本用法:嵌套 if
#!/bin/bash
if [ 測試條件成立 || 命令返回值爲 0 ]; then
if [ 測試條件成立 || 命令返回值爲 0 ]; then
相關行爲
else
相關行爲
fi # 結束
else
相關行爲
fi # 結束
實例實現:先判斷是不是王志遠賬戶,是則再執行對應文件
#!/bin/bash
if [ $UID = 0 ]; then
echo " root user "
elif [ $UID = 501 ]; then
if [ -x /Users/wzyan/Documents/selfspace/kkb-down/demo/if/index.sh ]; then
$(pwd)/index.sh
else
echo " wangzy user error $(pwd)"
fi
else
if [ $(pwd) = "/Users/wzyan/Documents/selfspace/kkb-down/demo/if" ]; then
echo " wangzy user path"
else
echo " wangzy user error $(pwd)"
fi
fi
case 語句
case "$變量" in
"情況 1" )
命令...;;
"情況 2" )
命令...;;
* )
命令...;;
esac
實例實現:先判斷是不是王志遠賬戶,是則再執行對應文件
#!/bin/bash
case "$1" in
"start" | "restart")
echo $0 starting...
;;
"stop")
echo $0 stoping...
;;
*)
echo "$0 傳參錯誤 {start|stop|restart|reload}"
;;
esac
循環語句
-
for
-
c 語言風格的 for
-
while
-
util
-
嵌套循環
-
使用關鍵字 break 和 continue
for 循環
注意:
-
當使用反引號或者 $() 執行命令時,其結果會被當作列表處理
-
列表中包含多個變量,變量間用空格分隔
-
對文本處理時,要用文本查看命令取出文本內容,默認裝處理,如果出現空格就會當成多行處理
for 參數 in 列表
do 執行的命令
done #封閉一個循環
實例實現:遍歷列表輸出
#!/bin/bash
for i in {1..9}
do zzxzxxxzzzzzzzzzzzzzzzzzzzzzzzzzzzxxxxxecho hello; echo $i
done #封閉一個循環
# 讀取指定目錄下的所有可執行 sh 文件
for sc_name in /etc/profile.d/*.sh
do echo $sc_name; echo $i
done #封閉一個循環
實例實現:讀取命令結果進行取出所有 mp3 文件的文件名(basename [文件路徑] [文件後綴])
#!/bin/bash
for filename in `ls *.mp3`
do
mv $filename $(basename $filename .mp3).mp4
done
c 語言風格的 for
for((變量初始化;循環判斷條件;變量變化))
do
循環執行命令
done
實例實現:輸出 1-10
#!/bin/bash
for((i=1;i<=10;i++))
do
echo $i
done
while
特點:直到輸入爲非 0 才中止
while [test 測試爲假時中止]
do
命令
done
實例實現:輸出 1-10
#!/bin/bash
a=1 # 小於 10
while [ $a -lt 10 ]
do
((a++)); echo $a;
done
實現死循環
while :; do echo ;done
until
特點:與 while 相反,直到輸入爲 0 才中止
until [test 測試爲真時中止]
do
命令
done
實例實現:輸出 1-10
#!/bin/bash
while []
do
命令
done
嵌套循環
找出 / etc/profile.d 目錄下所有可執行文件
for sc_name in /etc/profile.d/*.sh
do
echo $sc_name
done
找出 / etc/profile.d 目錄下所有可執行文件並執行
for sc_name in /Users/wzyan/Documents/selfspace/kkb-down/demo/for/test-x/*.sh; do
if [ -x $sc_name ]; then
. $sc_name
fi
done
使用關鍵字 break 和 continue
找出 / etc/profile.d 目錄下所有可執行文件並執行
for num in {1..9}
do
echo $num
done
實戰:處理命令行參數
-
命令行參數可以使用 2..{n} 進行讀取
-
$0 代表腳本名稱
-
和 @代表所有位置參數
-
$# 代表位置參數的數量
for pos in $*; do
if [ "$pos" = "help" ]; then
echo $pos
fi
done
shell 之函數
用於重複命令邏輯的集合
-
函數的定義和執行
-
函數參數
-
函數的作用範圍
-
模塊化
-
系統自建函數庫
函數的定義和執行
定義
# function 關鍵詞可以省略
function fname(){
命令
}
執行
fname
實例
# function 關鍵詞可以省略
function cdls() {
cd /var
ls
}
cdls
函數變量
分爲內部定義的變量和外部的傳參
內部定義
local 變量名
外部傳參
調用
funcName a b
這裏有巨坑:
- 函數的返回值(return)只支持數字類型,所以我們需要使用 echo 輸出流的形式巧妙的模擬返回其他類型數據
function funcName(){
echo aaa
}
returnContent=`funcName`
- 如果參數中包含了空格,一定要在傳遞時用引號包裹,不然會被解析成多個參數(比如數組)
arr=(a b c)
funcName "${arr[@]}"
-
參數和返回值只支持字符串,所以如果想支持數組,可以採用如下方式:
-
入參:將數組的所有內容轉爲字符串傳遞,再在內部轉爲數組
-
返回值:將數組的所有內容轉爲字符串返回,再在外部轉爲數組
<<'COMMENT'
追加元素
@param arr 數組名
@param item 被追加的元素
@return 改變後的數組字符串
COMMENT
function push() {
# 獲取數組名
local name=$1
# 獲取數組內容,創造一個對應的內部數組
eval local innerArr=(\${${name}[@]})
local item=$2
innerArr+=($item)
# 先清除的嘗試 失敗
# local length=${#innerArr[@]}
# eval unset $name
# for((i=0;i<$length;i++)); do
# eval unset $name[$i]
# done
echo ${innerArr[@]}
}
arr=(a b c)
arr=(`push arr aaa`) # 這裏的數組已經成爲(a b c d)
echo arr===${arr[@]}
內部使用
$1 $2 $3...$n
注意點
-
如果兩位數了,假如是 10,就需要使用 ${10}
-
$#:參數個數總數
-
$*:作爲字符串輸出所有參數
-
:腳本運行的當前進程號
funWithParam(){
echo "第一個參數爲 $1 !"
echo "第二個參數爲 $2 !"
echo "第十個參數爲 $10 !"
echo "第十個參數爲 ${10} !"
echo "第十一個參數爲 ${11} !"
echo "參數總數有 $# 個!"
echo "作爲一個字符串輸出所有參數 $* !"
}
funWithParam 1 2 3 4 5 6 7 8 9 34 73
實例之傳參
# function 關鍵詞可以省略
function cdls() {
cd $1
ls
}
cdls /tmp
實例之內部變量
# function 關鍵詞可以省略
function checkpid() {
local i
for i in $*; do
# linux 中,每個在運行的進程都會有一個在/proc 目錄下對應的子目錄,目錄名爲 pid,這也是 ps
[ -d "/proc/$i" ] && return 0
done
}
checkpid 5380
echo $?
模塊化
其實 shell 的模塊化(即函數的導入導出)非常簡單,我們知道 shell 會話間的變量是互通的(函數中的 local 變量除外),這時我們只要以當前 shell 會話執行下定義函數的 sh,就能訪問到對應的函數了。而如何以當前 shell 執行呢?就是之前講到的知識:以 source 或者. 方式執行 sh 文件。
引入
引入方式很簡單
source [函數庫的絕對路徑]
案例
存在函數庫 libs/os.sh
#!/bin/bash
# 獲取當前系統類型
# @return platForm
function getPlatForm() {
local innerPlatForm
if [[ $(uname) == 'Darwin' ]]; then
innerPlatForm=mac
fi
if [[ $(uname) == 'Linux' ]]; then
innerPlatForm=linux
fi
if [[ $(uname) == 'win32' ]]; then
innerPlatForm=window
fi
# shell 的 function 只能返回整數值,所以如果想接收字符串類型 echo+$()方式獲取
echo $innerPlatForm
}
引入使用時
source ./libs/os.sh
# 執行函數
platForm=$(getPlatForm)
# 看輸出結果
echo $platForm
注意事項
- shell 的 function 中的 return 語句只能返回整數值,所以如果想接收字符串類型 echo+$() 方式獲取
系統自建函數庫
linux 系統中存在自帶的函數庫,地址如下
/etc/init.d/functions
在配置文件中也是用了大量的循環判斷,可以看/etc/profile
或者/etc/.bashrc
語言特殊點
一門語言的出現必然存在其特殊點,java 的 jvm 和麪向對象、js 的解釋型、python 的自動化等等,shell 也不例外
-
腳本控制:優先級控制、捕獲信號
-
計劃任務:定時執行任務
-
延時計劃任務:anacontab
-
計算任務加鎖:flock
腳本控制
cpu 和資源的分配
-
nice 和 renice 調整優先級
-
死循環的控制:cpu 佔用和死機
當執行如下命令時,系統會進入假死狀態
.()^C|.&}.
捕獲信號
捕獲語句語法如下
trap "[行爲]" [信號編號]
-
kill 默認會發送 15 號信號給應用程序
-
ctrl + c 發送 2 號信息給應用程序
-
-9 號信號不可阻塞
案例一:監聽 15 號信號
循環監聽 15 信號,當此信號被下發時,打印sig 15
;
#!/bin/bash
trap "echo sig 15" 15
echo $$ # 打印端口
while :; do
:
done
啓動之後會首先打印端口口,然後我們執行如下命令像指定進程發送 15 信號
kill -15 [pid]
案例二:監聽 2 號信號
循環監聽 15 信號,當此信號被下發時,打印sig 15
;
#!/bin/bash
trap "echo sig 2" 2
echo $$ # 打印端口
while :; do
:
done
啓動之後我們ctrl+c
實現觸發
計劃任務
-
一次性計劃任務:at
-
週期性計劃任務:cron
-
延時計劃任務:anacrontab
-
計劃任務加鎖 flock
注意事項:
- 定時任務是沒有終端的,這也就意味這默認沒有輸出,如果需要查看輸出結果就需要用到重定向符號
>
一次性計劃任務
at [選項] [時間]
-
錄入命令:回車後就會出現
at>
這種要求輸入命令的提示,輸入需要在指定時間輸入命令 -
完成記錄:輸入完成後按下 ctrl + D 即完成了命令的存入
-
查看記錄:可以使用命令
atq
查看當前一次性任務列表 -
刪除記錄:可以使用
atrm + [編號]
進行刪除
注意事項
-
at 命令是一次性定時計劃任務,
at
的守護進程atd
會以後臺模式運行,檢查作業隊列來運行。 -
默認情況下,
atd
守護進程每60
秒檢查作業隊列,有作業時,會檢查作業運行時間,如果時間與當前時間匹配,則運行此作業。 -
at
命令是一次性定時計劃任務,執行完一個任務後不再執行此任務了 -
在使用
at
命令的時候,一定要保證atd
進程的啓動 , 可以使用相關指令來查看
ps -ef | grep atd //可以檢測 atd 是否在運行
畫一個示意圖
選項
時間定義
案例
1 分鐘後將在 / tmp 目錄下創建hello.txt
並寫入內容hello
at now + 1 minutes
# at>內容框出現後輸入如下內容,然後按 ctrl + D
echo hello > /tmp/hello.txt
# 驗證
cat /tmp/hello.txt
週期性計劃任務:cron
crontab [選項]
crontab 是根據選項執行對應行爲的
-
錄入命令:crontab -e ,回車後打開一個 vim 的編輯界面,輸入格式爲【分鐘 小時 日期 月份 星期 執行的命令】,_代表所有,比如小時位是 *,代表每小時;比如
_ \* \* \* \* /usr/bin/date >> /tmp/deepinout.com.txt
-
查看計劃執行記錄:可以執行
tail -f /var/log/cron
查看定時任務執行記錄 -
查看用戶的計劃:
crontab -l
,其實就等同於cat /var/spool/cron/和用戶同名的文件
,只是做了個文件讀取到標準輸出上操作而已 -
刪除記錄:可以使用
crontab -r [編號]
進行刪除
注意事項
-
最小的設置時間單位是分鐘(可以用第三方軟件包實現秒)
-
注意命令的路徑問題
每個用戶都有自己的週期性計劃任務配置文件,保存在 / var/spool/cron / 下面,以用戶名作爲文件名。
crontab 示例
每分鐘將日期保存在指定文件中
* * * * * /usr/bin/date >> /tmp/deepinout.com.txt
固定星期幾執行計劃任務
週一每分鐘執行
* * * * 1 /usr/bin/date >> /tmp/deepinout.com.txt
週五每分鐘執行
* * * * 5 /usr/bin/date >> /tmp/deepinout.com.txt
週一和週五每分鐘執行
* * * * 1,5 /usr/bin/date >> /tmp/deepinout.com.txt
週一至週五每分鐘執行
* * * * 1-5 /usr/bin/date >> /tmp/deepinout.com.txt
同時滿足指定日期和星期才執行任務
7 月 8 日且是週一至週五,每分鐘執行
* * 8 7 1-5 /usr/bin/date >> /tmp/deepinout.com.txt
延時計劃任務:anacontab
爲了緩解 cron 中一瞬間大量任務併發執行而導致系統壓力過大的問題。相關文件
-
/etc/cron.d/0hourly:存儲着運行任務的信息
-
/etc/anacontab:
# /etc/anacrontab: configuration file for anacron
# See anacron(8) and anacrontab(5) for details.
SHELL=/bin/sh
PATH=/sbin:/bin:/usr/sbin:/usr/bin
MAILTO=root
# the maximal random delay added to the base delay of the jobs
RANDOM_DELAY=45
# the jobs will be started during the following hours only
START_HOURS_RANGE=3-22
#period in days delay in minutes job-identifier command
1 5 cron.daily nice run-parts /etc/cron.daily
7 25 cron.weekly nice run-parts /etc/cron.weekly
@monthly 45 cron.monthly nice run-parts /etc/cron.monthly
環境變量部分
-
RANDOME_DELAY=45:表示 anacron 在執行任務前先延時一段隨記的時間再執行,這段隨機的時間爲 0-45 分鐘之內的隨機數。
-
START_HOURS_RANGE=3-22:指定了只有在凌晨 3 點到晚上 22 點這個時間段內才允許執行任務。
執行計劃部分
由四部分組成:
-
period in days:輪迴天數,表示任務多少天執行一次。
-
delay in minutes:表示啓動 Anacron 和運行作業時間之間的延遲,單位爲分鐘. 當然前提是自最後一次運行之後所經過的時間超出了輪迴天數。 但是它並不是作業真正運行的時間,真正運行的時間還需要加上 RANDOME_DELAY 中設置的隨機分鐘數。
-
job-identifier:作業的標識符。anacron 在執行任務時會將日期寫入
/var/spool/anacron/$job-identifier
文件中 -
command:實際運行的命令。這裏的
run-parts
是一個運行指定目錄中所有程序與腳本的命令,可以通過man run-parts
來查看它的說明
特點如下
-
anacron 只運行每天 / 周 / 月的任務
-
如果一項任務在預定的時間機器處於關機狀態,那麼在下次開機的時候會執行;
-
anacron 自己沒有守護進程運行,需要依賴於外部工具(例如 crond);
-
可以設置一個延時時間,當到指定時間時,等待一個延時時間再執行任務;
計劃任務加鎖:flock
爲什麼出現
在使用 crontab 管理定時腳本時,如果設定的腳本執行時間間隔較短,例如 5 分鐘執行一次,正常情況下,腳本執行耗時 1 分鐘,在非正常情況下(如服務器壓力較大的情況下,或數據量突然增大),腳本執行時間超過 5 分鐘,這時就會造成多個腳本同時執行,嚴重時甚至拖垮服務器,影響服務器上的其它服務。
當多個進程可能會執行同一個腳本,這些進程需要保證其它進程沒有在操作,以免重複執行,這就是 flock 的作用。
怎麼做到的
通常,這樣的進程會使用一個鎖文件,也就是建立一個文件來告訴別的進程自己在運行,如果檢測到那個文件存在則認爲有操作同樣數據的進程在工作。
如何使用
flock -h
Usage:
flock [options] <file|directory> <command> [command args]
flock [options] <file|directory> -c <command>
flock [options] <file descriptor number>
Options:
-s, --shared: 獲得一個共享鎖
-x, --exclusive: 獲得一個獨佔鎖
-u, --unlock: 移除一個鎖,通常是不需要的,腳本執行完會自動丟棄鎖
-n, --nonblock: 如果沒有立即獲得鎖,直接失敗而不是等待
-w, --timeout: 如果沒有立即獲得鎖,等待指定時間
-o, --close: 在運行命令前關閉文件的描述符號。用於如果命令產生子進程時會不受鎖的管控
-c, --command: 在 shell 中運行一個單獨的命令
-h, --help 顯示幫助
-V, --version: 顯示版本
使用案例
flock -xn "/tmp/f.lock" -c "/root/a.sh"
下圖是兩個終端同時運行這條命令,後執行者會因爲搶不到鎖而得不到執行,馬上退出
重點概念:管道
當我們單純的運行多個程序而不需要它們彼此間有互通時,存在如下方式
echo 1;echo 2;
echo 1&&echo 2;
echo 1||echo 2;
而如果需要互通,比如第一個命令的返回傳遞給第二個命令,就需要用到管道了,管道的本質就是將多個程序進行了一個連接,和信號一樣,也是進程通信的方式之一
-
管道與管道符(即匿名管道,|,作用是將前一個命令的結果傳遞給後面的命令,如
ls -l | more
) -
子進程與子 shell
ls /etc/ | more
netstat -an | grep ESTABLISHED | wc -l
注意:因爲管道是以子進程方式進行執行的,所以內建命令的執行不會傳遞給父進程。
重點概念:重定向
重定向的本質就是將文件和輸入、輸出(包含標準輸出、錯誤輸出)進行了一個連接
-
輸入重定向符號:<
-
輸出重定向符號:
-
:覆蓋寫入文件
-
:追加寫入文件
-
2>:錯誤輸出寫入文件
-
&>:正確和錯誤輸出統一寫入到文件中
多行內容寫入
cat > [文件名] << EOF
xxxx 內容
EOF
具體整理
案例
# 創建 a.txt 並寫入 123
echo '123' > a.txt
# 將文件內容輸入進變量
read a < a.txt
# 將 a 變量內容追加入 a.txt
echo $a >> a.txt
# 將命令執行錯誤的提示內容寫入 error.log 中
nocmd 2> error.log
shell 之調試
說了這麼多,最後還是要落到寫的程度來,【紙上得來終覺淺,絕知此事要躬行】,寫自然免不了有問題,當有問題的時候我們就需要一些手段去調試我們的代碼了。怎麼調呢?對我個人而言經歷了三個階段
-
log 時期:變量使用 echo 輸出,個人通過 vscode 插件優化至一鍵生成日誌語句
-
自動輸出時期:
set
命令用來修改 Shell 環境的運行參數,也就是可以定製環境,這裏就可以做到自動輸出日誌 -
運行時處理時期:也就是我們常說的 debugger 了,沒錯,我們可以和 debugger js 一樣去可視化處理 shell 腳本(cool~);個人通過 vscode 插件優化至一鍵完成搭建動作【如需試用可以搜索:weiyi-tools】
逐一分享啦
log 時期
這個很簡單,唯一要注意的就是 shell 變量類型導致的奇葩輸出,在 shell 中默認變量是字符串類型,而其他類型的輸出則有不同
-
string:echo $ 變量名
-
array:echo 變量名,日後直接變量名會只打印第一個字符
-
function:無法打印函數體(個人沒找到),echo 變量名會打印函數名,而且函數的引用不能加 $
這些就是 🐢 的臀部 -- 規定了,踩坑良久,獻於諸君。我們來看看我說的那個 vscode 插件支持【一鍵輸出打印語句】,目前支持 js 和 shell。【如需試用可以搜索:weiyi-tools】
自動輸出時期:set
set
命令用來修改 Shell 環境的運行參數,也就是可以定製環境。一共有十幾個參數可以定製,官方手冊有完整清單。
使用方式很簡單,在腳本的頂部放上 set 命令 + 對應配置即可
set -[...options]
具體配置如下
順便提一下,如果命令行下不帶任何參數,直接運行set
,會顯示所有的環境變量和 Shell 函數。
常見使用方式
將下面內容放在腳本頂部可以做到
-
只要發生錯誤,就終止執行 (建議不用)
-
遇到不存在的變量就會報錯,並停止執行。
-
運行結果之前,先輸出執行的那一行命令。
-
管道符鏈接的,只要一個子命令失敗,整個管道命令就失敗,腳本就會終止執行。
set -euxo pipefail
補充:其實本人還是喜歡下面這樣就好啦,因爲我會封裝很多函數,如果加上了 e 就會導致中止
set -ux
運行時處理時期:debugger
在這,會發現自動化時期其實並沒有做到隨時隨地查看自己想看的運行狀態,甚至還會打印很多很多不需要看的內容。有沒有一種辦法,可以直接圖形化的查看當前調試狀態下參數的值、卡住程序快照、逐步運行呢?聰明的小夥伴肯定想到了,這不就是 debugger 調試嘛。
js 我們肯定是可以的,shell 呢?其實也可以,只是有幾個小要求
-
系統 bash 版本 > 4.x (mac 自帶版本爲 3.2,需要手動升級):參考 Mac 升級最新版 bash
-
需要安裝 vsode 的插件:bash debug
-
完成 bash debug 插件配置文件,即 launch.json 文件
這幾步的動作原博主寫的很贊,我就不贅述了,可見:https://liushiming.cn/article/debug-bash-on-macos.html;唯一有一點補充的就是,記得把 program 替換成${file}
(原文複製下面的也可以),這代表要調試當前打開的 shell 文件。
{
// 使用 IntelliSense 瞭解相關屬性。
// 懸停以查看現有屬性的描述。
// 欲瞭解更多信息,請訪問: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "bashdb",
"request": "launch",
"name": "Bash-Debug (simplest configuration)",
"program": "${file}"
}
]
}
最後,我們來看下效果
關於個人的優化:一鍵完成搭建動作
直接看效果【如需試用可以搜索:weiyi-tools】
至此,shell 之調試篇,完成。
自實現 vscode 插件對 shell 的支持
除此之外的內容
-
vscode 插件支持常見的 shell snippet
-
vscode 插件支持【屬性. xxx】形式使用函數庫
寫不動了,到時候另起一文,看下效果吧,對 vscode 插件開發想了解的同學也可以前往我的專欄查看:https://juejin.cn/column/7078626256777904165
尾聲
救命,內容實在是太多了,而且還有非常非常多踩的坑沒有分享【封裝 shell 函數庫、vscode 智能補全等等等】,開始以爲能一文結束,寫着寫着才發現 shell 的世界浩如煙海,我只是抓住了一個角而已,還好,名字是帶你走進這個世界,本文只能作爲一個脈絡文了,後續會根據本文進行拆分,輸出一個專欄,希望同學們有遇到 shell 中有趣的知識點也能夠留言和我分享。
老規矩,雞湯一下
當你埋頭苦讀的時候,阿拉斯加的鱈魚正躍出水面,當你伏案寫作的時候,南太平洋的海鷗正掠過海岸,當你認真工作的時候,地球的極圈正五彩斑斕,但夢要你親自實現,那些你覺得看不到的人,和遇不到的風景,都終將在你生命裏出現
每到一個新世界,都是一片新的美好風景,luck!
常用文檔
-
寫給前端的 shell 腳本編程詳解 - SegmentFault 思否:https://segmentfault.com/a/1190000037797344
-
Shell 流程控制 | 菜鳥教程:https://www.runoob.com/linux/linux-shell-process-control.html
-
Shell 字符串截取(非常詳細):http://c.biancheng.net/view/1120.html
-
Shell 腳本入門 - 阿里雲開發者社區:https://developer.aliyun.com/learning/course/794?spm=a2c6h.21254954.graph.26.eabc4fe0ZUb86s
-
Shell:常用的語句整理 - 阿里雲開發者社區:https://developer.aliyun.com/article/1007026?spm=a2c6h.21258778.0.0.6a4f39d26z0Qhx
-
Linux shell 腳本之 if 條件判斷_doiido 的博客 - CSDN 博客_shell 的 if 判斷語句:https://blog.csdn.net/doiido/article/details/43966819
-
Mac 常用命令清單 - 掘金:https://juejin.cn/post/6844903608996069390
-
Shell 基礎及實例 - 掘金:https://juejin.cn/post/6844903943886077960#heading-46
-
shell 命令行參數(基本) - 蒼青浪 - 博客園:https://www.cnblogs.com/cangqinglang/p/11942567.html
-
在 Shell 腳本中解析選項 | 始終:https://liam.page/2016/11/11/ways-to-parse-arguments-in-shell-script/
-
淺談 shell 遍歷數組的幾種方法 - 編程寶庫:http://www.codebaoku.com/it-shell/it-shell-201789.html
-
使用 vscode 調試 bash 腳本:https://liushiming.cn/article/debug-bash-on-macos.html
-
Mac 安裝新版本 Bash | Happy Hack Everday:https://blog.happyhack.io/2020/07/16/upgrade-bash-on-macOS/
-
Linux:查看佔用 cpu / 內存 資源最多的進程並殺死:https://blog.csdn.net/qq_41538097/article/details/107539714
-
Bash 腳本 set 命令教程:https://www.ruanyifeng.com/blog/2017/11/bash-set.html
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/gtIWO3ItxyLdQeVorXtShQ