簡單的 Shell 指令碼入門教程

AaronLin發表於2022-01-05

Shell指令碼 運作方式與解釋型語言相當,如果有語言基礎,學起 Shell 指令碼就非常容易,但是 Shell 與常見的語言不同,一些常見的函式在 Shell 中需要組合一些命令得以實現

工具推薦

Shell 似乎沒有定製的 IDE,這裡推薦 VS Code 搭配對應的外掛:

  1. shellman 智慧提示和自動補全,在外掛頁面有介紹常用程式碼片段的觸發關鍵詞,作者在 Shellman reborn 中寫到了 Shellman 誕生的故事,挺有趣的
  2. shellcheck 語法靜態檢查工具,外掛安裝後需要本地安裝 shellcheck,參考 shellcheck Installing,Mac OS 可以使用 brew install shellcheck,這樣在寫 Shell 的時候,語法有誤的地方就會以波浪線的方式提示
  3. shell-format 程式碼整理,Win 快捷鍵:Alt + Shift + F,Mac OS 快捷鍵:option + shift + F
  4. Code Runner 指令碼執行,右鍵 Run Code,Win 快捷鍵:Ctrl + Alt + N,Mac OS 快捷鍵:control + option + N

執行 shell 指令碼

新建指令碼:test.sh

#!/usr/bin/env bash

# 使用echo 列印字串或者變數
echo 'hello world'

可以用 Code Runner 執行,就會輸出:hello world

在 Shell指令碼 的第一行一般會寫 #!/bin/bash 這個是 Shebang#! 後面是直譯器的絕對路徑,指令碼將用該直譯器執行。還有一種寫法是:#!/usr/bin/env bash/usr/bin/env 是 env 命令的絕對路徑,而 env 命令用於顯示系統中已存在的環境變數,其中包含了 $PATH ,會在 $PATH 包含的目錄依次找 bash,常見的命令列直譯器有:sh ,bash ,zsh(Mac OS 預設直譯器)

如果在 Linux 或 類Unix 下執行,有這麼幾種方式:

  1. 先給指令碼新增執行許可權:chmod +x test.sh,然後執行指令碼:./test.sh,這種方式執行會讀取 Shebang,用指定的直譯器執行指令碼
  2. sh test.sh,使用 sh 這個直譯器執行指令碼,當然也可以用其他執行,比如:bash test.sh。與第一種方式相同,當前的 shell 是父程式,生成一個子 shell 程式(子程式會繼承父程式的環境變數),在子 shell 中執行指令碼,指令碼執行完畢,退出子 shell 回到當前 shell
  3. source 點命令方式:source test.sh 等效於 . test.sh。source 讓指令碼在當前 shell 執行,不生成新的子程式。使用 source 執行指令碼,指令碼中對於環境變數的修改會作用於當前 shell,這就是為什麼我們在修改了一些配置如:~/.bashrc,執行 source ~/.bashrc 後配置就生效了
  4. exec 方式:有需要先給指令碼新增執行許可權:chmod +x test.sh,執行 exec ./test.sh,也是讓指令碼在同一個程式上執行不生成新的子程式,與 source 的區別就是,在指令碼執行完成後程式會被結束

基礎命令

可以按照 [Bash Shell] Shell學習筆記 學習,這篇文章講的非常詳細,本篇部落格也是在學習這篇文章後寫下的

獲取輸入

使用 read 命令,從標準輸入流 (stdin) 獲取輸入

#!/usr/bin/env bash
read var
echo "${var}"

執行指令碼,輸入任意字元,回車確認,輸入的值會賦值給變數 var,並列印出該變數

輸出

#!/usr/bin/env bash
var=1
# 輸出變數
echo ${var}
# 輸出字串 顯示部分字元需要轉義
echo "\"hello world\"" # "hello world"

# 換行使用 -e 引數:使轉義字元生效
# 使用 \n 換行
echo -e "newline\n"

也可以讓 shell 輸出不同顏色的字元,可以參考:shell指令碼中echo顯示內容帶顏色

#!/usr/bin/env bash
echo -e "\033[30m 黑色字 \033[0m" 
echo -e "\033[31m 紅色字 \033[0m" 
echo -e "\033[32m 綠色字 \033[0m" 
echo -e "\033[33m 黃色字 \033[0m" 
echo -e "\033[34m 藍色字 \033[0m" 
echo -e "\033[35m 紫色字 \033[0m" 
echo -e "\033[36m 天藍字 \033[0m" 
echo -e "\033[37m 白色字 \033[0m" 

變數使用

# = 兩邊不能有空格
var="hello world"
num=100


# 在引用變數時,這種方式可以,但是推薦下面一種
echo $var
# 推薦在使用字串變數時,在兩側加上雙引號,否則如果變數字串中存在空格,則字串會被切分
echo "$var"
# 如果涉及字串拼接,可以在變數名兩側加上花括號
echo "變數為: ${var}."

# 將變數設定為只讀,再次修改會報錯
readonly var
# var="wolrd"

# 刪除變數,不能刪除 readonly 修飾的變數
unset num

變數賦值時,變數名命名規則和其他語言類似,注意變數賦值時 = 兩邊不能有空格

使用時在變數名前加上 $,推薦所有的變數都使用 ${} 的方式使用變數

運算

算術運算:Bash 原生不支援數學運算,可以使用 awkexpr

注意乘號需要加上轉義:\*,而且運算子兩側必須空格

a=10
b=3
val=`expr $a + $b`
echo "a + b : $val"
val=`expr $a - $b`
echo "a - b : $val"
val=`expr $a \* $b`
echo "a * b : $val"
val=`expr $b / $a`
echo "b / a : $val"
val=`expr $b % $a`
echo "b % a : $val"

執行命令

$()與 ``(反引號)都可以用於執行命令,並會將執行的結果返回,shellcheck 推薦使用第一種 $() 的方式

#!/usr/bin/env bash
result=`date "+%Y-%m-%d"`
echo "${result}"

result=$(date "+%Y-%m-%d")
echo "${result}"

運算子

關係運算子只支援數字,如果字串為數字也可以,關係運算子包括:

運算子 含義
-eq 等於
-ne 不等於
-gt 大於
-lt 小於
-ge 大等於
-le 小等於

條件表示式必須放在 [] 中,並且 [ 的右側,和 ] 的左側必須留有空格

布林運算子列表:

運算子 含義
!
-o 或 (or)
-a 與 (and)
#!/usr/bin/env bash

a="10"
b="3"
c=1

if [ ${a} -ne ${b} ]
then
    echo "相同"
else
    echo "不相同"
fi

if [ ${a} -gt ${b} -a ${b} -gt ${c} ]
then
    echo "a > b & b > c"
fi

其他常用判斷:

  1. 直接在 [ ] 中放字串變數 如 [ ${str} ] 則就是判斷 str 這個字串是否非空
  2. -f 判斷是否為普通檔案,如:[ -f $file ]
  3. -d 判斷是否為資料夾,如:[ -d $file ]

字串擷取

字元擷取的格式:${string: start :length}
索引從 0 開始,可以省略 :length 這樣就擷取到最後,注意空格要空在 : 後,否則可能提示:bad substitution

#!/usr/bin/env bash
string="hello world"
echo ${string: 1 : 3} # ell
# 擷取到最後
echo ${string:1} # ello world

陣列

#!/usr/bin/env bash
# 1. 定義陣列:使用括號宣告,用“空格”分隔開,也可以換行隔開
arr=(1 2 3)
strArr=(
"first"
"second"
)

# 2. 讀取陣列:通過下標讀取,下標從 0 開始計算
echo "${arr[0]}"

# 使用 * 或者 @ 讀取所有元素
echo ${arr[*]}
echo ${arr[@]}

# 讀取陣列長度 讀取全部元素前面加上 #
echo ${#arr[*]}
echo ${#arr[@]}

# 遍歷下標
for(( i=0;i<${#strArr[@]};i++)) 
do
echo ${strArr[i]};
done;

# for in 遍歷元素
for element in ${strArr[*]}
do
echo $element
done

# 3. 修改陣列元素
strArr[0]="modify"
echo ${strArr[0]}

# 4. 刪除元素
unset arr[1]
echo ${#arr[*]}
echo ${arr[*]} # 1 3
# !使用 unset 要注意,這其實並不是真正刪除了該元素,而只是將該元素置空,所以使用下標遍歷會出問題,如下
echo "陣列遍歷:"
for(( i=0;i<${#arr[@]};i++)) 
do
echo "index ${i} -> ${arr[i]}";
done;
# index 0 -> 1
# index 1 -> 

# 解決 unset 無法真正刪除的方法:重新賦值給新的陣列
echo "陣列遍歷:"
arr=( "${arr[@]}" )
for(( i=0;i<${#arr[@]};i++)) 
do
echo "index ${i} -> ${arr[i]}";
done;
# index 0 -> 1
# index 1 -> 3

判斷語句

使用 iffi 定義判斷的邊界,使用 then , elif , else 定義條件

#!/usr/bin/env bash
#!/usr/bin/env bash

a=10
b=20
if [ $a == $b ]
then
    echo "相等"
else
    echo "不相等"
fi

if [ $a == $b ]
then
    echo "相等"
elif [ $a -lt $b ]
then
    echo "a 小於 b"
else
    echo "其他情況"
fi

函式

呼叫函式時,我們可以傳入引數,可以通過 $n 來獲取引數,這裡的 n 表示 需要取的引數的索引,當n>=10時,需要使用${n}來獲取引數

$# 傳遞給函式的引數個數,$*$@ 顯示所有傳遞給函式的引數,$? 表示函式的返回值,也可以用於獲取上一個命令的退出狀態,執行成功會返回 0,失敗返回 1

# 定義函式
#!/usr/bin/env bash
funWithParam(){
    echo "引數個數:$#"  # 引數個數:11
    echo "傳遞給函式的所有引數:$*" # 傳遞給函式的所有引數:1 2 3 4 5 6 7 8 9 34 73
    echo "$1" # 1

    # 超過 9 的引數需要用 ${} 接收引數,否則直接顯示數值
    echo "$10" # 10
    echo "${11}" # 73  
}

# 呼叫函式:函式名後面直接跟上引數
funWithParam 1 2 3 4 5 6 7 8 9 34 73
echo "$?" # 0

輸入輸出重定向

使用 > 將應該輸出到終端上的資料重定向輸出到檔案,> 預設為覆蓋檔案,使用 >> 追加寫入檔案
使用 < 將預設從鍵盤輸入的資料,定向為從檔案輸入

# who 命令用於顯示系統中有哪些使用者正在上面
# 將結果輸入 who.txt
who > who.txt

# wc -l 作用是計算文字行數
wc -l < who.txt

一般情況下,每個 Unix/Linux 命令執行時都會開啟三個檔案:

  1. 標準輸入 (stdin):stdin 的檔案描述符為 0,Unix 程式預設從 stdin 讀取資料
  2. 標準輸出 (stdout):stdout 的檔案描述符為 1,Unix 程式預設向 stdout 輸出資料
  3. 標準錯誤輸出 (stderr):stderr 的檔案描述符為 2,Unix 程式會向 stderr 流中寫入錯誤資訊

所以一般我們後臺啟動應用並且輸出日誌檔案都使用:

nohup java -jar xxx.jar >> nohup.log  2>&1 & 

nohup:(no hang up) 保證在退出帳戶或者關閉終端之後繼續執行相應的程式
>> nohup.log:將 java -jar xxx.jar 的輸出追加到 nohup.log 檔案
2>&1:將 java -jar xxx.jar 的 標準錯誤輸出 也重定向到 標準輸入
&:讓程式在後臺執行

預設情況下,command > file 將 stdout 重定向到 file,command < file 將stdin 重定向到 file。
如果希望 stderr 重定向到 file,可以這樣寫:

坑梳理

  1. 變數賦值時,變數名命名規則和其他語言類似,注意變數賦值時 = 兩邊不能有空格
  2. 陣列 unset 元素,並不是真正的移除元素
  3. 獲取引數時,當 n>=10 時,需要使用${n}來獲取引數

常見的特殊 Shell 環境變數

  • $$ 表示當前Shell程式的ID,即pid
  • $0 表示當前指令碼的絕對路徑
  • $# 傳遞給指令碼或函式的引數個數
  • $n 傳遞給指令碼或函式的引數
  • $? 上個命令的退出狀態
  • $*$@ 傳遞給指令碼或函式的所有引數
  • $n n 代表 1~9 其中任意一個數字,傳遞給指令碼或函式該位置的引數

$*$@ 區別:

#!/usr/bin/env bash
function asterisk () {
    echo "\"\$*\""
    for var in "$*"
    do
        echo "$var"
    done
}

function mail () {
    echo "\"\$@\""
    for var in "$@"
    do
    echo "$var"
    done
}
asterisk a b c 
mail a b c

輸出

"$*"
a b c
"$@"
a
b
c

$*$@ 直接使用效果相同,都是接收一份資料如上所示的例子,接收到的就是:a b c,一份資料,以空格隔開。加了雙引號後 "$@" 會將每個引數都當成一份獨立的資料

參考資料

VS code 打造 shell指令碼 IDE
#!/bin/bash 和 #!/usr/bin/env bash 的區別
Shell指令碼 - wiki
Linux跑指令碼用sh和./有什麼區別?
執行shell指令碼三種方法的區別:(sh、exec、source)
Shell特殊變數:Shell $0, $#, $*, $@, $?, $$和命令列引數
exec 跟 source 差在哪?
bash - 如何刪除陣列中的元素,然後在 Shell 指令碼中移動陣列?
nohup /dev/null 2>&1 含義詳解
Linux—shell中$(( ))、$( )、``與${ }的區別
Shell $*和$@的區別

相關文章