文章目錄
- The Missing Semester of Your CS Education 學習筆記以及一些拓展知識
- Bash腳本
- 筆記部分
- 一些在Bash腳本中的常用命令補充
- 常用標準輸入輸出命令
- 常用環境變量(普通變量)控制命令
- 常用系統時間信息獲取命令
- 常用函數執行狀態控制命令
- 常用腳本執行控制命令
- Bash腳本的創建和運行
- Bash腳本的SheBang
- Bash腳本的變量
- 變量的定義
- 變量的使用
- 變量的類型
- 局部變量
- 環境變量
- 基本內置變量(位置參數)
- 特殊內置變量
- Bash變量的三個特殊機制
- 數組
- 命令替換 (Command Substitution)
- 變量擴展(參數拓展)
- 變量使用時的注意事項
- Bash腳本的條件結構
- test命令
- test的基本功能
- test指令的三種形式
- test的常用測試表達式
- 組合條件
- test的使用基本注意事項
- let命令
- let的基本功能
- let支持的運算符
- let的改進寫法:雙括號算術運算 ((...))
- if-then結構
- if的三種結構
- if的判斷條件類型
- if的一些使用注意
- case結構
- case的基本結構
- case使用的一些注意事項
- &&和||
- Bash腳本的循環結構
- for循環
- for循環的兩種基本結構
- for循環的注意事項
- while循環
- while的基本語法:
- while的常見用法
- while的注意事項
- until循環
- 循環控制指令
- break
- continue
- Bash腳本的函數
- 函數的定義
- 函數的調用
- 函數的傳參
- 函數變量的作用域
- 函數的執行結果
- 總結函數的注意事項
- Bash腳本的Debug
- 語法檢查
- 腳本邏輯的debug
- 其他
- 習題部分
The Missing Semester of Your CS Education 學習筆記以及一些拓展知識
以下是使用The Missing Semester of Your CS Education的個人學習筆記,方便之后翻閱
Bash腳本
上一節我們學習了簡單的命令。假設有這么一種情況:我們希望每次開機后都執行一串相同的命令,難道說我們只能一個命令一個命令地在終端里面敲嗎?答案是我們可以把這些重復命令編寫成shell腳本,以下是二者的主要區別:
- 交互性 vs. 自動化
命令行是為交互而生的。你輸入一個命令,馬上就能看到結果,然后根據結果決定下一步做什么。這個即時反饋的循環對于文件管理、系統監控和問題排查至關重要。
腳本是為自動化而生的。它的設計初衷就是“一次編寫,多次運行”。對于那些需要重復執行的、由多個步驟組成的任務(例如:每日備份、部署網站、批量轉換文件),寫成腳本可以極大地提高效率并減少人為錯誤。 - 編程能力上,Bash腳本引入了非常多的語法結構,支持編寫復雜的邏輯功能。
- 運行環境的差別(非常重要)
命令行中的命令在當前 Shell 環境中執行。你在命令行里做的任何環境改變(如用 cd 切換目錄、用 export 設置環境變量),都會立即對你當前的 Shell 生效。
腳本默認在一個新的子 Shell (subshell) 中執行。這意味著腳本內部對環境的修改(如 cd、變量賦值)不會影響到執行它的那個父 Shell。
舉個例子:你在命令行里現在位于 /home/user。你有一個腳本 test.sh,內容是 cd /tmp。你執行 ./test.sh。腳本執行時,它的工作目錄確實切換到了 /tmp。但當腳本執行完畢,你回到命令行時,你所在的目錄仍然是 /home/user,而不是 /tmp。因為 cd 命令只在那個短暫存在的子 Shell 里生效了。
例外:如果你使用 source 命令(或其簡寫 .)來執行腳本,那么腳本會在當前 Shell 中執行,其內部的環境變量和目錄切換會保留下來。例如:source test.sh。
筆記部分
一些在Bash腳本中的常用命令補充
常用標準輸入輸出命令
命令 | 全稱 | 基本功能 | 常用參數 |
---|---|---|---|
read | read | 從標準輸入(stdin)中讀取一行文本,并將其賦值給一個或多個變量 | -p(prompt):在讀取輸入前,先顯示指定的提示信息。常用于交互式腳本。 -r (raw read):原始讀取模式。禁止反斜杠 \ 的轉義功能。 -s (silent):靜默模式。不將用戶輸入的內容顯示在屏幕上。非常適合用于輸入密碼或敏感信息。 -t <秒數> :設置一個超時時間。如果在指定秒數內用戶沒有輸入,read 命令會失敗并返回一個非零狀態碼。 -n <字符數> :讀取指定數量的字符后立即返回,而無需等待用戶按回車。 |
echo | echo(回聲) | 在標準輸出(stdout)上打印文本或變量內容。 | -n :輸出內容后不自動添加換行符。 -e :啟用對反斜杠轉義字符的解釋。 |
crul | See URL | 默認將從URL獲取的響應打印到標準輸出 | -s (silent):靜默模式。不顯示進度條和錯誤信息。在腳本中強烈推薦使用。 -L (Location):自動跟隨重定向。如果請求的 URL 返回一個3xx重定向,curl 會自動請求新的 URL。 -o <文件名> (output):將下載內容寫入指定文件,而不是標準輸出。 -O:將下載內容以 URL 中的原始文件名保存在當前目錄。 -I (Information):只獲取 HTTP 響應頭(HEAD請求)。 -X <方法> (e.g., POST, PUT, DELETE):指定 HTTP 請求方法 -H “<頭部信息>” (Header):添加自定義的 HTTP 請求頭。 -d “<數據>” (data):發送 POST 請求的數據體。 |
crul有一個姊妹命令wget(web get),與 curl 不同,它的默認行為是將內容保存到文件而非stdio。
常用環境變量(普通變量)控制命令
命令 | 全稱 | 基本功能 | 常用參數 |
---|---|---|---|
env | environment | 顯示所有環境變量或者進行一些環境變量臨時設置 | env: 直接執行,列出當前所有的環境變量,每行一個 變量名=值。 -i表示忽略當前的所有環境變量,在一個完全干凈的空環境中執行后續命令。 env <變量=值> <命令>: 臨時為 <命令> 設置一個環境變量,但不影響當前的 Shell 環境。 |
printenv | print environment | 打印環境變量的值 | printenv: 不帶參數時,行為與 env 幾乎完全相同,列出所有環境變量。 printenv <變量名>: 只打印指定單個環境變量的值。 |
export | export | 將一個變量“輸出”到環境中,用于創建新的環境變量,或者將一個已經存在的局部變量提升(或稱“導出”)為環境變量。 | export 變量名=值: 定義一個變量并立即將其導出為環境變量(最常用)。 變量名=值; export 變量名: 先定義一個局部變量,再將其導出。 -p: 打印出當前 Shell 中所有被導出的環境變量,其輸出格式是可被 Shell 重新執行的 export 變量名=“值” 形式。單獨執行 export -p 的效果與 env 類似。 -n: 將一個已存在的環境變量降級為一個局部變量,它將不再被子進程繼承 |
unset | unset | 從當前 Shell 環境中徹底刪除一個變量或函數 | -v (variable):明確表示要刪除的是一個變量(這是默認行為,通常可省略)。 -f (function):明確表示要刪除的是一個函數。 |
常用系統時間信息獲取命令
命令 | 全稱 | 基本功能 | 常用參數 |
---|---|---|---|
date | date | 獲取系統時間或者設置系統時間 | +Format:自定義日期形式,如:date +‘%Y-%m-%d %H:%M:%S’ -s 設置系統時間 -d 計算相對時間 |
關于獲取系統的硬件信息和系統信息之后再說吧
常用函數執行狀態控制命令
命令 | 全稱 | 基本功能 | 常用參數 |
---|---|---|---|
return | return | 立即終止當前正在執行的函數并為該函數設置一個退出狀態碼 ($?) | return [n],n 是一個可選的整數參數,范圍是0到255。如果省略n,函數的退出狀態碼將是函數中最后一條被執行的命令的退出狀態碼。 |
true | true | 不做任何事,返回狀態碼0 | 無 |
false | false | 不做任何事,返回狀態碼1 | 無 |
常用腳本執行控制命令
命令 | 全稱 | 基本功能 | 常用參數 |
---|---|---|---|
exit | exit | 立即終止整個腳本的執行,并可以為腳本設置一個最終的退出狀態碼 ($?)。 | exit [n]如果省略 n,則腳本的退出狀態碼是 exit 命令之前最后一條被執行的命令的退出狀態碼。 |
sleep | sleep | 讓腳本的執行暫停指定的一段時間。 | sleep <數字>[后綴],后綴可以是s、m、h、d |
shift | shift | 將參數列表向左“移動”一位。原來的$1會被丟棄(不可恢復),$2變成新的$1,$3變成新的 $2,以此類推。同時,參數總數 $# 的值也會減 1。 | shift [n],n 是一個可選的數字,表示要移動的位數。如果省略,默認為 1。 |
source | source | 在當前的Shell環境中讀取并執行一個腳本文件中的命令,而不是為該腳本啟動一個新的子 Shell。 | 無 |
exec | execute | 使用一個新的命令來替換當前的 Shell 進程。 | 無 |
關于更進一步的一些進程控制命令如kill、jobs、fg、bg之后再說吧
Bash腳本的創建和運行
還是HelloWorld程序起手:
touch helloworld.sh
nano helloworld.sh# 編寫如下內容,保存后退出
#!/bin/bash
echo "Hello World"chmod u+x helloworld.sh #更改權限
./helloworld.sh
從這個小小的例子可以看出:創建文件、編寫代碼、賦予權限、執行是所有 Bash 腳本開發的基礎流程。
Bash腳本的SheBang
Shebang,即#(sharp,但簡寫成she)+!(Bang)是位于腳本文件第一行開頭的一個特殊字符序列。它的作用是向操作系統(OS)的程序加載指明當用戶嘗試直接執行這個文件時,請不要把它當作普通的二進制文件,而是應該用我后面指定的這個解釋器來運行。
基本語法:
#!/path/to/interpreter [optional-argument]
- #!: 這兩個字符是固定不變的“魔術字符”,內核通過它們識別這是一個需要解釋器執行的腳本。
- /path/to/interpreter: 這是解釋器程序的絕對路徑。對于 Bash 腳本,它通常是 /bin/bash。
- [optional-argument]: 這是一個可選的參數,會傳遞給解釋器,一般情況下省略。
例如:
#!/bin/bash
#!/bin/sh
#!/usr/bin/python3
#!/usr/bin/node
以上分別是bash、通用shell、python和Node.js腳本的解釋器聲明。
如果你不寫 Shebang,直接運行 ./myscript.sh,那么你當前的 Shell(比如你正在使用的 Bash 或 Zsh)會嘗試去逐行解釋執行這個腳本,就可能會因為語法不兼容而出錯。
Bash腳本的變量
變量的定義
基本語法和示例
var=value# 字符串變量
greeting="Hello, World!"# 整數變量(本質上仍是字符串)
cnt=10
定義時注意事項
- 等號兩邊不能有空格! 這是初學者最常犯的錯誤。VAR = “value” 是錯誤的,Shell 會把 VAR 當成一個命令來執行。
- 變量名規范:通常由字母、數字和下劃線組成,不能以數字開頭。習慣上,環境變量和全局變量使用全大寫,腳本內部的局部變量使用小寫,以作區分。
- 無需聲明類型:Bash 中的變量默認都是字符串類型。即使你賦值一個數字 10,它本質上也是字符串 “10”,但在進行數學運算時,Shell 會自動進行轉換。
變量的使用
基本語法和示例:
$變量名 或者 ${變量名}
# 更推薦后者寫法name="Alex"
echo "My name is $name"
echo "My name is ${name}"
花括號 {} 是一種更嚴謹和安全的寫法,它可以明確變量名的邊界,避免歧義。看下面的例子:
fruit="apple"
echo "I have five ${fruit}s" # 正確輸出: I have five apples
echo "I have five $fruits" # 錯誤輸出: I have five (因為 Shell 試圖尋找一個叫 fruits 的變量,但它不存在)
變量的類型
在Bash中變量本質上都被視作字符串,所以下面分類依據是作用域和來源。
局部變量
在腳本中直接定義的變量,其作用域默認為整個腳本。如果在函數內部定義,為了防止污染外部作用域,應使用local關鍵字聲明。
#!/bin/bash
name="global"my_func() {local name="local" # 使用 local 關鍵字,此變量只在函數內有效echo "Function sees name as: $name"
}echo "Before function call, name is: $name"
my_func
echo "After function call, name is: $name" # 外部變量不受函數內部影響# 輸出
Before function call, name is: global
Function sees name as: local
After function call, name is: global
環境變量
這些變量對當前 Shell 會話以及由它啟動的所有子進程(包括你運行的腳本)都可見。它們通常用于配置系統環境。環境變量可以自己利用report工具定義,也有如下常見的內置環境變量:
環境變量 | 說明 |
---|---|
$HOME | 當前用戶的夾目錄 |
$PATH | 可執行文件的搜索路徑 |
$USER | 用戶名 |
$PWD | 當前工作目錄 |
$HOSTNAME | 系統主機名稱 |
$LANG | 系統語言和編碼 |
基本內置變量(位置參數)
這些變量由 Shell 自動賦值,用于獲取傳遞給腳本的命令行參數。
- $0: 腳本本身的文件名。
- $1, $2, $3…: 分別代表第 1、第 2、第 3 個參數。
- ${10}, ${11}… 當參數超過 9 個時,必須用花括號 {} 包裹。
- $#: 傳遞給腳本的參數總個數。
- $@: 代表所有參數的列表,每個參數都是獨立的字符串。在循環中,“$@” 是最常用和最安全的方式。
- $*: 代表所有參數組成的單個字符串。
#!/bin/bash
# 用法: ./backup.sh /path/to/source /path/to/destinationSOURCE_DIR=$1
DEST_DIR=$2echo "將從 '$SOURCE_DIR' 備份到 '$DEST_DIR'..."
cp -r "$SOURCE_DIR" "$DEST_DIR"
這里特別注意一下$@與$*的區別:
當不使用雙引號時,$@ 和 $* 的表現是一樣的。但一旦用雙引號包裹,它們的行為就截然不同。
- “$*”:會將所有參數視為一個整體的字符串。“param1 param2 param3”。
- “$@”:會將每個參數視為獨立的字符串。“param1” “param2” “param3”
例如腳本loop_test.sh:
#!/bin/bash
echo "--- 循環遍歷 \"\$*\" (單個字符串) ---"
for arg in "$*"; doecho "參數: $arg"
doneecho ""
echo "--- 循環遍歷 \"\$@\" (獨立字符串列表) ---"
for arg in "$@"; doecho "參數: $arg"
done## 執行
chmod +x loop_test.sh
./loop_test.sh "first arg" "second" "third"## 結果
--- 循環遍歷 "$*" (單個字符串) ---
參數: first arg second third--- 循環遍歷 "$@" (獨立字符串列表) ---
參數: first arg
參數: second
參數: third
在需要遍歷或傳遞參數列表時,99% 的情況下你都應該使用 “$@”。它能保證參數(即使包含空格)被正確、獨立地處理。
特殊內置變量
這些變量由 Shell 預設,用于提供腳本運行狀態的信息。
- $?: 上一個命令的退出狀態碼。這是腳本中進行錯誤檢查的最重要變量。0代表成功。非0代表失敗。
- $$:當前腳本的進程ID。常用于創建唯一的臨時文件名。
- $!: 上一個在后臺運行的命令的進程 ID。
cp source.txt dest.txt
if [ $? -eq 0 ]; thenecho "文件復制成功!"
elseecho "文件復制失敗!"
fi
Bash變量的三個特殊機制
數組
Bash 支持一維數組來存儲一組值。
- 定義數組: array=(“apple” “banana” “cherry”)
- 獲取元素: ${array[0]} (獲取第一個元素,索引從0開始)
- 獲取所有元素: ${array[@]}
- 獲取數組長度: ${#array[@]}
SERVERS=("server1.com" "server2.com" "server3.com")for server in "${SERVERS[@]}"; doecho "正在 ping 服務器: $server"ping -c 1 "$server"
done
命令替換 (Command Substitution)
這是一種非常強大的機制,允許你將一個命令的輸出結果賦值給一個變量。
基本語法和示例:
var=$(command) 或者 VAR=`command`# 獲取當前日期
TODAY=$(date +%Y-%m-%d)
echo "今天是: $TODAY"# 獲取 txt 文件數量
FILE_COUNT=$(find . -type f -name "*.txt" | wc -l)
echo "這里有 $FILE_COUNT 個 txt 文件。"
推薦使用()而非``,因為使用 $(…) 的好處是它可以輕松地嵌套,而反引號`嵌套起來非常麻煩和混亂。
變量擴展(參數拓展)
Bash 提供了強大的參數擴展功能,可以在不使用外部命令的情況下對變量進行處理。
-
提供默認值(提升代碼健壯性)
- ${variable:-default}: 如果 variable 未定義或為空,則返回 default,但不改變 variable 本身的值。
- ${variable:=default}: 如果 variable 未定義或為空,則返回 default,并同時將 default 賦值給 variable。
-
字符串操作
- ${#variable}: 獲取變量值的長度。
- ${variable#pattern}: 從開頭刪除最短匹配 pattern 的部分。
- ${variable##pattern}: 從開頭刪除最長匹配 pattern 的部分。
- ${variable%pattern}: 從結尾刪除最短匹配 pattern 的部分。
- ${variable%%pattern}: 從結尾刪除最長匹配 pattern 的部分。
- ${variable/pattern/string}: 替換第一個匹配的 pattern。
- ${variable//pattern/string}: 替換所有匹配的 pattern。
# 默認值
echo "未定義的用戶: ${UNSET_USER:-guest}" # 輸出:未定義的用戶: guest# 字符串操作
FILEPATH="/home/user/document.txt"
echo "文件名: ${FILEPATH##*/}" # 輸出:document.txt (刪除最長的前綴 */)
echo "目錄名: ${FILEPATH%/*}" # 輸出:/home/user (刪除最短的后綴 /*)
echo "擴展名: ${FILEPATH##*.}" # 輸出:txt
變量使用時的注意事項
- 永遠用雙引號包裹變量:使用$variable 或 ${variable} 時,請始終用雙引號包裹,即 “$variable”。這可以防止當變量值包含空格或特殊字符時,發生意想不到的“分詞”和“文件名擴展”問題。這是編寫健壯腳本的黃金法則。
- 賦值時等號兩邊無空格:再次強調,var=“value” 是正確的,其他形式都是錯誤的。
- 優先使用 ${}:使用 ${variable} 的形式可以使代碼更清晰,并避免變量名邊界的歧義。
- 檢查命令成功與否:在執行一個重要命令后,檢查 $? 的值,以確保腳本在出錯時能妥善處理,而不是繼續盲目執行。
- 區分環境變量和局部變量:使用大寫命名環境變量,使用小寫命名腳本內部的變量,可以提高代碼的可讀性。在函數內部盡量使用 local 關鍵字。
Bash腳本的條件結構
test命令
test的基本功能
test 是一個標準的、內建于 Shell 的命令。它的核心用途只有一個:計算一個表達式的真偽。
強調一下是表達式的真偽而非命令執行的情況
- 如果表達式為真 (true),test 命令執行成功,并返回退出狀態碼 0。
- 如果表達式為假 (false),test 命令執行失敗,并返回一個非 0 的退出狀態碼(通常是 1)。
它的輸出不是文本"true"或"false",而是通過退出狀態碼 ($?) 來報告結果。這正是if語句所需要的判斷依據。
# 檢查文件 /etc/hosts 是否存在 (這個文件通常存在)
test -f /etc/hosts
echo $? # 輸出: 0 (代表 true)# 檢查一個不存在的文件
test -f /nonexistent/file
echo $? # 輸出: 1 (代表 false)
test指令的三種形式
- test expression (經典形式)
這是 test 命令最原始的形態,現在已不常用,但了解它有助于理解本質。
if test -f "/etc/hosts"; thenecho "文件存在。"
fi
- [ expression ] (POSIX 標準形式)
是test命令的等效寫法,一個方括號 [。它在功能上與 test 完全等價。
if [ -f "/etc/hosts" ]; thenecho "文件存在。"
fi
注意:
-
[ 實際上是一個命令的名稱,和 test 一樣。
-
[ 后面和 ] 前面必須有空格! 這是強制性的語法要求。可以理解為 [ 文件存在嗎 /etc/hosts ],每個部分都是獨立的參數。[ -f …] 是錯誤的。
- [[ expression ]] (Bash 擴展形式,推薦使用)
在編寫 Bash 腳本時,強烈推薦使用它。它解決了 [ 的一些不足。
if [[ -f "/etc/hosts" ]]; thenecho "文件存在。"
fi
[[ … ]] 相比于 [ … ] 的優勢:
- 更安全的變量處理:在 [[ … ]] 中,即使變量包含空格或為空,不加雙引號通常也不會導致錯誤。而在 [ … ] 中,不加雙引號的變量是導致腳本錯誤的常見原因。盡管如此,為變量加上雙引號永遠是最佳實踐。
- 更直觀的邏輯運算:可以直接使用 && (與), || (或),而不是 -a, -o。
- 支持模式匹配和正則:可以用 == 和 != 進行通配符匹配(globbing),用 =~ 進行正則表達式匹配。
test的常用測試表達式
- 文件測試
這是腳本中最常見的測試類型,用于檢查文件或目錄的狀態。
表達式 | 描述 |
---|---|
-e <路徑> | exists - 路徑是否存在(文件或目錄均可)。 |
-f <路徑> | file - 路徑是否存在且為一個普通文件。 |
-d <路徑> | directory - 路徑是否存在且為一個目錄。 |
-s <路徑> | size - 文件是否存在且大小不為零。 |
-r <路徑> | readable - 文件是否存在且當前用戶可讀。 |
-w <路徑> | writable - 文件是否存在且當前用戶可寫。 |
-x <路徑> | xecutable - 文件是否存在且當前用戶可執行。 |
CONFIG_FILE="/etc/myapp.conf"if [[ -f "$CONFIG_FILE" && -r "$CONFIG_FILE" ]]; thenecho "正在讀取配置文件..."# ... 讀取文件內容 ...
elseecho "錯誤:配置文件不存在或不可讀!" >&2exit 1
fi
- 字符串測試
表達式 | 描述 |
---|---|
$str1" = “$str2” | 字符串內容是否相等。(== 在 [ 和 [[ 中效果相同) |
“$str1” != “$str2” | 字符串內容是否不相等。 |
-z “$str” | zero length - 字符串長度是否為零(即是否為空)。 |
-n “$str” | non-zero length - 字符串長度是否不為零(即是否非空)。 |
read -p "請輸入您的名字: " user_nameif [[ -z "$user_name" ]]; thenecho "錯誤:名字不能為空!"
elseecho "你好, $user_name!"
fi
- 整數測試
用于比較整數大小時,必須使用下面的專屬操作符。
表達式 | 描述 |
---|---|
$int1 -eq $int2 | equal - 是否相等。 |
$int1 -ne $int2 | not equal - 是否不相等。 |
$int1 -gt $int2 | greater than - 是否大于。 |
$int1 -ge $int2 | greater than or equal - 是否大于等于。 |
$int1 -lt $int2 | less than - 是否小于。 |
$int1 -le $int2 | less than or equal - 是否小于等于。 |
# 檢查傳入腳本的參數數量是否為 2
if [ "$#" -ne 2 ]; thenecho "用法: $0 <源文件> <目標文件>"exit 1
fi
組合條件
- 在 [ … ] 中(不推薦)
- -a: and (與)
- -o: or (或)
- !: not (非)
# 判斷 num 是否在 0 到 100 之間
if [ "$num" -ge 0 -a "$num" -le 100 ]; thenecho "有效范圍"
fi
- 在 [[ … ]] 中(推薦)
- &&: and (與)
- ||: or (或)
- !: not (非)
# 同樣是判斷 num 是否在 0 到 100 之間,可讀性更高
if [[ "$num" -ge 0 && "$num" -le 100 ]]; thenecho "有效范圍"
fi
test的使用基本注意事項
- 優先使用 [[ … ]]:在編寫 Bash 腳本時,只要不需要考慮兼容古老的 sh,就應該優先使用 [[ … ]],它更健壯、功能更強、語法更友好。
- 永遠用雙引號包裹變量:在進行字符串比較或文件測試時,務必將變量用雙引號 “” 包裹起來,即 [[ -f “file"]]和[["file" ]] 和 [[ "file"]]和[["str1” == “$str2” ]]。這可以防止當變量為空或包含空格時,test 命令解析出錯。
- 區分字符串和整數比較:比較字符串用 == 或 =,比較整數用 -eq, -gt 等。混用會導致意想不到的結果。
- test的結果是狀態碼:牢記 test 命令本身不產生任何可見輸出,它的價值在于其退出狀態碼 $?,if 和 while 語句正是依賴這個狀態碼來工作的。
let命令
let的基本功能
在 Bash Shell 中,變量默認是被當作字符串來處理的。這意味著你不能直接用常規的方式進行數學計算。let 命令的核心作用就是告訴Bash把接下來的表達式當作一個或多個整數算術運算來處理,而不是當作字符串。
let的基本語法結構和示例:
let <算術表達式1> [算術表達式2] ...a=10
b=5let result=a+b # 加法
echo $result # 輸出: 15
let result=a-b # 減法
echo $result # 輸出: 5
let result=a*b # 乘法
echo $result # 輸出: 50
let result=a/b # 除法 (整數除法)
echo $result # 輸出: 2
let result=a%b # 取余
echo $result # 輸出: 0
let表達式有一個突出的優點,那就是變臉引用可以不加上$:
a=10
b=5
let result=a+b # 推薦,更簡潔
let result=$a+$b # 也可以,效果相同
但let也有一個突出的缺點,如果你的表達式中包含空格,必須用引號將整個表達式引起來。
# 錯誤寫法,Shell 會把 =、5、+、3 當成多個獨立參數
let x = 5 + 3 # 正確寫法
let "x = 5 + 3"
echo $x # 輸出: 8
let支持的運算符
- 賦值:=
- 算術:+ (加), - (減), * (乘), / (除), % (取余), ** (冪運算)
- 自增/自減:var++ (后增), var-- (后減), ++var (前增), --var (前減)
- 復合賦值:+=, -=, *=, /=, %=
- 位運算:<< (左移), >> (右移), & (按位與), | (按位或), ^ (按位異或), ~ (按位非)
- 邏輯運算:! (邏輯非), && (邏輯與), || (邏輯或), , (逗號,用于分隔多個表達式)
- 三元運算符:?: (例如 let “a = 1 > 0 ? 1 : 0” )
let的改進寫法:雙括號算術運算 ((…))
這是目前最推薦的進行整數運算的方式。它是一個命令,也可作為一個表達式。
- 作為命令,進行賦值:(( 算術表達式 ))
- 作為表達式,獲取結果:$(( 算術表達式 ))
a=10
b=5# 1. 賦值
(( c = a + b * 2 ))
echo $c # 輸出: 20# 2. 獲取結果
result=$(( a + b ))
echo $result # 輸出: 15# 3. 在 if 語句中直接使用
if (( a > b )); thenecho "a 大于 b"
fi
注意:
- ((…))相比于let的巨大優勢:表達式內可以隨意使用空格,不需要引號;
- 變量引用無需$:和let 一樣,內部的變量無需 $ 前綴。
- 使用((…))處理整數判斷,進而替代[[…]]的一部分功能,使用起來更接近C語言,可以直接用于流程控制:if 和 while 語句可以直接使用雙括號進行判斷。
if-then結構
首先還是要強調if語句的核心本質:檢查命令的退出狀態碼。在很多編程語言(如 Python,Java)中,if 后面跟的是一個布爾值(true/false)。但在 Bash 中,if 的工作方式有本質不同:if 后面跟的是一個或多個 命令,它檢查的是這些命令執行后的 退出狀態碼 (Exit Status)。
# -q 選項讓 grep "安靜"執行,不輸出任何內容,只通過退出狀態碼報告結果
if grep -q "root" /etc/passwd; thenecho "文件中找到了 'root' 用戶。"
fi
if的三種結構
- 單分支
if <命令或條件>;then<要執行的代碼塊>
fi# 使用我們之前學過的 [[ ... ]] 作為判斷條件
if [[ -d "/var/log" ]]; thenecho "/var/log 是一個目錄。"
fi
- 雙分支
if <命令或條件>;then<條件為真時執行的代碼塊>
else<條件為假時執行的代碼塊>
fiif [[ "$USER" == "root" ]]; thenecho "當前用戶是 root,擁有最高權限。"
elseecho "當前用戶是 $USER,一個普通用戶。"
fi
- 多分支
if <條件1>;then<條件1為真時執行的代碼塊>
elif <條件2>;then<條件2為真時執行的代碼塊>
elif <條件3>;then<條件3為真時執行的代碼塊>
...
else<所有條件都為假時執行的代碼塊>
firead -p "請輸入你的分數 (0-100): " score
if (( score >= 90 )); thenecho "優秀 (A)"
elif (( score >= 80 )); thenecho "良好 (B)"
elif (( score >= 60 )); thenecho "及格 (C)"
elseecho "不及格 (F)"
fi
if的判斷條件類型
if后面可以跟任何“命令”,但最常用的是以下三種:
- 使用 test, [] 或 [[ … ]]
- 文件測試: if [[ -f “$file” ]]
- 字符串測試: if [[ “str1"=="str1" == "str1"=="str2” ]]
- 整數測試: if [[ “num1"?gt"num1" -gt "num1"?gt"num2” ]]
強烈推薦在 Bash 腳本中使用 [[ … ]],因為它更健壯、功能更強。
- 使用算術運算 ((…))
雙括號 ((…)) 用于整數算術運算。它同樣會返回一個退出狀態碼,這使得它可以非常直觀地用于 if 語句中的數字比較。
count=10
if (( count > 5 )); thenecho "count 大于 5。"
fi# 注意:(( 0 )) 會被認為是 false
if (( 0 )); thenecho "這句不會被打印"
elseecho "0 在 if 中被視為 false"
fi
- 使用任意普通命令
# 檢查網絡連通性
if ping -c 1 -W 1 "google.com" &> /dev/null; thenecho "網絡連接正常。"
elseecho "無法連接到外部網絡。"
fi
# &> /dev/null 的作用是將 ping 的所有輸出(標準和錯誤)都丟棄,我們只關心它的退出狀態碼。
if的一些使用注意
- 優先使用 [[…]] 和 ((…)):在編寫 Bash 腳本時:進行文件和字符串判斷,優先使用 [[ … ]]。進行整數判斷,優先使用 (( … ))。
- 變量引用要加雙引號 (“$var”):在 [[ … ]] 和特別是 [ … ] 中,對變量進行引用時,務必加上雙引號。這能防止當變量值包含空格或為空時,產生意想不到的錯誤。
case結構
和if的高度靈活不同,case語句是根據變量的不同取值來選擇分支的。
case的基本結構
case <變量> in<模式1>)<命令塊1>;;<模式2>)<命令塊2>;;<模式3> | <模式4>) # 模式3或模式4<命令塊3>;;*)<默認命令塊>;;
esac
- case <變量> in: 語句的開始。<變量> 通常是一個變量的引用,如 $action。in 是關鍵字。
- <模式n>): 每一個分支的匹配模式。模式后面的 ) 是必需的。
- <命令塊n>: 如果變量的值匹配了該模式,則執行這里的命令。
- ;; (雙分號): 這是 case 語句中至關重要的部分。它標志著一個命令塊的結束,其作用類似于其他語言中 switch 的 break。執行完命令塊后,遇到 ;; 就會直接跳到 esac,結束整個 case 語句。
- *): 這是一個特殊的通配符模式,可以匹配任何沒有被前面模式捕獲到的值。它通常作為默認分支,放在最后。
- esac: case 語句的結束標志 (case 的反寫)。
case的強大之處:它天然支持通配符匹配,靈活性很強:
- *: 匹配任意長度的任意字符序列。
- ?: 匹配任意單個字符。
- […]: 匹配方括號中任意一個字符。例如 [0-9] 匹配任意數字,[a-z] 匹配任意小寫字母。
- |: 或操作符,用于在一個分支中匹配多個模式
舉一些例子:
read -p "請輸入一個字符: " char
case "$char" in[a-z])echo "你輸入了一個小寫字母。";;[A-Z])echo "你輸入了一個大寫字母。";;[0-9])echo "你輸入了一個數字。";;?)echo "你輸入了一個特殊符號。";;*)echo "你輸入了多個字符。";;
esac
#!/bin/bash
# 用法: ./classify_file.sh document.pdffilename="$1"case "$filename" in*.jpg | *.jpeg | *.png | *.gif)echo "'$filename' 是一個圖片文件。";;*.tar.gz | *.zip | *.rar)echo "'$filename' 是一個壓縮包文件。";;*.sh)echo "'$filename' 是一個 Shell 腳本文件。";;*)echo "無法識別 '$filename' 的文件類型或它沒有擴展名。";;
esac
case使用的一些注意事項
- 別忘了 ;;:這是初學者最容易犯的錯誤。忘記寫 ;; 會導致“穿透”(fall-through),即匹配成功后,腳本會繼續執行下一個分支的命令塊,直到遇到 ;; 或 esac 為止。這種行為在絕大多數情況下都不是我們想要的,所以請務必在每個命令塊的末尾加上 ;;。
- 引用變量 (case “$var” in):在 case 語句中,總是用雙引號把要測試的變量包起來。這可以防止當變量為空或包含特殊字符時出現問題。
- *) 默認分支的重要性:強烈建議總是提供一個 *) 默認分支。這能讓你的腳本更健壯,可以優雅地處理所有未預料到的輸入,而不是靜默失敗或產生錯誤。
- 模式的順序:case 語句會從上到下依次匹配,一旦找到第一個匹配的模式,就會執行對應的代碼塊然后跳出。因此,應該將更具體的模式放在前面,更通用的模式(如 *)放在后面。
&&和||
我們知道&&和||可以在[[]]中用作邏輯判斷,但如果單獨使用它們也可以起到命令短路的作用。和if一樣,&&和||也是根據命令執行后的狀態碼來工作的:
基本語法:
command1 && command2 # 只有當command1成功執行(退出狀態碼為0)時,command2 才會被執行。
command1 || command2 # 只有當command1執行失敗(退出狀態碼為1)時,command2 才會被執行。
例如:
# 如果 mkdir 由于權限等問題失敗,cd 就不會執行,避免了進入錯誤目錄的風險
mkdir -p /var/data/new_project && cd /var/data/new_project# 嘗試 ping 谷歌,如果失敗(比如沒網),就打印錯誤信息
ping -c 1 google.com &> /dev/null || echo "錯誤:網絡連接中斷。"
二者還可以組合起來用,實現簡單的 if-then-else 邏輯:
command1 && command2 || command3# 執行邏輯:
# 執行 command1。
# 如果 command1 成功 (0),則執行 command2。
# 如果 command1 失敗 (非 0),則執行 command3。
但是,這種組合不完全等價于if-else。因為它的行為依賴于 command2 的退出狀態。看這個例子: true && false || echo “This gets printed”
true 執行成功。于是 && 后面的 false 被執行。false 命令執行失敗(退出狀態碼為1)。于是 || 后面的 echo 被執行了!這通常不是我們想要的結果。在 if/else 中,如果 true 成立,else 部分是絕對不會執行的。
對于復雜的邏輯判斷,使用標準的 if/then/else/fi 結構更清晰、更安全。
Bash腳本的循環結構
for循環
for循環的兩種基本結構
Bash 中的 for 循環主要有兩種風格:一種是傳統的列表式循環,另一種是類似 C 語言的數值計算循環。
- 形式一:列表式for循環。
這是Shell腳本中最經典、最常用、最靈活的for循環形式。
for <變量名> in <項目列表>;do<循環體代碼,使用 $變量名>
done
這樣形式的for循環使用起來靈活多變,以下是項目列表的多種形式
# 顯式列表(或數組)
for color in "red" "green" "blue"
doecho "當前顏色是: $color"
done# 范圍序列(花括號擴展)
for i in {1..10..2}
doecho "奇數: $i"打印 1 3 5 7 9
done# 文件名通配符 —> 這是極其常用的方式,用于處理當前目錄下的文件。
for file in *.txt
do# 引用變量時一定要加雙引號,以防文件名包含空格mv "$file" "${file}.bak"echo "已將 '$file' 重命名為 '${file}.bak'"
done# 命令替換
for user in $(cat user_list.txt)
doecho "正在為用戶 $user 創建家目錄..."
done
# 注意:這種方式對于包含空格或特殊字符的輸出處理不佳,容易出錯。對于逐行讀取文件內容,更健壯的方法是使用 while read 循環。# 腳本參數 ("$@")
echo "你傳入了 $# 個參數,它們是:"
for arg in "$@"
doecho "參數: $arg"
done
- 形式二:C語言風格for循環。
這種形式對于有其他編程語言背景的開發者來說非常熟悉,主要用于數值計算和控制循環次數。
for (( 初始化; 條件; 迭代表達式 ));do<循環體代碼>
done
舉個例子:
# 計算 1 到 100 的和
sum=0
for (( i=1; i<=100; i++ ))
do(( sum += i ))
done
echo "1 到 100 的總和是: $sum"
這種寫法語法直觀,類似C。并且在雙括號 ((…)) 內,變量引用可以不加$。
for循環的注意事項
- 引用循環變量時加雙引號:這是最重要的規則之一。在循環體中使用循環變量時,請務必用雙引號包裹,如 “$item”。這能防止因項目內容包含空格或特殊字符而導致命令執行失敗。
- 避免 for file in $(ls) 的寫法:這是一個非常經典的反面教材。ls 的輸出格式不穩定,且當文件名包含空格時,$(ls) 的結果會被“分詞”,導致循環出錯。直接使用文件名通配符 for file in * 是更安全、更高效的做法。
while循環
while 循環用于只要某個條件持續為真(命令返回退出狀態碼 0),就重復執行一段代碼塊。
while的基本語法:
while <條件命令> ;do<循環體代碼>
done
while 的條件命令和if非常像,<條件命令> 最常見的形式是 [[ … ]](測試條件)和 (( … ))(算術條件),但也可以是任何能返回退出狀態碼的命令。
while的常見用法
- 逐行讀取文件(王牌用法)
while IFS= read -r line
doecho "正在處理行: $line"
done < "filename.txt"
說明:
- read 是一個內建命令,用于從標準輸入讀取一行數據。當它成功讀到一行時,返回退出狀態碼 0;當它讀到文件末尾(EOF)時,返回非 0。這恰好完美契合 while 循環的機制。
- -r: (raw read)選項,防止 read 命令對反斜杠 \進行轉義。在處理文件路徑或包含特殊字符的行時,強烈建議總是加上 -r。
- line: 你定義的變量名,read 命令會將讀到的整行內容(除了行尾的換行符)賦值給這個變量。
- IFS 是“內部字段分隔符”的縮寫,Shell 默認用它來分割單詞(默認為空格、制表符、換行符)。在read命令前加上 IFS=(將其臨時設置為空),可以防止 read 命令修剪掉行首行尾的空格和制表符,保證讀到的 line 變量內容與文件中的行內容完全一致。這也是一個最佳實踐。
- < “filename.txt”:這是輸入重定向。它將 filename.txt 文件的內容作為整個 while 循環的標準輸入。這樣,read 命令就能從這個文件中逐行讀取數據。
# servers.list文件如下
google.com
github.com
# local.server
192.168.1.1#!/bin/bash
while IFS= read -r server
do# 跳過空行和注釋行if [[ -z "$server" || "$server" == \#* ]]; thencontinuefiecho "--- 正在檢查服務器: $server ---"ping -c 1 "$server"
done < "servers.list"
特別注意:不能使用管道代替重定向cat file.txt | while …; do …; done。使用管道時,| 右邊的 while 循環會在一個新的子 Shell中執行。這意味著在循環內部對變量做的任何修改,在循環結束后都會丟失。
- 無限循環 ->用于需要持續運行的服務、監控或主菜單程序。
# 使用 true 命令,它永遠返回 0
while true
doecho "系統正在運行... 按 [CTRL+C] 退出。"# ... 執行監控任務 ...sleep 5
done
while的注意事項
- 避免死循環:確保你的循環體內部有邏輯能最終改變 while 的判斷條件,使其變為假,除非你有意編寫無限循環。
until循環
until循環就是while循環的反意詞:until 循環會只要某個條件持續為假(命令返回非 0 退出狀態碼),就重復執行一段代碼塊。當條件第一次變為真(命令返回退出狀態碼 0)時,循環就會停止。
until的基本語法:
until <條件命令>;do<循環體代碼>
done
其等效為:
while !<條件命令>;do<循環體代碼>
done
! 是一個邏輯非操作符,它會反轉后面命令的退出狀態碼(0 變成 1,非 0 變成 0)。
循環控制指令
和C語言一樣,Bash也使用continue和break控制循環,并且功能類似。
break
基本語法:
break [n]
[n] 是一個可選的整數參數,代表要跳出的循環層數。如果省略,n 默認為 1,即跳出當前最內層的循環。
示例:
#!/bin/bash
TARGET_FILE="sshd_config"echo "開始在 /etc/ 目錄中搜索文件 '$TARGET_FILE'..."for file in /etc/*
do# basename 命令用于提取路徑中的文件名部分if [[ "$(basename "$file")" == "$TARGET_FILE" ]]; thenecho ""echo "找到了!文件路徑是: $file"break # 任務完成,立即跳出 for 循環fi# -n 讓 echo 不換行,制造一個動態的搜索效果echo -n "."sleep 0.05
doneecho ""
echo "搜索過程結束。"
continue
基本語法:
continue [n]
[n] 同樣是可選參數,代表要“繼續”的是第幾層循環。如果省略,n 默認為 1,即作用于當前最內層的循環。
#!/bin/bash
sum=0for i in {1..10}
do# (( i % 2 != 0 )) 判斷是否為奇數if (( i % 2 != 0 )); then# 如果是奇數,就跳過本次循環的剩余部分(即下面的 sum+=i)# 直接開始下一次循環(i會變成下一個值)continuefiecho "將偶數 $i 加入總和。"(( sum += i ))
doneecho "1到10之間所有偶數的和是: $sum"
注意:
- 對于 while true 或 until false 這樣的無限循環,break 是唯一的程序化退出方式(不包括 exit 或 kill)。必須在循環體內設計一個或多個 if 條件來觸發 break,否則循環將永不停止。
Bash腳本的函數
函數的定義
Bash 中,定義函數有兩種常見的方式,它們在功能上是等價的。
函數名() {<命令塊>
}# 或者function 函數名 {<命令塊>
}
在腳本中,函數必須先被定義,然后才能被調用。Shell 是自上而下解釋執行的,它需要先“學習”到函數的存在,之后才能執行它。
函數的調用
直接使用函數名即可:
#!/bin/bash
# 定義一個問候函數
greet() {echo "你好, 歡迎來到 Bash 函數的世界!"
}echo "準備調用函數..."
greet # 調用 greet 函數
echo "函數調用完畢。"
函數的傳參
函數內部處理參數的方式與腳本處理位置參數的方式完全相同。
例如:
- $1, $2, $3…: 代表傳遞給函數的第 1、2、3 個參數。
- $#: 代表傳遞給函數的參數總數。
- $@: 代表傳遞給函數的所有參數列表。
# 定義一個可以向特定人問好的函數
greet_someone() {# $1 指的是傳遞給 greet_someone 的第一個參數echo "你好, $1! 祝你有美好的一天。"
}# 調用函數并傳遞參數
greet_someone "Alice"
greet_someone "Bob"
函數變量的作用域
- 局部變量
在 Bash 中,默認情況下,你在任何地方(包括函數內部)定義的變量都是全局變量。想要使用局部變量需要local關鍵字,local 聲明的變量只在當前函數的作用域內有效,函數執行完畢后,該變量就會被銷毀。
#!/bin/bash
count=100 # 全局變量update_count() {local count=10 # 這是一個全新的、只屬于函數的局部變量(( count++ ))echo "函數內的 count: $count"
}echo "調用前, 全局 count: $count"
update_count
echo "調用后, 全局 count: $count" # 全局變量并未被修改# 輸出
調用前, 全局 count: 100
函數內的 count: 11
調用后, 全局 count: 100
- 引用變量
默認情況下,我們輸入給函數的參數在執行后不會影響原來的參數的,想要實現C語言中參數引用的效果,可以使用local -n關鍵字。
#/bin/bash
modify_var() {local -n ref=$1 # 引用變量名ref="新值"
}
value="舊值"
modify_var value
echo "$value" # 輸出: 新值# 輸出
新值 # 不加上-n輸出是"舊值"
函數的執行結果
- 返回狀態
return 命令用于設置函數的退出狀態碼 ($?),它和普通命令的退出碼作用一樣,用于表示函數執行的成功或失敗。
is_file_exist() {if [[ -f "$1" ]]; thenreturn 0 # 文件存在,返回成功elsereturn 1 # 文件不存在,返回失敗fi
}# 使用 if 來捕獲函數的退出狀態
if is_file_exist "/etc/hosts"; thenecho "狀態檢查成功:文件 /etc/hosts 存在。"
elseecho "狀態檢查失敗:文件 /etc/hosts 不存在。"
fi
- 返回數據:使用 echo 和命令替換
Shell 函數不能像其他語言那樣直接返回一個字符串、數組或其他復雜數據。
要從函數中獲取數據(比如一個計算結果或處理過的字符串),標準做法是:
在函數中,使用 echo 將結果打印到標準輸出;在調用函數的地方,使用命令替換 $(…) 來捕獲這個輸出,并賦值給一個變量。
get_timestamp() {# 將 date 命令的結果打印到標準輸出echo "$(date +%Y-%m-%d_%H:%M:%S)"
}# 使用命令替換來“接收”函數的返回值
log_prefix=$(get_timestamp)
echo "[$log_prefix] 應用程序已啟動。"## 輸出
[2025-07-16_17:11:05] 應用程序已啟動。
總結函數的注意事項
- 先定義后調用:始終確保在調用函數之前,它已經被 Shell 讀取和定義。通常的做法是將所有函數定義放在腳本的開頭。
- 函數內部多用 local:這是編寫高質量函數的黃金法則。除非你刻意要修改一個全局狀態,否則函數內的所有變量都應該用 local 聲明。
- return 用于狀態,echo 用于數據:清晰地區分這兩種“返回”方式。不要試圖用 return 來返回一個字符串(例如 return “some string” 是無效的)。
- 保持函數單一職責:一個好的函數應該只做一件事,并把它做好。這使得函數更容易理解、測試和復用。
- 傳遞參數使用 “@":如果需要將腳本收到的所有參數原封不動地傳遞給一個函數,請使用myfunction"@":如果需要將腳本收到的所有參數原封不動地傳遞給一個函數,請使用 my_function "@":如果需要將腳本收到的所有參數原封不動地傳遞給一個函數,請使用myf?unction"@”。
Bash腳本的Debug
語法檢查
- 使用shellcheck工具對Bash腳本進行語法檢測
腳本邏輯的debug
- 注釋大法,把可疑的問題代碼注釋一下確定問題來源
- 打印大法,對關鍵變量echo一下
- 把輸出重定向到log文件中處理
- 借助調試命令set、trap、strace。
- set命令
set 主要用來開啟“嚴格模式”和“追蹤模式”,讓腳本在出錯時立即失敗,或者清晰地展示出每一行代碼的執行情況。可以在腳本的任何地方使用 set -<選項> 來開啟,用 set +<選項> 來關閉。
- set -e :錯誤退出
作用:當腳本中的任何命令執行后返回非 0 的退出狀態碼時(即執行失敗),立即退出整個腳本。這能防止錯誤像滾雪球一樣越滾越大。例如,如果 cd到一個不存在的目錄失敗了,后續的 rm -rf * 就不會在錯誤的目錄下執行,避免了災難。
#!/bin/bash
set -eecho "準備創建一個不存在的目錄..."
ls /nonexistent_directory # 這個命令會失敗# 因為 set -e,下面這行代碼將永遠不會被執行
echo "腳本執行完畢。"
在某些情況下,set -e 不會立即退出,例如在 if 或 while 的條件判斷中,或者命令后面跟著 &&, || 時,因為 Shell 認為這些錯誤已經被“處理”了。
- set -u:未定義變量視為錯誤
當腳本嘗試使用一個未被定義的變量時,視為錯誤并立即退出。這能幫你捕捉到變量名的拼寫錯誤。
#!/bin/bash
set -uMY_NAME="Alex"
# 假設手誤,將 MY_NAME 寫成了 MY_NAM
echo "你好, $MY_NAM" # 這行會觸發錯誤,腳本退出
- set -x:執行追蹤
這是最常用的調試工具。在執行每一行命令之前,Shell 會先把它打印到標準錯誤輸出,并且是變量和通配符都已展開后的最終樣子。你可以清晰地看到每個變量在執行時的實際值,以及命令最終是以何種形態被執行的。
#!/bin/bash
set -x # 開啟追蹤USER_COUNT=$(who | wc -l)
echo "當前有 $USER_COUNT 個用戶登錄。"set +x # 關閉追蹤
echo "追蹤已關閉。"
- set -o pipefail:管道失敗
在管道命令中(如 cmd1 | cmd2),只要有任何一個命令失敗,整個管道的退出狀態碼就是失敗(非 0)。默認情況下,只有最后一個命令的退出碼才算數。保證了管道中任何一步的失敗都能被捕獲到。
這些選項可以組合在一起使用,set -euxo pipefail 是一個非常流行的“非官方嚴格模式”的開頭。
- trap 命令
介紹trap前需要先講一下Linux的信號。信號是操作系統與進程之間通信的一種方式。對于 trap,最關心的幾個信號是:
- EXIT: 腳本退出時觸發(無論正常退出、出錯退出還是被中斷)。這是最適合用于清理工作的信號。
- ERR: 當任何命令執行失敗(返回非 0 狀態碼)時觸發(set -e 可能會影響其行為)。
- INT: 當用戶按下 Ctrl+C 中斷腳本時觸發。
- TERM: 當腳本被 kill 命令(默認信號)終止時觸發。
trap的語法和示例
trap '<要執行的命令或函數>' <信號1> [<信號2> ...]#!/bin/bash
# 創建一個臨時文件,文件名包含進程ID以保證唯一性
TEMP_FILE="/tmp/my_app_temp_$$"
touch "$TEMP_FILE"# 定義一個清理函數
cleanup() {echo "捕獲到退出信號,正在執行清理..."rm -f "$TEMP_FILE"echo "臨時文件 '$TEMP_FILE' 已刪除。"
}# 設置 trap:無論腳本如何退出(正常、出錯、被中斷),都調用 cleanup 函數
trap cleanup EXITecho "腳本正在執行,臨時文件是 $TEMP_FILE"
echo "你可以嘗試用 Ctrl+C 中斷此腳本來測試 trap。"
sleep 20
echo "腳本正常結束。"# 當你運行這個腳本,無論你是等它自然結束,還是中途按 Ctrl+C,最后的清理函數總會被調用。
如果你想讓腳本在運行時不被 Ctrl+C 中斷,可以設置一個空的 trap。:
trap '' INT
echo "你無法用 Ctrl+C 中斷我..."
sleep 10
注意:trap 的位置:trap 命令應該放在腳本的開頭部分,以確保它能盡早生效。
- strace命令(初學者不推薦)
strace 用于追蹤一個程序在運行期間發生的所有系統調用和接收到的信號。
其他
其實還有一些交互式的debug工具,由于我們寫的腳本一般比較簡單,這里先略過。
習題部分
題目一:
ls -l -a -h -t --color=yes
題目二:
首先說明一下我的工作目錄:/home/michael/Documents/YSYX/Yuxuexi/StudyProj/2/homework
以下是我的腳本:
## macro.sh#/bin/bash
echo "$PWD" > ~/Documents/YSYX/Yuxuexi/StudyProj/2/homework/macro_dst.txt # 保存當前路徑## polo.sh#!/bin/bash
cat ~/Documents/YSYX/Yuxuexi/StudyProj/2/homework/macro_dst.txt # 先顯示一下保存的路徑
cd "$(cat ~/Documents/YSYX/Yuxuexi/StudyProj/2/homework/macro_dst.txt)" # 載入保存的路徑
在運行腳本的時候也需要注意:
export PATH="$PATH:$PWD" # 在工作目錄下運行
macro.sh # 在任意目錄下運行,保存路徑
source polo.sh # 在任意目錄下運行,并且使用source在當前shell中執行
值得說明的是答案的思路,它直接使用了source,并在marco下編寫這樣的腳本:
!/bin/bashmarco(){echo "$(pwd)" > $HOME/Documents/YSYX/Yuxuexi/StudyProj/2/homework/answer/marco_history.logecho "save pwd $(pwd)"
}polo(){cd "$(cat "$HOME/Documents/YSYX/Yuxuexi/StudyProj/2/homework/answer/marco_history.log")"
}
這樣使用時也很方便。我的方法麻煩地多,主要是因為我之前不知道source的機制,以為source只會讓腳本在當前shell執行,其實source還有其他功能: ?讓腳本中的變量、函數或環境變更直接影響當前 Shell 會話??。
題目三
我先將題干的代碼放在一個debug.sh的腳本中,然后在同一個目錄下寫下如下dedebug.sh的腳本:
#!/bin/bashi=0
echo "" > debug.txt #清空文件
while true; do(( i++ ))echo "***第${i}次執行***" >> debug.txt./debug.sh >> debug.txt 2>> debug.txtif [[ $? -eq 0 ]]; then #執行成功echo "現在共運行${i}次"elseecho "在第${i}次運行出現錯誤,現在打印報告"cat "./debug.txt"breakfi
done
題目四
find -name "*.html" -print0 | xargs -0 tar -cf html.tar
- find自帶遞歸
- print0用來防止文件中的空格
- xargs用來講輸出的文件轉成路徑參數給tar
- -0是配合-print0使用的
- -cf是用來產生.tar的常用命令
上面實現的效果其實有一些不足,就是它會把包好html文件的文件夾也一起壓縮,不是純粹的提取html文件
題目五
這一題沒太讀懂,想著不是使用ls -R -l -t就可以按時間列出文件了嗎,后來明白這一題只能輸出一行,并且只能是文件(不包含文件夾),看了答案如下:
find -type f -print0 | xargs -0 ls -l -t |head -1