萬字長文:帶你走進 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 而言怎麼實現上面的需求:刪除佔用內存最多的兩個進程

  1. 創建 shell 文件,作爲函數庫,假定取名爲shell-libs.sh

  2. 定義對應函數,假定取名爲killProcessByMemory,實現功能爲,傳遞進程數 n,殺死佔用內存排名前 n 位進程

  3. 引入函數庫文件: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

函數庫很多,但最重要的是,我們的初始動作只需要做一遍,一勞永逸!!

其實不只是做一個常用命令的封裝,我們還可以做到

好了,好處顯而易見,我們現在對於 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 號:

sort 對內容根據指定列進行排序

head 只顯示前指定行的數據,獲取默認前 10 行數據

awk 數據處理神器,這裏用於獲取第二列(PID,進程 id)

亂入結束,讓我們開始叭!

如何學習一門語言

語言大同小異,核心不過【變量、運算、語句、函數、框架】,shell 也是門語言,所以我們也會根據這個脈絡進行學習;

但 shell 又有所不同,因爲它的出現強依賴【unix】,unix 的哲學:一條命令只做一件事情;爲了組合命令和多次執行,最先是分號; 用於同行組合,但不利於展示,於是出現了 shell 腳本文件,用來保存需要執行的命令,所以有別於其他語言又會出【環境變量、配置文件等等概念】,也強依賴 unix 的各個命令,這點是需要貫徹整個 shell 學習脈絡的。

注意:本文不會過多的牽扯 linux 的知識,awk、xarg、print 等等等,小夥伴放心,我們學習最核心的語言邏輯,至於工具,多用即可,放心食用

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 腳本文件,用來保存需要執行的命令。

  1. 指定解釋器:#!/bin/xxx,默認是#!/bin/bash;這其實是後綴,因爲在 Linux 中,文件後綴是沒有意義的,所以操作系統要知道這個腳本文件該用什麼應用來執行,就需要這個註釋來指明,比如 node 就是#!/usr/bin/node

  2. 爲文件賦予可執行權限,即chmod u+x [filename]

案例:實現 sh 腳本文件,查看 / var 下所有子目錄空間
cat >> memo.sh << EOF
#!/bin/bash
cd /var
du -sh *
EOF
# 賦予權限 chmod u+x memo.sh
# 執行 ./memo.sh
怎麼執行一個 shell 腳本

存在四種執行方式

PoKwr4

shell 之變量

增刪改查:定義、賦值、引用、刪除、類型處理

定義

即變化的量,字母、數字、下劃線的組合,且不能以數字開頭(shell 變量是弱類型,不區分變量類型);

賦值

非交互式賦值存在四種賦值方式

注意:變量值如果有空格等特殊字符,可以包含在 ""或''中

引用 + 查詢

在 shell 中存在四種引用:單引號、雙引號、${}、``

變量名即對變量的引用,在部分情況下可以縮寫爲變量名(不產生歧義,比如字符串拼接就不能縮寫);

而查詢則可以使用set命令,其會默認查詢系統中默認所有已經生效的變量,包括系統變量, 也包括自定義變量,結合管道運算符可以精準查詢

set | grep []

舉例:查詢wzy變量的值

刪除

刪除則是unset命令,使用方式如下

unset [變量名]

特殊數據結構之數組

colors=( yellow red blue )
echo ${colors[@]}
echo ${#colors[@]}
echo ${colors[0]}
echo $colors

類型處理

shell 變量爲弱類型並且默認是字符串類型,要想設置變量的類型,可以使用declare命令。

declare 命令用來聲明變量類型

declare [+/-] [選項] 變量名

3xplZn

設定類型
整數型

舉例,將變量設定爲整數型從而實現加和,通過配置項-i進行設定

數組類型

通過配置項-a進行設定

定義

取第一個值

取第二個值

輸出所有

取消變量的類型屬性

承接上面,此時變量c已經是整型了,我們希望實現取消變量c的類型(即變回字符型),從而實現拼接的效果

設定爲只讀變量

通過配置項-r進行設定

declare -r [變量名]

查詢變量

此時也可以通過配置項-p查詢變量類型

declare -p [變量名] #顯示指定變量的被聲明的類型

設置爲環境變量

通過配置項-x設置變量爲環境變量

declare -x [變量名]

由此也可以看到,之前定義全局環境變量的方式export [變量名]其實就是declare -x [變量名]的語法糖

變量的作用範圍

  1. 默認範圍:只針對當前的終端(shell)生效

  2. 支持子進程訪問父進程的變量:export [變量名]=[變量值];(取消變量可以使用unset [變量名]

  3. 系統環境變量:每個 shell 打開都能獲取到的變量

其中第一條可以使用bash命令創建一個新的 shell 進行測試;第二條就是 export 和 unset 關鍵字;關鍵是系統環境變量,重點分析:

系統環境變量

環境變量主要根據兩個角度進行劃分:用戶級別是屬於系統還是屬於用戶;shell 級別是屬於當前的 shell 還是所有 shell。而這些是通過配置文件進行區分和記錄的,不同作用範圍和功能的變量分屬不同的配置文件中,主要有四個文件/etc/profile~/.bash_profile~/.bashrc/etc/bashrc。系統環境變量的查詢:env 和 set

常見變量

FlKSSO

環境變量配置文件

需要注意的是

加載順序爲

/etc/profile - ~/.bash_profile - ~/.bashrc - /etc/bashrc

如果修改了配置文件,是不會立即重新加載的,需要我們重啓終端或者執行 source 命令

source [修改的配置文件地址]

在操作系統加載過程中,主要按如下順序進行加載

而作用範圍如下圖

shell 之運算

其實 expr、let 或者 (()) 都是爲了向 shell 聲明,我目前在做算數賦值的動作,這也就能理解爲什麼要把整個式子都放在雙圓括號中了。

shell 之特殊符號

引號
括號
[ 5 -gt 4 ] # 5 是否大於 4
[[ 5 -gt 4 ]] # 5 是否大於 4
echo $? # 測試上條命令執行結果
# 下面兩條命令等價
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 $?
轉義符號
其他特殊字符

nWH0Qj

# if
if [ "$PS1" ]; then

enif
# case
case $TERM in
  xterm*|vte*)
   語句 1
  ;;
  screen*)
   語句 2
  ;;
  *)
   語句 3
  ;;
esac
運算的優先級

shell 之語句

測試命令(學習分支語句的前提)

參考文檔:

狀態:$? 和 exit

根據程序是否正常執行(程序退出的狀態)進行判斷

命令: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
整數比較測試

Z1lgaY

#!/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
文件測試

L4uCsV

分支語句

基本用法:基礎 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 循環

注意:

  1. 當使用反引號或者 $() 執行命令時,其結果會被當作列表處理

  2. 列表中包含多個變量,變量間用空格分隔

  3. 對文本處理時,要用文本查看命令取出文本內容,默認裝處理,如果出現空格就會當成多行處理

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=# 小於 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

實戰:處理命令行參數

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

這裏有巨坑:

  1. 函數的返回值(return)只支持數字類型,所以我們需要使用 echo 輸出流的形式巧妙的模擬返回其他類型數據
function funcName(){
 echo aaa
}
returnContent=`funcName`
  1. 如果參數中包含了空格,一定要在傳遞時用引號包裹,不然會被解析成多個參數(比如數組)
arr=(a b c)
funcName "${arr[@]}"
  1. 參數和返回值只支持字符串,所以如果想支持數組,可以採用如下方式:

  2. 入參:將數組的所有內容轉爲字符串傳遞,再在內部轉爲數組

  3. 返回值:將數組的所有內容轉爲字符串返回,再在外部轉爲數組

<<'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

注意點

  1. 如果兩位數了,假如是 10,就需要使用 ${10}

  2. $#:參數個數總數

  3. $*:作爲字符串輸出所有參數

  4. :腳本運行的當前進程號

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
注意事項
  1. shell 的 function 中的 return 語句只能返回整數值,所以如果想接收字符串類型 echo+$() 方式獲取

系統自建函數庫

linux 系統中存在自帶的函數庫,地址如下

/etc/init.d/functions

在配置文件中也是用了大量的循環判斷,可以看/etc/profile或者/etc/.bashrc

語言特殊點

一門語言的出現必然存在其特殊點,java 的 jvm 和麪向對象、js 的解釋型、python 的自動化等等,shell 也不例外

腳本控制

cpu 和資源的分配

當執行如下命令時,系統會進入假死狀態

.()^C|.&}.

捕獲信號

捕獲語句語法如下

trap "[行爲]" [信號編號]
案例一:監聽 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實現觸發

計劃任務

注意事項:

  1. 定時任務是沒有終端的,這也就意味這默認沒有輸出,如果需要查看輸出結果就需要用到重定向符號>

一次性計劃任務

at [選項] [時間]
注意事項
  1. at 命令是一次性定時計劃任務,at 的守護進程 atd 會以後臺模式運行,檢查作業隊列來運行。

  2. 默認情況下,atd 守護進程每 60 秒檢查作業隊列,有作業時,會檢查作業運行時間,如果時間與當前時間匹配,則運行此作業。

  3. at 命令是一次性定時計劃任務,執行完一個任務後不再執行此任務了

  4. 在使用 at 命令的時候,一定要保證 atd 進程的啓動 , 可以使用相關指令來查看

ps -ef | grep atd //可以檢測 atd 是否在運行

畫一個示意圖

選項

時間定義

TsS4yV

案例

1 分鐘後將在 / tmp 目錄下創建hello.txt並寫入內容hello

at now + 1 minutes
# at>內容框出現後輸入如下內容,然後按 ctrl + D
echo hello > /tmp/hello.txt
# 驗證
cat /tmp/hello.txt

週期性計劃任務:cron

crontab [選項]

crontab 是根據選項執行對應行爲的

注意事項
  1. 最小的設置時間單位是分鐘(可以用第三方軟件包實現秒)

  2. 注意命令的路徑問題

每個用戶都有自己的週期性計劃任務配置文件,保存在 / 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/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
環境變量部分
執行計劃部分

由四部分組成:

特點如下

計劃任務加鎖: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"

下圖是兩個終端同時運行這條命令,後執行者會因爲搶不到鎖而得不到執行,馬上退出

重點概念:管道

當我們單純的運行多個程序而不需要它們彼此間有互通時,存在如下方式

OJT1E9

echo 1;echo 2;
echo 1&&echo 2;
echo 1||echo 2;

而如果需要互通,比如第一個命令的返回傳遞給第二個命令,就需要用到管道了,管道的本質就是將多個程序進行了一個連接,和信號一樣,也是進程通信的方式之一

ls /etc/ | more
netstat -an | grep ESTABLISHED | wc -l

注意:因爲管道是以子進程方式進行執行的,所以內建命令的執行不會傳遞給父進程。

重點概念:重定向

重定向的本質就是將文件和輸入、輸出(包含標準輸出、錯誤輸出)進行了一個連接

多行內容寫入

cat > [文件名] << EOF
xxxx 內容
EOF

具體整理

wuO8gZ

xiDfii

案例

# 創建 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 時期

這個很簡單,唯一要注意的就是 shell 變量類型導致的奇葩輸出,在 shell 中默認變量是字符串類型,而其他類型的輸出則有不同

這些就是 🐢 的臀部 -- 規定了,踩坑良久,獻於諸君。我們來看看我說的那個 vscode 插件支持【一鍵輸出打印語句】,目前支持 js 和 shell。【如需試用可以搜索:weiyi-tools】

自動輸出時期:set

set命令用來修改 Shell 環境的運行參數,也就是可以定製環境。一共有十幾個參數可以定製,官方手冊有完整清單。

使用方式很簡單,在腳本的頂部放上 set 命令 + 對應配置即可

set -[...options]

具體配置如下

uU1Kva

順便提一下,如果命令行下不帶任何參數,直接運行set,會顯示所有的環境變量和 Shell 函數。

常見使用方式

將下面內容放在腳本頂部可以做到

set -euxo pipefail

補充:其實本人還是喜歡下面這樣就好啦,因爲我會封裝很多函數,如果加上了 e 就會導致中止

set -ux

運行時處理時期:debugger

在這,會發現自動化時期其實並沒有做到隨時隨地查看自己想看的運行狀態,甚至還會打印很多很多不需要看的內容。有沒有一種辦法,可以直接圖形化的查看當前調試狀態下參數的值、卡住程序快照、逐步運行呢?聰明的小夥伴肯定想到了,這不就是 debugger 調試嘛。

js 我們肯定是可以的,shell 呢?其實也可以,只是有幾個小要求

  1. 系統 bash 版本 > 4.x (mac 自帶版本爲 3.2,需要手動升級):參考 Mac 升級最新版 bash

  2. 需要安裝 vsode 的插件:bash debug

  3. 完成 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 插件開發想了解的同學也可以前往我的專欄查看:https://juejin.cn/column/7078626256777904165

尾聲

救命,內容實在是太多了,而且還有非常非常多踩的坑沒有分享【封裝 shell 函數庫、vscode 智能補全等等等】,開始以爲能一文結束,寫着寫着才發現 shell 的世界浩如煙海,我只是抓住了一個角而已,還好,名字是帶你走進這個世界,本文只能作爲一個脈絡文了,後續會根據本文進行拆分,輸出一個專欄,希望同學們有遇到 shell 中有趣的知識點也能夠留言和我分享。

老規矩,雞湯一下

當你埋頭苦讀的時候,阿拉斯加的鱈魚正躍出水面,當你伏案寫作的時候,南太平洋的海鷗正掠過海岸,當你認真工作的時候,地球的極圈正五彩斑斕,但夢要你親自實現,那些你覺得看不到的人,和遇不到的風景,都終將在你生命裏出現

每到一個新世界,都是一片新的美好風景,luck!

常用文檔

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/gtIWO3ItxyLdQeVorXtShQ