Bash 相關

Posted by Adam on August 24, 2022
### 以日期為檔名命名 ```bash #!/bin/bash # 獲取當前日期 current_date=$(date +%Y-%m-%d) # 檔名 filename="file_${current_date}.txt" # 使用檔名進行相應操作 echo "This is a sample file" > "$filename" date +%Y-%m-%d # 2023-07-07 date +%Y%m%d # 20230707 ``` --- # 函式 ```bash greet() { local name=$1 echo "Hello, $name!" } greet "Alice" # 傳遞參數 "Alice" 給函式 greet greet "Bob" # 傳遞參數 "Bob" 給函式 greet ``` 帶有預設值的參數 ```bash #!/bin/bash # 定義一個函式,帶有一個帶有預設值的參數 greet() { name="${1:-Guest}" # 如果沒有提供參數,使用 "Guest" 作為預設值 echo "Hello, $name!" } # 呼叫函式,不提供參數 greet # 呼叫函式,提供參數 greet "Alice" ``` --- # If 在bash中,if判斷式通常使用以下語法: ## 字串判斷 ``` str="hello" # 1. 判斷是否相等 if [[ "$str" == "hello" ]]; then echo "字串完全相同" fi # 2. 判斷是否不相等 if [[ "$str" != "world" ]]; then echo "字串不相同" fi filename="report_2026_draft.pdf" # 1. 判斷是否以 "report_" 開頭 (右側 * 不加引號) if [[ $filename == report_* ]]; then echo "這是 report 開頭的檔案" fi # 2. 判斷是否以 ".pdf" 結尾 (右側 * 不加引號) if [[ $filename == *.pdf ]]; then echo "這是 PDF 檔案" fi # 3. 判斷是否包含某字串 (前後加 *) if [[ $filename == *2026* ]]; then echo "檔名包含 2026" fi ``` ## 數值判斷 ### 方法一:雙括號語法 `(( ... ))`(最推薦) `(( ... ))` 是 `bash` 專門用來處理數學運算與比較的語法。最大的優點是可以**直接使用標準的數學符號** (`>`, `<`, `>=`, `<=`, `==`, `!=`),而且**變數前面不需要加 `$` 符號**。 ```bash age=20 # 1. 判斷是否大於等於 (>=) if (( age >= 18 )); then echo "已成年" fi # 2. 判斷是否等於 (==) if (( age == 20 )); then echo "剛好 20 歲" fi # 3. 判斷是否不等於 (!=) if (( age != 0 )); then echo "年齡不為零" fi # 4. 複合判斷:可以直接用 && 或 ||,且支援括號 if (( age >= 18 && age < 65 )); then echo "屬於勞動人口" fi ``` ### 方法二:條件判斷式 `[[ ... ]]`(傳統字母運算子) 如果習慣在 `[[ ... ]]` 裡面進行判斷,**不能**直接使用 `>` 或 `<`(因為在 `[[ ]]` 中 `>` 會被當作字串字典順序比較,而非數字比較)。必須使用英文縮寫的運算子: | 英文縮寫 | 全稱 | 數學符號 | 範例 | | --- | --- | --- | --- | | **`-eq`** | **Eq**ual | `==` | `[[ $a -eq $b ]]` | | **`-ne`** | **N**ot **E**qual | `!=` | `[[ $a -ne $b ]]` | | **`-gt`** | **G**reater **T**han | `>` | `[[ $a -gt $b ]]` | | **`-ge`** | **G**reater or **E**qual | `>=` | `[[ $a -ge $b ]]` | | **`-lt`** | **L**ess **T**han | `<` | `[[ $a -lt $b ]]` | | **`-le`** | **L**ess or **E**qual | `<=` | `[[ $a -le $b ]]` | **範例:** ```bash score=85 if [[ $score -ge 60 ]]; then echo "及格" fi if [[ $score -gt 80 && $score -le 100 ]]; then echo "優秀" fi ``` ### ⚠️ 重要陷阱:浮點數(小數點)問題 `bash` 原生**只支援整數運算**,不支援包含小數點的浮點數(例如 `3.14` 或 `85.5`)。若直接將小數傳給 `(( ... ))` 或 `-ge` 會跳出 `integer expression expected` 錯誤。 若要處理浮點數比較,需要呼叫外部命令 `bc`: ```bash num1=3.14 num2=2.71 # 利用 bc 進行判斷,bc 成立會回傳 1,不成立回傳 0 if (( $(echo "$num1 > $num2" | bc -l) )); then echo "$num1 大於 $num2" fi ``` --- # for Bash 的 `for` 迴圈主要有兩種常見寫法:**列表迭代(走訪項目)** 與 **C 語言風格(計數器)**。 ## 📌 核心知識 * **列表迭代**:依序讀取每一個項目(字串、檔名、陣列內容等)。 * **C 語言風格**:使用數字計數器,指定起始值、終止條件與遞增量。 ## 🔸 類似概念 * 列表迭代類似 Python 的 `for item in sequence:` * C 語言風格類似 C/C++ 或 Java 的 `for (int i = 0; i < n; i++)` ## 📝 範例說明 **範例 1:走訪固定清單(列表迭代)** 語法:`for 變數 in 項目1 項目2 ...; do 動作; done` ```bash # 印出水果名稱 for fruit in apple banana orange; do echo "水果名稱:$fruit" done ``` **範例 2:數字範圍(使用序列)** 語法:`for 變數 in {起始..結束}; do 動作; done` ```bash # 印出 1 到 5 的數字 for i in {1..5}; do echo "數字:$i" done ``` **範例 3:C 語言風格(雙括號計數器)** 語法:`for (( 初始值; 條件; 遞增 )); do 動作; done` ```bash # 從 1 數到 5,每次加 1 for (( i=1; i<=5; i++ )); do echo "當前計數:$i" done ``` **範例 4:處理目錄下的檔案** ```bash # 批次印出所有 .txt 檔名 for file in *.txt; do echo "找到文字檔:$file" done ``` --- # 串列 List In Bash 中,「串列」主要指 **陣列(Array)**。Bash 支援一維陣列,常用於儲存與處理多個資料項目。 ## 📌 核心知識 * **宣告與建立**:使用圓括號 `(項1 項2)`,項目之間以**空格**分隔(非逗號)。 * **讀取全部元素**:使用 `${陣列名[@]}`。 * **取的元素數量**:使用 `${#陣列名[@]}`。 * **索引取值**:使用 `${陣列名[索引]}`,索引從 0 開始。 ## 🔸 類似概念 * Bash 陣列就像 Python 的 `list` 或 JavaScript 的 `Array`,但 Bash 預設**沒有多維陣列**,且元素間嚴格以空白區隔。 ## 📝 範例說明 **範例 1:宣告陣列與讀取** ```bash # 宣告陣列 fruits=("apple" "banana" "orange") # 讀取單一元素(索引從 0 開始) echo "第一個水果:${fruits[0]}" # 輸出:apple # 讀取所有元素 echo "所有水果:${fruits[@]}" # 輸出:apple banana orange # 取得陣列長度(元素個數) echo "總共有 ${#fruits[@]} 個水果" # 輸出:3 ``` **範例 2:搭配 for 迴圈走訪陣列** ```bash servers=("web01" "db01" "cache01") # 方式 A:直接走訪元素 for server in "${servers[@]}"; do echo "正在連接至:$server" done # 方式 B:透過索引走訪 for (( i=0; i<${#servers[@]}; i++ )); do echo "第 $i 個伺服器是:${servers[$i]}" done ``` **範例 3:新增與修改元素** ```bash names=("Alice" "Bob") # 附加新元素到末端 names+=("Charlie") # 修改指定索引的值 names[1]="Bobby" echo "更新後的名單:${names[@]}" # 輸出:Alice Bobby Charlie ``` --- # 輸入 Input ```bash # 提示使用者輸入 echo "請輸入您的名字:" # 讀取使用者輸入並將其存入變數中 read name # 使用讀取到的值進行後續處理 echo "您輸入的名字是:$name" # 使用 Esc 作為結束字元 read -d $'\e' input ``` ```bash #!/bin/bash # 使用 read 命令接收輸入,如果使用者直接按下 Enter 鍵,則使用預設值 "John" read -p "Enter your name (default is John): " -r name name=${name:-John} # 顯示輸入值 echo "Your name is: $name" ``` ### 連續輸入 以下是一個擁有連續請使用者輸入功能的bash腳本: ```bash #!/bin/bash inputs=() while true; do read -p "請輸入內容 (按下Esc退出): " input # 檢查使用者是否按下Esc鍵 if [[ $input == $'\e' ]]; then break fi inputs+=("$input") done echo "輸入的內容為:" for item in "${inputs[@]}"; do echo "$item" done ``` 可以保存上面的程式碼到一個檔案(例如 `input_script.sh`),然後在終端中執行以下命令: ```bash bash input_script.sh ``` 使用者將可以連續輸入內容,直到按下Esc鍵。所有輸入的內容將被存入串列並顯示在終端中。 ### Here Document ```bash #!/bin/bash # 產生另一個 shell script # 創建或覆蓋新的腳本文件 new_script.sh cat << EOF > new_script.sh #!/bin/bash echo "This is the new script!" EOF # 給予可執行權限 chmod +x new_script.sh echo "New script file 'new_script.sh' generated!" ``` ### 資料夾或檔案 ```bash # 目前位置的所有檔案 for file in *; do # 如果是資料夾 if [ -d "$file" ]; then echo "$file is Folder" fi # 如果是檔案 if [ -f "$file" ]; then echo "$file is File" fi done ``` ### 檔名空白處理 ```bash filename=$(printf '%q' "$file") ``` ### 僅檔名(不要副檔名) ```bash filename_no_extension="${filename%.*}" ``` ### eval 在bash shell中,`eval` 是一個用於執行字串中包含的命令的指令。它會將字串視為一個命令並執行它。`eval` 指令通常用於將變數或其他字串中的命令執行起來。 例如,假設有一個變數包含一個命令: ``` command="ls -l" ``` 要執行這個命令,你可以使用 `eval` 指令: ``` eval $command ``` 這將會顯示目前目錄下的所有檔案和資料夾的詳細資訊。 要注意的是,使用 `eval` 指令時,必須非常謹慎,特別是當命令是來自不受信任的來源時,因為它可能會造成安全漏洞或意外的結果。 ```bash #!/bin/bash # 定義一個函式,接受命令字符串作為參數並執行它 run_command() { command_to_run="$1" echo "Running command: $command_to_run" eval "$command_to_run" } # 使用 run_command 函式來執行不同的命令 run_command "ls -l" run_command "echo Hello, World!" ``` ### Debug 在 bash 腳本中,使用 `set -x` 可以啟用 debug 模式,會讓腳本的每一行在執行時都會被顯示出來。這樣可以幫助你追蹤程式執行的過程,找出問題所在。 要在腳本中啟用 debug 模式,只需要在腳本的一開始加入以下行: ```bash set -x ``` 接著執行腳本時,你會看到每一行指令被顯示出來,方便你檢查程式的執行過程。 ```bash $ bash myscript.sh + echo 'Hello, world!' Hello, world! + echo 'This is a debug message' This is a debug message ``` 當你完成 debug 後,可以使用 `set +x` 來關閉 debug 模式: ```bash set +x ``` 這樣就可以關閉 debug 模式,腳本會恢復到正常執行狀態。 # While `while` 是一個迴圈結構,能夠不斷重複執行特定的指令,直到滿足某個條件為止。 以下是一個簡單的 `while` 迴圈的範例: ```bash #!/bin/bash count=1 while [ $count -le 5 ] do echo "Count is: $count" ((count++)) done ``` 在這個範例中,我們使用 `while [ $count -le 5 ]` 來設定迴圈的條件,即當 `count` 變數小於或等於 5 時,迴圈會不斷執行。在迴圈內部我們使用 `echo` 來輸出 `count` 的值,然後執行 `((count++))` 來將 `count` 變數加一。 當 `count` 變數等於 6 時,條件不再滿足,迴圈就會終止。 ### While true ```bash #!/bin/bash count=1 while true do echo "Count: $count" count=$((count+1)) if [ $count -eq 6 ] then break fi done ``` 這個腳本會不斷地印出 count 變數的值,直到 count 等於 6 時才會停止執行並結束迴圈。當 count 變數達到 6 時,便會觸發 if 條件式並執行 break 來跳出迴圈。 ### While 檔案讀取 在Bash腳本中,您可以使用while迴圈來讀取檔案的內容。以下是一個示例腳本,演示如何使用while迴圈從檔案中讀取每一行數據: ```bash #!/bin/bash # 請更換成您要讀取的檔案路徑 file="sample.txt" # 使用while迴圈讀取檔案內容 while IFS= read -r line do echo "$line" done < "$file" ``` 在這個示例中,我們將檔案路徑指定給變數file,然後使用while迴圈和read命令來一行一行地讀取檔案中的內容並進行輸出。注意,我們使用<運算符將檔案作為迴圈的輸入。 您可以將這個示例腳本保存為一個.sh檔,然後在終端中運行它,來讀取檔案的內容並進行處理。 ### 參數 在bash中,以下是一些特殊變數的含義和用法: 1. `$0`:代表目前執行的腳本名稱。 2. `$@`:代表所有參數的列表。 3. `$1`、`$2`、`$3`...:代表分別是第一個、第二個、第三個參數的值。 4. `$#`:代表參數的個數。 下面是一個示例腳本,假設腳本名稱為example.sh,有三個參數: ```bash #!/bin/bash echo "腳本名稱:$0" echo "所有參數:$@" echo "第一個參數:$1" echo "第二個參數:$2" echo "第三個參數:$3" echo "參數個數:$#" ``` 執行腳本時,可以透過以下方式傳入參數: ```bash ./example.sh arg1 arg2 arg3 ``` 輸出結果將如下: ``` 腳本名稱:example.sh 所有參數:arg1 arg2 arg3 第一個參數:arg1 第二個參數:arg2 第三個參數:arg3 參數個數:3 ``` ### 讀取檔案 你可以使用以下的 bash 腳本來迴圈列出檔案的每一行: ```bash #!/bin/bash file="example.txt" while IFS= read -r line; do echo "$line" done < "$file" ``` 在這個範例中,`example.txt` 是要處理的檔案名稱,`while IFS= read -r line; do` 是一個迴圈,使用 `read` 命令來讀取檔案的每一行,再將每一行的內容輸出至螢幕上。這樣就可以逐行輸出檔案的內容了。 IFS 是 Input Field Separator 的縮寫,可以用來設定用來切割讀取資料的分隔符號。在這個例子中,我們將 IFS 設定為空格,以便讀取包含空格的每一行。這樣可以確保讀取到完整的每一行,並避免遺失空格。 ### 隨機字串 ```bash #!/bin/bash randomString() { # 宣告字串亂數池 pool="abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890" # 要生成的亂數字串長度 length=$1 # 初始化結果字串 result="" # 使用迴圈從亂數池中隨機取出字元,直到達到指定長度 for (( i=0; i<length; i++ )); do # 取得隨機索引值 index=$((RANDOM % ${#pool})) # 取得隨機字元並加入結果字串 result="${result}${pool:index:1}" done # 輸出結果 echo $result } randomString 10 #ph0DQjVcM3 randomString 20 #09WdUYAZm5OxI5V0bZdQ randomString 30 #4yDkmgxdB73Cu3hj6VGvns0GnYGlS1 ``` ### 數字格式化 ```bash formatNumber() { num=$1 result=$(printf "%06d" $num) # 使用 printf 函式補零到6位數 echo $result } # 呼叫函式並輸出結果 result=$(formatNumber 123) echo $result ``` --- # 引號困惑 Bash 中的引號主要用來控制「字串邊界」**以及**「變數與特殊符號是否會被解析(替換)」。 ## 📌 核心知識 * **雙引號 `"..."`(弱引號)**:保持字串中的空格,但**允許**解析變數(`$VAR`)、命令替換(`$(cmd)`)與轉義符號(`\`)。 * **單引號 `'...'`(強引號)**:完全字面化,包在裡面的所有字元都會**原封不動**保留,不解析任何變數或符號。 * **不使用引號**:Bash 會以**空白**進行字詞分割(Word Splitting),且會觸發檔名展開(Wildcards/Globbing,如 `*`、`?`)與變數解析。 ## 🔸 類似概念 * 雙引號就像 Python 的 `f"Hello {name}"` 或 JavaScript 的 ``Hello ${name}``(會替換變數)。 * 單引號就像 Python 的 raw string `r"..."` 或純字串,變數寫進去只會被當成普通文字。 ## 📝 範例說明 **範例 1:變數解析與空格處理** ```bash name="World" # 1. 雙引號:會解析變數 $name echo "Hello $name" # 輸出:Hello World # 2. 單引號:不解析變數,寫什麼印什麼 echo 'Hello $name' # 輸出:Hello $name # 3. 不加引號:若變數含空格,傳給指令時會被拆成多個參數 msg="Hello World" echo $msg # 輸出:Hello World(多餘空格被吃掉了) echo "$msg" # 輸出:Hello World(完整保留空格) ``` **範例 2:檔名展開(通配符 `*`)** ```bash # 1. 不加引號:會展開目前目錄下的所有 .txt 檔名 echo *.txt # 輸出:file1.txt file2.txt ... # 2. 加單/雙引號:不會展開,直接輸出字詞本身 echo "*.txt" # 輸出:*.txt ``` **範例 3:何時一定要加雙引號?** 在處理檔案路徑或變數時,**隨時加雙引號**是避免程式出錯(如路徑包含空格)的最安全習慣: ```bash file_path="My Documents/notes.txt" # 錯誤示範(不加引號):cat 會把 "My" 和 "Documents/notes.txt" 當成兩個檔案 cat $file_path # 正確示範(加雙引號):完整傳遞帶空格的路徑 cat "$file_path" ``` ## 💡 快速使用建議 1. **優先用雙引號 `"$VAR"**`:日常寫腳本時,只要用到變數,一律養成用雙引號包起來的習慣,可預防 90% 以上因空格引發的 Bug。 2. **需要純文字用單引號 `'...'**`:當內容包含大量的 `$`、`\`、`!` 等符號,且不希望 Bash 自動幫你解析時使用(例如編寫 regex 正則表達式或印出程式碼範例)。 3. **只有純數字、固定無空格字詞時才不加引號**:例如 `count=1` 或 `cd dir_name`。 --- # Regex Bash 處理字串的 Regex(正則表達式)判斷與取值,主要採用 **`[[ ... ]]` 搭配 `=~` 運算子**,並透過 Bash 內建變數 **`BASH_REMATCH`** 來提取結果。 ## 📌 核心知識 * **判斷條件**:使用 `[[ "$str" =~ $pattern ]]` 進行正則表達式比對。 * **取值機制**:比對成功後,結果會存入陣列 `BASH_REMATCH`: * `${BASH_REMATCH[0]}`:代表整個比對到的完整字串。 * `${BASH_REMATCH[1]}`:代表第一個小括號 `()` 捕捉(Capture Group)到的內容。 * `${BASH_REMATCH[2]}`:代表第二個小括號捕捉到的內容,以此類推。 * **語法陷阱**:Pattern(正則表達式)**千萬不能加雙引號**,否則會被當作普通字串完全比對。 ## 🔸 類似概念 * 類似 Python 的 `re.search(pattern, string)` 與 `match.group(1)`。 * 類似 JavaScript 的 `string.match(regex)` 與取用索引陣列。 ## 📝 範例說明 **範例 1:Regex 條件判斷** ```bash email="user@example.com" # 判斷是否符合 Email 格式 if [[ "$email" =~ ^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$ ]]; then echo "這是有效的 Email 格式" else echo "無效的 Email 格式" fi ``` **範例 2:從字串中擷取數值(捕獲組 Capture Group)** ```bash log="[2026-08-18] ERROR: Connection failed (code: 404)" # 使用 () 捕捉日期與錯誤碼 pattern='\[([0-9]{4}-[0-9]{2}-[0-9]{2})\].*code: ([0-9]+)' if [[ "$log" =~ $pattern ]]; then echo "完整比對文字:${BASH_REMATCH[0]}" echo "擷取到的日期:${BASH_REMATCH[1]}" # 輸出:2026-08-18 echo "擷取到的代碼:${BASH_REMATCH[2]}" # 輸出:404 fi ``` **範例 3:將 Pattern 拆成變數(建議做法)** 將複雜的 Regex 存入變數後再使用,可以避免特殊字元轉義引發的語法錯誤: ```bash url="https://api.github.com/v1/users" # 建議將正則表示式存在變數中(注意:變數給予值時不需要加引號,或者正常雙引號) re_domain='https://([^/]+)' if [[ "$url" =~ $re_domain ]]; then echo "Domain 為:${BASH_REMATCH[1]}" # 輸出:api.github.com fi ``` ## 💡 注意事項 * 如果需要全域多重比對(例如尋找字串中「所有」出現的數字並全部取出),Bash 內建語法較難一次做完,此時建議搭配外部工具(如 `grep -oP`、`sed` 或 `perl`)。