Bash 指令碼是 Linux 系統管理和自動化任務的重要工具。它將一系列命令列指令整合到一個檔案中,方便重複執行和管理。本文從命令列的基礎開始,逐步深入講解 Bash 指令碼的各個方面,包括變數、引數處理、條件判斷、迴圈控制、函式以及模式匹配等,並提供豐富的程式碼範例,幫助讀者理解和應用。對於想要提升 Linux 系統操作效率和自動化能力的工程師來說,Bash 指令碼的學習至關重要,本文提供了一個入門,幫助讀者快速上手並掌握 Bash 指令碼的核心概念和技巧。

從命令列到指令碼

shell 指令碼只是包含命令列指令的檔案。將一個或多個指令放入檔案中,就建立了一個 shell 指令碼。如果將檔案命名為 myscript,可以透過輸入 bash myscript 來執行該指令碼,或者賦予其執行許可權(例如,chmod 755 myscript),然後直接呼叫它來執行指令碼:./myscript

通常在指令碼的第一行加入以下內容,以告訴作業系統所使用的指令碼語言:

#!/bin/bash -

或者,為了提高可移植性,可以使用:

#!/usr/bin/env bash

內容解密:

  • #!/bin/bash -:指定使用 /bin/bash 來解釋執行指令碼。
  • #!/usr/bin/env bash:使用 env 命令查詢 bash 的位置,以提高可移植性。

練習題

  1. 編寫一個指令,執行 ifconfig 並將標準輸出重新導向到名為 ipaddress.txt 的檔案。

    ifconfig > ipaddress.txt
    

    內容解密:

    • ifconfig:顯示或組態網路介面引數。
    • > ipaddress.txt:將輸出重新導向到 ipaddress.txt
  2. 編寫一個指令,執行 ifconfig 並將標準輸出附加到名為 ipaddress.txt 的檔案。

    ifconfig >> ipaddress.txt
    

    內容解密:

    • >>:將輸出附加到指設定檔案的末尾。
  3. 編寫一個指令,將 /etc/a 目錄中的所有檔案複製到 /etc/b 目錄,並將標準錯誤重新導向到 copyerror.log 檔案。

    cp /etc/a/* /etc/b/ 2> copyerror.log
    

    內容解密:

    • cp /etc/a/* /etc/b/:將 /etc/a 中的所有檔案複製到 /etc/b/
    • 2>:將標準錯誤重新導向到指設定檔案。
  4. 編寫一個指令,對根目錄進行目錄列表(ls),並將輸出透過管道傳輸到 more 命令。

    ls / | more
    

    內容解密:

    • ls /:列出根目錄下的檔案和目錄。
    • |:管道符號,將前一個命令的輸出作為下一個命令的輸入。
  5. 編寫一個指令,執行 mytask.sh 並將其放到背景。

    ./mytask.sh &
    

    內容解密:

    • ./mytask.sh:執行當前目錄下的 mytask.sh 指令碼。
    • &:將任務放到背景執行。
  6. 給定以下任務列表,編寫指令將 Amazon ping 任務帶回前景:

    [1] Running ping www.google.com > /dev/null &
    [2]- Running ping www.amazon.com > /dev/null &
    [3]+ Running ping www.example.com > /dev/null &
    
    fg 2
    

    內容解密:

    • fg %2:將任務編號 2 帶回前景繼續執行。

Bash 簡介

Bash 不僅是一個簡單的命令列介面,用於執行程式;它本身也是一種程式語言。預設情況下,它用於啟動其他程式。當命令列上出現多個單詞時,bash 假設第一個單詞是要啟動的程式名稱,其餘單詞是要傳遞給該程式的引數。

輸出資訊

與任何程式語言一樣,bash 能夠將資訊輸出到螢幕。可以使用 echo 命令實作輸出:

$ echo "Hello World"
Hello World

也可以使用 printf 內建命令,它允許額外的格式化功能:

$ printf "Hello World\n"
Hello World

內容解密:

  • echo "Hello World":輸出字串 “Hello World”。
  • printf "Hello World\n":格式化輸出字串 “Hello World”,並在末尾新增換行符號。

如前一章所述,可以將輸出重新導向到檔案、stderr,或透過管道傳輸到另一個命令。接下來的章節中,將會看到更多這些命令及其選項的使用範例。

Bash 變數與引數處理

Bash 變數以字母或底線開頭,後面可接字母、數字或底線。預設情況下,這些變數被視為字串變數,除非特別宣告。指定給變數的方式如下:

MYVAR=textforavalue

要檢索變數的值,例如使用 echo 命令列印出來,需要在變數名稱前加上 $ 符號,像這樣:

echo $MYVAR

如果要將一系列單詞指定給變數,也就是保留空白字元,需要在值周圍使用引號,如下所示:

MYVAR='here is a longer set of words'
OTHRV="either double or single quotes will work"

使用雙引號會允許在字串內進行其他替換。例如:

firstvar=beginning
secondvr="this is just the $firstvar"
echo $secondvr

這將輸出 this is just the beginning

內容解密:

  1. 變數指定MYVAR=textforavalue 將字串 textforavalue 指定給 MYVAR
  2. 變數檢索echo $MYVAR 透過 $ 符號檢索並列印 MYVAR 的值。
  3. 引號的使用:單引號保留所有字元的字面意義,而雙引號允許變數替換。
  4. 命令替換:可以使用 $( ) 將命令的輸出儲存在變數中,例如 CMDOUT=$(pwd)

位置引數

在命令列工具中,通常透過引數傳遞資料到命令。Bash 透過特殊識別符號存取這些引數。在 Bash 指令碼中,第一個引數使用 $1 存取,第二個使用 $2,依此類別推。$0 儲存指令碼的名稱,而 $# 傳回引數的總數。

例子:echoparams.sh

#!/bin/bash -
# Cybersecurity Ops with bash
# echoparams.sh
# Description:
# Demonstrates accessing parameters in bash
# Usage:
# ./echoparms.sh <param 1> <param 2> <param 3>
echo $#
echo $0
echo $1
echo $2
echo $3

執行 ./echoparams.sh bash is fun 的輸出如下:

3
./echoparams.sh
bash
is
fun

內容解密:

  1. $#:傳回傳遞給指令碼的引數數量。
  2. $0:傳回指令碼的名稱。
  3. $1, $2, $3:分別傳回第一、第二和第三個引數。

使用者輸入

Bash 使用 read 命令接收使用者輸入,並將其儲存在指定變數中。下面的指令碼讀取使用者輸入到 MYVAR 變數並列印到螢幕上:

read MYVAR
echo "$MYVAR"

內容解密:

  1. read 命令:從標準輸入讀取使用者輸入。
  2. 變數儲存:將輸入儲存在 MYVAR 變數中。

條件判斷

Bash 提供豐富的條件判斷陳述式。許多條件陳述式以 if 關鍵字開頭。任何在 Bash 中執行的命令或程式都會傳回成功或失敗的值,這個值可以在命令執行後立即在 $? 變數中找到。

簡單的 if 陳述式形式:

if cmd
then
    some cmds
else
    other cmds
fi

例如,下面的指令碼嘗試切換目錄到 /tmp。如果該命令成功(傳回 0),則執行 if 陳述式的主體:

if cd /tmp
then
    echo "here is what is in /tmp:"
    ls -l
fi

Bash 甚至可以處理管道命令的成功或失敗:

if ls | grep pdf
then
    echo "found one or more pdf files here"
else
    echo "no pdf files found"
fi

內容解密:

  1. if 陳述式:根據命令的傳回值(0 為成功,非零為失敗)決定執行路徑。
  2. 管道命令:管道中最後一個命令的傳回值決定了整個管道的成功或失敗。

檔案測試運算元

Bash 可以使用 [[ ]][ ]test 命令測試檔案屬性或比較值。例如,測試檔案是否存在:

if [[ -e $FILENAME ]]
then
    echo $FILENAME exists
fi

常見的檔案測試運算元包括:

  • -d:測試目錄是否存在。
  • -e:測試檔案是否存在。

內容解密:

  1. [[ -e $FILENAME ]]:檢查 $FILENAME 是否存在。
  2. 檔案測試運算元:用於檢查檔案或目錄的屬性。

Bash 條件測試與迴圈控制

在 Bash 程式設計中,條件測試與迴圈控制是兩個非常重要的概念。條件測試允許程式根據特定條件執行不同的操作,而迴圈控制則允許程式重複執行某些操作。

檔案測試

Bash 提供了多種檔案測試運算元,用於檢查檔案的屬性,例如是否存在、可讀、可寫或可執行。

  • -r:測試檔案是否存在且可讀
  • -w:測試檔案是否存在且可寫
  • -x:測試檔案是否存在且可執行
if [[ -r /path/to/file ]]
then
    echo "檔案可讀"
fi

內容解密:

這段程式碼檢查 /path/to/file 是否存在且可讀。如果條件成立,則輸出 “檔案可讀”。這裡使用 [[ ]] 進行條件測試,-r 是測試檔案是否可讀的運算元。

數值比較

Bash 支援多種數值比較運算元,用於比較數值的大小或相等性。

  • -eq:測試兩個數值是否相等
  • -gt:測試一個數值是否大於另一個
  • -lt:測試一個數值是否小於另一個
VAL=10
MIN=5
if [[ $VAL -lt $MIN ]]
then
    echo "數值太小"
fi

內容解密:

這段程式碼比較 VALMIN 的大小。如果 VAL 小於 MIN,則輸出 “數值太小”。這裡使用 [[ ]] 進行條件測試,-lt 是測試數值是否小於的運算元。

使用雙括號進行數值比較

雙括號 (( )) 可以用於數值比較,與 C、Java 或 Python 等語言的語法相似。

VAL=10
if (( VAL < 12 ))
then
    echo "數值 $VAL 太小"
fi

內容解密:

這段程式碼檢查 VAL 是否小於 12。如果條件成立,則輸出 “數值 $VAL 太小”。在雙括號內,不需要使用 $ 符號來取得變數的值,除非是位置引數如 $1$2

邏輯控制

Bash 允許使用 &&|| 符號進行邏輯控制,根據命令的執行結果決定是否執行後續命令。

[[ -d $DIR ]] && ls "$DIR"

內容解密:

這段程式碼檢查 $DIR 是否為目錄。如果是,則列出目錄中的檔案。這等同於使用 if 陳述式進行相同的檢查。

使用大括號分組命令

當使用 &&|| 時,如果需要在條件成立或不成立時執行多個命令,可以使用大括號 {} 將這些命令分組。

[[ -d $DIR ]] || { echo "錯誤:目錄不存在:$DIR" ; exit ; }

內容解密:

這段程式碼檢查 $DIR 是否為目錄。如果不是,則輸出錯誤訊息並離開程式。大括號用於將多個命令分組,確保它們在相同的條件下執行。

迴圈控制

Bash 支援多種迴圈控制結構,包括 whilefor 迴圈。

While 迴圈

while 迴圈會持續執行,直到指定的條件不再成立。

i=0
while (( i < 1000 ))
do
    echo $i
    let i++
done

內容解密:

這段程式碼使用 while 迴圈輸出從 0 到 999 的數字。迴圈內部使用 let i++i 的值遞增。

For 迴圈

Bash 中的 for 迴圈有多種形式,包括使用雙括號進行數值迴圈和遍歷引數或任意列表。

for ((i=0; i < 100; i++))
do
    echo $i
done

內容解密:

這段程式碼使用 for 迴圈輸出從 0 到 99 的數字。迴圈語法與 C 或 Java 等語言相似,但需要使用雙括號。

###遍歷引數或列表

for VAL in 20 3 dog peach 7 vanilla
do
    echo $VAL
done

內容解密:

這段程式碼遍歷指定的列表,並輸出每個元素。列表可以是任意的字串或數字序列。

使用命令生成列表

for VAL in $(ls | grep pdf) {0..5}
do
    echo $VAL
done

內容解密:

這段程式碼首先使用 lsgrep 命令找出包含 “pdf” 的檔案名稱,然後遍歷這些名稱和數字序列 {0..5},輸出每個元素。這裡展示瞭如何將命令的輸出作為迴圈的輸入。

Bash 函式與模式匹配詳解

Bash 函式的定義與使用

Bash 函式是一種將多個命令組織在一起以執行特定任務的程式碼塊。定義一個函式的基本語法如下:

function myfun() {
    # 函式主體
}

在這個語法中,function 關鍵字是可選的,但使用它可以提高程式碼的可讀性。函式定義後,可以像執行普通命令一樣呼叫它,並傳遞引數。

函式引數處理

在函式內部,引數透過 $1$2 等變數來存取,這些變數會遮蔽指令碼原本的引數。因此,如果需要在函式記憶體取指令碼的原始引數,應在呼叫函式前將它們儲存在其他變數中。

#!/bin/bash

# 定義函式
function myfun() {
    echo "函式內引數數量: $#"
    echo "第一個引數: $1"
}

# 呼叫函式並傳遞引數
myfun "Hello" "World"

#### 內容解密:
1. `$#` 用於取得傳遞給函式的引數數量。
2. `$1` 用於存取第一個引數。
3. 函式內的變數預設為全域變數,除非使用 `local` 關鍵字宣告為區域性變數。

傳回值與輸出處理

函式可以透過傳回狀態碼或輸出結果來傳遞資訊。傳回狀態碼通常用於表示函式執行是否成功,而輸出結果可以用於取得計算值或路徑名等。

#!/bin/bash

# 定義函式
function compute() {
    local result=$(( $1 + $2 ))
    echo "$result"
}

# 呼叫函式並捕捉輸出
RETVAL=$(compute 10 20)
echo "計算結果: $RETVAL"

#### 內容解密:
1. `local` 關鍵字用於宣告區域性變數,避免影響全域變數。
2. `echo` 用於輸出計算結果。
3. `$( )` 用於捕捉命令的輸出結果。

Bash 中的模式匹配

Bash 提供了模式匹配功能,允許使用者透過模式指定一組檔案。最簡單的模式匹配符號是 *,它可以匹配任意數量和型別的字元。

基本模式匹配符號

  • *:匹配任意字元序列。
  • ?:匹配單個字元。
  • [ ]:匹配方括號內列出的任意一個字元。
# 列出當前目錄下所有 .txt 檔案
ls *.txt

# 列出 /usr/bin 目錄下以 g 開頭的檔案
ls /usr/bin/g*

#### 內容解密:
1. `*` 用於匹配任意字元序列,例如 `*.txt` 匹配所有以 `.txt` 結尾的檔案。
2. `?` 用於匹配單個字元,例如 `source.?` 可能匹配 `source.c``source.o`3. `[ ]` 用於匹配特定字元集合,例如 `x[abc]y` 匹配 `xay``xby``xcy`

字元類別

Bash 支援使用字元類別進行模式匹配,這需要在方括號內使用特定的類別名稱。

# 列出包含標點符號並以 .jpg 結尾的檔案
ls *[[:punct:]]*.jpg

#### 內容解密:
1. `[[:punct:]]` 用於匹配標點符號。
2. 字元類別需要放在方括號內,例如 `[[:punct:]]`3. 這種模式將匹配包含標點符號且以 `.jpg` 結尾的檔案名。