Bash 迴圈是 shell scripting 的核心,能有效自動化重複性任務。我經常使用迴圈處理大量檔案、網路連線或自動化系統管理任務。本文將探討 Bash 迴圈、陣列和正規表示式的用法,並分享我的實戰經驗和獨到見解。

Bash 迴圈:自動化的利器

while 迴圈:條件為真,持續執行

while 迴圈的核心概念是:只要條件成立,就持續執行迴圈內的程式碼。我常用它讀取檔案中的主機名稱或 IP 地址,並執行特定操作。例如,某些滲透測試工具僅適用於單一主機,但我需要將其應用到多個主機。以下是一個使用 while 迴圈從檔案讀取 IP 地址的單行指令碼:

while read line; do python3 PetitPotam.py 10.2.10.99 $line; done < ips.txt

這個指令碼結合了幾個關鍵元素:

  • while read line: while 關鍵字確保迴圈持續執行,直到條件不成立。read 關鍵字從標準輸入 (stdin) 讀取一行資料,並將其指定給變數 line。檔案讀取結束時,read 傳回非零狀態,導致迴圈終止。
  • ;: 分號用於在同一行分隔多個命令。
  • do ... done: dodone 標記迴圈的開始和結束。
  • < ips.txt: 將 ips.txt 檔案的內容重新導向到標準輸入。

在執行 PetitPotam 命令之前,我在另一個終端使用 sudo responder -I eth0 執行了 Responder,這是一個偽造伺服器,用於從受害者那裡引出驗證資訊。請確保將 IP 地址替換為您自己的 IP 地址。

Responder 的輸出中,我捕捉了來自易受攻擊系統的密碼雜湊:

  graph LR
    A[攻擊者主機] --> B(Responder)
    C[受害者主機] --> B
    B --> A[捕捉密碼雜湊]

上圖展示了使用 Responder 從受害者主機捕捉密碼雜湊的過程。攻擊者主機執行 Responder,受害者主機嘗試連線,Responder 捕捉連線請求並記錄密碼雜湊,然後將雜湊值傳回攻擊者主機。

沒有 Bash while 迴圈,我必須手動為每個主機執行命令,在大型網路中非常耗時。

until 迴圈:條件為假,持續執行

until 迴圈與 while 迴圈相反,它持續執行直到條件成立。

#!/usr/bin/env bash
until [ -f done.txt ]; do
  echo "Waiting for done.txt..."
  sleep 1
done

這個迴圈會一直執行,直到 done.txt 檔案存在於當前目錄中,每秒檢查一次。 until 迴圈在特定情況下非常有用,例如等待某個檔案出現。

for 迴圈:迭代執行

for 迴圈用於迭代集合中的每個元素。

#!/usr/bin/env bash
for i in {1..5}; do
  echo "Number: $i"
done

這個迴圈會印出 1 到 5 的數字。for 迴圈常用於處理一系列檔案或目錄。

select 命令:互動式選單

select 命令適合在指令碼中建立互動式選單。

#!/usr/bin/env bash
echo "What's your favorite programming language?"
select lang in Python Bash Ruby "C/C++" Quit; do
  case $lang in
    Python) echo "Great choice! Python is versatile." ;;
    Bash) echo "Bash is great for shell scripting and automation!" ;;
    Ruby) echo "Ruby is used in the Metasploit Framework." ;;
    "C/C++") echo "C/C++ is powerful for system-level programming." ;;
    Quit) break ;;
    *) echo "Invalid option. Please try again." ;;
  esac
done

這個指令碼會顯示一個程式語言列表,並根據使用者的選擇執行相應的命令。select 命令會自動建立一個編號選單。每個選項都必須以兩個分號 (;;) 結尾。* 表示任何不符合先前選項的輸入。

巢狀迴圈與 break/continue:進階技巧

您可以巢狀使用迴圈,並使用 breakcontinue 關鍵字控制流程。

#!/usr/bin/env bash
for i in {1..3}; do
  for j in {1..3}; do
    echo -n "$i$j "
  done
  echo "" # New line after each row
done

這個指令碼會列印一個 3x3 的數字網格,展示了巢狀迴圈的運作方式。

break 命令會完全離開迴圈,而 continue 命令會跳過當前迴圈的其餘部分,並開始下一次迭代。

#!/usr/bin/env bash
for i in {1..20}; do
  if ! [[ $(($i%2)) == 0 ]]; then
    continue
  elif [[ $i -eq 10 ]]; then
    break
  else echo $i
  fi
done

在上面的例子中,for 迴圈迭代 1 到 20 的序列。如果 i 不是偶數,則使用 continue 跳到下一次迭代。如果 i 等於 10,則使用 break 離開迴圈。否則,列印 i 的值。

Bash 陣列:資料容器

Bash 陣列允許在單個變數中儲存多個值。

宣告與存取

my_array=(apple banana cherry)
echo ${my_array[0]}  # 輸出 apple

這行程式碼建立了一個名為 my_array 的陣列,包含三個元素:apple、banana 和 cherry。 使用 ${array_name[index]} 的語法存取陣列元素,索引從 0 開始。

修改與新增

my_array[1]=blueberry
my_array+=(date)

+= 運算元可以新增元素到陣列末尾。可以直接指定修改現有元素。

迴圈遍歷

#!/usr/bin/env bash
my_array=(apple banana cherry)
for fruit in "${my_array[@]}"; do
  echo "Fruit: $fruit"
done

此迴圈會印出陣列中的每個元素。${my_array[@]} 表示陣列的所有元素。

關聯陣列 (Associative Array)

Bash 也支援關聯陣列,每個元素由鍵而不是數字索引標識。

#!/usr/bin/env bash
declare -A my_assoc_array
my_assoc_array[apple]="green"
my_assoc_array[banana]="yellow"
for key in "${!my_assoc_array[@]}"; do
  echo "$key: ${my_assoc_array[$key]}"
done

使用 declare -A 宣告關聯陣列。${!my_assoc_array[@]} 取得關聯陣列的所有鍵。

  graph LR
    C[C]
A[宣告陣列] --> B(存取元素)
B --> C{修改元素}
C --> D[迴圈遍歷]
D --> E(關聯陣列)

此流程圖展示了 Bash 陣列操作的典型順序。

  classDiagram
class Array {
    +add(element)
    +get(index)
    +set(index, element)
}
class AssociativeArray {
    +put(key, value)
    +get(key)
}

這個類別圖描述了 Array 和 AssociativeArray 的基本操作。

正規表示式:精準的文書處理

正規表示式提供強大的模式比對工具。

常用元字元解析

以下是一些常用的元字元:

  • .: 比對除換行符以外的任意單個字元。
  • *: 比對前一個元素零次或多次。
  • +: 比對前一個元素一次或多次。
  • ?: 比對前一個元素零次或一次。
  • ^: 比對行的開頭。
  • $: 比對行的結尾。
  • []: 比對括號內的任意單個字元。
  • -: 在括號內表示範圍。
  • {n}: 比對前一個元素 n 次。
  • {n,m}: 比對前一個元素 n 到 m 次。
  • {n,}: 比對前一個元素 n 次或更多次。
  • (a|b): 比對 a 或 b。
  • =~: 比對運算元,常用於指令碼中。
  flowchart LR
    B[B]
    A[正規表示式] --> B{元字元}
    B --> C[點、星號、加號、問號、插入符、美元、方括號、減號、大括號、小括號、等號波浪號]

此圖表展示了正規表示式和其常用的元字元。

希望本文能幫助你提升 Bash scripting 技巧。 我是玄貓(BlackCat),持續分享我的技術見解。

parse_options() {
  while getopts ":a:b:c:" opt; do
    case $opt in
      a)
        option_a="$OPTARG"
        ;;
      b)
        option_b="$OPTARG"
        ;;
      c)
        option_c="$OPTARG"
        ;;
      \?)
        echo "Invalid option: -$OPTARG" >&2
        exit 1
        ;;
    esac
  done
}

# 使用範例
parse_options -a "value_a" -b "value_b" -c "value_c"

echo "Option a: $option_a"
echo "Option b: $option_b"
echo "Option c: $option_c"
  • parse_options() 函式使用 getopts 命令解析命令列引數。
  • getopts ":a:b:c:" opt 指定了可接受的選項 abc,後面的冒號表示這些選項需要引數。
  • case 陳述式根據不同的選項執行相應的操作。
  • OPTARG 變數包含當前選項的引數值。
  • \?) 處理無效選項的情況。

正規表示式、awk、以及 Bash 函式的應用,並提供實用的技巧和最佳實務。這些工具在文書處理、資料分析和系統管理等方面都扮演著重要的角色。熟練掌握這些工具,能大幅提升程式開發和系統管理的效率。

  graph LR
A[正規表示式] --> B(文書處理);
C[Awk] --> D(資料分析);
E[Bash 函式] --> F(系統管理);

在 Bash 程式設計中,函式是組織程式碼、提升程式碼重複使用性的關鍵。恰當的引數傳遞技巧能讓函式更具彈性,而理解變數範圍和生命週期則能避免許多常見的錯誤。我將結合自身經驗,分享一些 Bash 函式引數傳遞和變數範圍的最佳實務。

引數傳遞的藝術

Bash 函式可以接收多個引數,透過 $1, $2 等特殊變數來存取,分別代表第一個、第二個引數,以此類別推。$@ 代表所有引數,$# 代表引數的數量。

greet() {
  echo "您好,$1 $2!共有 $# 個引數:$@"
}

greet "玄貓" "讀者" "額外引數"

這個 greet 函式示範瞭如何使用 $1, $2, $#$@ 來處理多個引數。 函式會印出問候語,並顯示傳入的引數數量和所有引數值。

  graph LR
    B[B]
    A[呼叫 greet 函式] --> B{傳遞引數}
    B --> C[引數1, 引數2... 接收個別引數]
    B --> D[所有引數 接收所有引數]
    B --> E[引數數量 取得引數數量]
    C --> F[處理引數]
    D --> F
    E --> F
    F --> G[輸出結果]

圖表說明: 此流程圖展示了 Bash 函式接收和處理引數的流程,包含個別引數、所有引數以及引數計數的處理。

變數範圍與生命週期

Bash 中的變數有全域和區域之分。在函式外宣告的變數是全域變數,在函式內宣告的變數預設是全域變數,除非使用 local 關鍵字將其宣告為區域變數。區域變數只在函式內有效,函式執行結束後即被銷毀。

global_var="全域變數"

my_function() {
  local local_var="區域變數"
  echo "函式內:全域變數 = $global_var, 區域變數 = $local_var"
}

my_function

echo "函式外:全域變數 = $global_var, 區域變數 = $local_var" # $local_var 在此處不存在

這個例子展示了全域變數和區域變數的差異。local_var 僅在 my_function 內可見,而 global_var 在函式內外皆可見。

  stateDiagram
    [*] --> 全域變數: 宣告於函式外
    全域變數 --> 函式內: 可見
    全域變數 --> 函式外: 可見
    [*] --> 區域變數: 使用 local 宣告於函式內
    區域變數 --> 函式內: 可見
    區域變數 --> 函式外: 不存在

圖表說明: 這個狀態圖清楚地說明瞭全域變數和區域變數的生命週期和可見範圍。

實務應用:引數與變數的協奏曲

結合引數傳遞和變數範圍的概念,可以編寫更具彈性的函式。例如,一個計算檔案大小的函式:

calculate_file_size() {
  local file="$1"
  local unit="$2"

  local size=$(stat -c%s "$file")

  if [[ "$unit" == "KB" ]]; then
    size=$((size / 1024))
  elif [[ "$unit" == "MB" ]]; then
    size=$((size / 1024 / 1024))
  fi

  echo "$file 的大小為:$size $unit"
}

calculate_file_size "/path/to/file" "MB"

這個 calculate_file_size 函式接收檔案路徑和單位作為引數,使用 stat 指令取得檔案大小,並根據指定的單位進行轉換,最後輸出結果。 使用 local 關鍵字確保函式內的變數不會影響全域環境。

透過理解並善用 Bash 函式引數傳遞和變數範圍的機制,可以編寫更具結構性、可讀性和可維護性的 Bash 指令碼,提升程式碼品質和開發效率。