文章目錄
- bash 概念與學習目的
- 第一個 bash 腳本
- bash 語法
- 變量的使用
- 位置參數
- 管道符號(過濾條件)
- 重定向符號
- 條件測試命令
- 條件語句
- case 條件分支
- Array
- for 循環
- 函數
- exit 關鍵字
- bash 腳本
- 記錄歷史命令
- 查詢文件
- 分發內容
bash 概念與學習目的
bash(Bourne Again Shell)是一種廣泛使用的 Unix/Linux Shell(命令行界面)。它是由 Brian Fox 為 GNU 項目開發的,是 Bourne Shell(sh)的增強版本,因此得名 “Bourne Again”。
Bash 是大多數 Linux 發行版(如 Ubuntu、CentOS)和 macOS 的默認 Shell,具有如 命令歷史記錄、管道、重定向、變量操作以及腳本編寫能力
相比于 bash , Python 或是 Ansible 腳本更易讀寫,一般是更好的選擇,但是學習 bash 只需要了解基礎知識就在可以在生產環境中有更多的選擇
bash 初級學習需要少量的 Linux command (vim、cat、echo等)與權限概念基礎
第一個 bash 腳本
- 創建一個 bash 腳本
vim bash-demo1.sh
- 編寫第一個 bash 腳本
腳本文件中寫入以下內容
#!/bin/bash
echo Hello World!
其中,第一行注解告訴 Linux 操作系統采用哪一個 bash 解釋器(默認選項,可省略),第二行會回顯輸出 Hello World
退出插入模式 :wq
(或是鍵盤大寫后按鍵 ZZ)保存退出
- 為腳本添加可執行權限
執行腳本首先需要先賦予腳本執行權限
ls -l
ls 命令的 long 格式查看腳本是否具有執行權限
可以看到 root用戶、root同組用戶、其他用戶 都沒有 x
- 執行權限
# 為當前用戶添加執行權限
chmod u+x bash-demo1.sh
# 為所有用戶添加執行權限
chmod +x bash-demo1.sh
作者的當前用戶為 root ,可以看到 root 權限欄出現 x 執行權限
前三個字母為 root 用戶讀寫執行權限,中間三個字母為 root同組用戶讀寫執行權限、最后三個字母為其他用戶的讀寫執行權限
- 執行 bash 腳本
./bash-demo1.sh
# 或是在不更改腳本執行權限的情況下
bash bash-demo1.sh
bash 語法
變量的使用
- 變量在路徑中的使用
此處編寫一個可用于文件復制的 bash shell
#!/bin/bash
cp /home/fishpie/workspace/bash-shell/tempdir01/demo1.txt /home/fishpie/workspace/bash-shell/tempdir02/
cp /home/fishpie/workspace/bash-shell/tempdir02/demo2.txt /home/fishpie/workspace/bash-shell/tempdir01/CPfile.txt
可以發現當文件路較長時可讀性很差,可以使用變量來解決這個問題
#!/bin/bash
# 變量
MY_LOCATION_FROM=/home/fishpie/workspace/bash-shell/tempdir01
MY_LOCATION_TO=/home/fishpie/workspace/bash-shell/tempdir02
# 顯示變量
echo $MY_LOCATION_FROM
echo $MY_LOCATION_TO
# 使用變量
cp "$MY_LOCATION_FROM/demo1.txt" "$MY_LOCATION_TO/"
cp "$MY_LOCATION_TO/demo2.txt" "$MY_LOCATION_FROM/CPfile.txt"
對于變量的使用(引用)需要使用到
$
推薦使用
""
包裹路徑,如果變量的值(比如 $MY_LOCATION_TO 或 $MY_LOCATION_FROM)包含空格,Bash 會將空格視為參數的分隔符,導致命令解析錯誤
- 讀取輸入的變量
讀取用戶輸入,比如常見的 yum 安裝軟件包過程中的 Y/n 用戶輸入等需要用戶交互的地方
讀取用戶輸入并設置為變量的關鍵字為 read
,此處編寫一個讀取用戶名稱的腳本
#!/bin/bash
# 提示用戶輸入名字和姓氏
echo What is your first name?
read FIRST_NAME
echo What is your last name?
read LAST_NAME# 提示用戶輸入性別選擇
echo "Are you male? (Y/n): "
read gender_choice# 根據用戶輸入判斷性別并顯示結果
if [ "$gender_choice" = "Y" ] || [ "$gender_choice" = "y" ]; thenecho "Name: $FIRST_NAME $LAST_NAME, Gender: Male"
elif [ "$gender_choice" = "N" ] || [ "$gender_choice" = "n" ]; thenecho "Name: $FIRST_NAME $LAST_NAME, Gender: Female"
elseecho "Invalid input! Please enter Y/y for male or N/n for female."
fi
位置參數
在 Linux command 中,所有命令的開頭(0號位置,$0
)是為 shell 本身保留,是腳本名稱或解釋器名稱
如果我們想將參數等信息通過 空格 傳入,則可以使用位置參數 $1
、$2
、$3
…
#!/bin/bashecho Hello $1 $2
管道符號(過濾條件)
管道符號 | 用于將一個命令的輸出傳遞給另一個命令作為輸入,是 Bash(及其他 shell)中的一種進程間通信機制
管道符號 | 會將左側命令的標準輸出(stdout)作為右側命令的標準輸入(stdin) (標準流)
- 標準輸出(stdout):命令執行后顯示在終端的結果
- 標準輸入(stdin):命令從外部接收的數據(比如鍵盤輸入或管道傳遞的數據)
管道的本質是:避免中間結果保存到文件,直接在內存中傳遞數據,提高效率
管道在內存中操作,效率高,但大量數據時可能會占用較多內存
右側的命令必須能從 stdin 接收輸入。例如,
ls | cp
是無效的,因為 cp 不支持從 stdin 讀取數據
示例:
# 查看本機網絡中的 80 端口服務
netstat -tunlp | grep ":80"# 統計系統中正在運行的進程數量
ps aux | wc -l# 找出占用 CPU 最多的前 5 個進程
ps aux | sort -k 3 -nr | head -n 5# 查看系統中所有用戶的唯一用戶名并排序
cat /etc/passwd | cut -d: -f1 | sort | uniq
重定向符號
重定向符號 >
和 >>
可以將命令的標準輸出(stdout)或標準錯誤(stderr)從默認的終端重定向到文件或其他地方
和管道符號 |
都是非常常用的符號
- > 將命令的輸出寫入指定文件,如果文件已存在,則覆蓋原有內容
- >> 將命令的輸出追加到指定文件末尾,如果文件已存在,則不覆蓋,而是在末尾添加內容
<
將文件內容作為命令的輸入
示例:
# 統計當前目錄中的所有 txt 文件
ls -l | grep "txt" >> txt_files.txt# 追加日期到日志文件
date +"%Y-%m-%d" >> log.txt# 清空文件
> something.txt# 統計文件行數
wc -l < file.txt# 將篩選出的 bash 進程信息保存到 processes.txt
ps aux | grep "bash" > processes.txt
- 進階用法
這里再次提到標準流
- 標準輸入(stdin,文件描述符 0):命令接收的數據,默認來自鍵盤
- 標準輸出(stdout,文件描述符 1):命令的正常輸出,默認顯示在終端
- 標準錯誤(stderr,文件描述符 2):命令的錯誤信息,默認也顯示在終端
示例1:
# 重定向標準錯誤,將標準錯誤重定向到標準輸出的位置
ls tempdir03/ tempdir05/ 2>&1 > output.txt
如果不加入 &1
參數
ls tempdir03/ tempdir05/ 2> output.txt
可見不會顯示錯誤信息
示例2:
在組合重定向時,順序很重要
ls > file.txt 2>&1 # 正確
ls 2>&1 > file.txt # 錯誤:stderr 不會進入 file.txt
條件測試命令
test
或者 [ ]
,空格是必須的,用于條件判斷,通常與 if、while 等流程控制語句搭配使用,來檢查文件狀態、比較數值或字符串等
[ ]
的正式名稱:test
命令,其實 [ ]
就是 test
命令的符號化
[ -f /etc/passwd ]
# 等價于
test -f /etc/passwd
echo $?
用于查看上一個命令的退出狀態,輸出 0 則為真,輸出1 則為 假
常見用法與分類
- 文件測試
測試選項 | 含義 | 示例 |
---|---|---|
-e | 文件是否存在 | [ -e /etc/passwd ] |
-f | 是否為普通文件(非目錄) | [ -f /etc/passwd ] |
-d | 是否為目錄 | [ -d /home ] |
-r | 是否可讀 | [ -r file.txt ] |
-w | 是否可寫 | [ -w file.txt ] |
-x | 是否可執行 | [ -x script.sh ] |
-s | 文件是否非空(大小大于 0) | [ -s log.txt ] |
# 判斷 /etc/passwd 是否為一個文件
if [ -f /etc/passwd ]; thenecho "passwd file exists and is a regular file"
fi
- 字符串比較
測試選項 | 含義 | 示例 |
---|---|---|
= 或 == | 字符串是否相等 | [ “$str” = “hello” ] |
!= | 字符串是否不相等 | [ “$str” != “hello” ] |
-z | 字符串是否為空 | [ -z “$str” ] |
-n | 字符串是否非空 | [ -n “$str” ] |
# 判斷字符串是否相等
name="Alice"
if [ "$name" = "Alice" ]; thenecho "Hello, Alice!"
fi
【注】:變量需要用雙引號 “$name” 包裹,避免變量為空時語法錯誤
例如:
var=""
[ $var = "" ] # 出錯,解析為 [ = "" ]
[ "$var" = "" ] # 正確
- 數值比較
測試選項 | 含義 | 示例 |
---|---|---|
-eq | 等于 | [ 5 -eq 5 ] |
-ne | 不等于 | [ 5 -ne 6 ] |
-gt | 大于 | [ 10 -gt 5 ] |
-lt | 小于 | [ 5 -lt 10 ] |
-ge | 大于等于 | [ 10 -ge 10 ] |
-le | 小于等于 | [ 5 -le 6 ] |
# 判斷年齡是否大于 18 歲
age=20
if [ "$age" -gt 18 ]; thenecho "You are an adult."
fi
- 邏輯運算
操作符 | 含義 | 示例 |
---|---|---|
-a | 與(AND) | [ -f file.txt -a -r file.txt ] |
-o | 或(OR) | [ “ a g e " ? g t 18 ? o " age" -gt 18 -o " age"?gt18?o"age” -eq 18 ] |
! | 非(NOT) | [ ! -d /tmp ] |
注意空格的使用!
# 判斷 file.txt 是否為普通文件,是否有寫權限
if [ -f file.txt -a -w file.txt ]; thenecho "file.txt exists and is writable"
fi
可能會注意到,如果遇到了需要正則表達式,如判斷一個變量是否符合某個格式
Bash 中還有一個增強版 [[ ]],功能更強大,支持**正則匹配(=~)**等,且對未定義變量更寬容
# 檢查 $var 是否為數字
[[ $var =~ ^[0-9]+$ ]]
# 正則匹配(=~)
條件語句
if
elif
else
,注意不是 elsif
與條件測試命令搭配使用
基本語法:
if [ 條件 ]; then# 條件為真時執行的代碼
elif [ 條件 ]; then# 上一個條件為假,此條件為真時執行的代碼
else# 所有條件都為假時執行的代碼
fi
此處編寫一個根據用戶輸入的數字判斷其范圍的腳本:
#!/bin/bashecho "Please enter a number: "
read numif [ "$num" -gt 0 ]; thenecho "$num is positive."
elif [ "$num" -lt 0 ]; thenecho "$num is negative."
elseecho "$num is zero."
fi
- 多條件組合
使用邏輯運算符(如 -a、-o)或 &&、|| 組合條件
# 判斷 85 分是一個什么樣的等級
score=85
if [ "$score" -ge 90 ]; thenecho "Grade: A"
elif [ "$score" -ge 80 ] && [ "$score" -lt 90 ]; thenecho "Grade: B"
elif [ "$score" -ge 70 ]; thenecho "Grade: C"
elseecho "Grade: D"
fi
- 嵌套結構
在 if
內部嵌套另一個 if
,特別注意腳本的結構問題
# 判斷 25 歲是一個什么樣的年齡
age=25
if [ "$age" -gt 18 ]; thenif [ "$age" -lt 30 ]; thenecho "Young adult"elseecho "Adult"fi
elseecho "Minor"
fi
- 正則表達式作為判斷條件
# 判斷 名字是否以 F 開頭
name="FISHPIE"
if [[ "$name" =~ ^F ]]; thenecho "Name starts with F"
elseecho "Name does not start with F"
fi
示例腳本:
- 用戶輸入輸出處理
#!/bin/bashread -p "Enter Y/N: " choice
if [ "$choice" = "Y" ] || [ "$choice" = "y" ]; thenecho "Yes"
elif [ "$choice" = "N" ] || [ "$choice" = "n" ]; thenecho "No"
elseecho "Invalid input"
fi
- 文件檢查
#!/bin/bashif [ -f "data.txt" ]; thenecho "File exists"
elif [ -d "data.txt" ]; thenecho "It's a directory"
elseecho "File does not exist"
fi
case 條件分支
case 語句是一種條件分支結構,類似于 if-elif-else,但它更適合處理多分支選擇,尤其是需要根據變量值匹配多個模式時
基本語法:
case 表達式 in模式1)# 匹配模式1時執行的代碼;;模式2)# 匹配模式2時執行的代碼;;*)# 默認情況(可選);;
esac
- case 表達式:指定要匹配的值(通常是變量)
- in:開始模式匹配部分
- 模式):定義匹配的模式,后面跟 )
- ;;:表示該分支的代碼結束,類似 break
- *****:通配符,表示默認分支(當沒有模式匹配時執行)
- esac:case 的結束標志(case 反過來)
對格式的要求相對嚴格
示例:
- 用戶輸入匹配
#!/bin/bashecho "Enter a fruit: "
read fruitcase "$fruit" in"apple")echo "You chose an apple.";;"banana")echo "You chose a banana.";;"orange")echo "You chose an orange.";;*)echo "Unknown fruit: $fruit";;
esac
- 多模式匹配與命令結合
#!/bin/bash# case 中可以直接匹配命令的輸出
case $(uname) in"Linux")echo "Running on Linux";;"Darwin")echo "Running on macOS";;*)echo "Unknown system: $(uname)";;
esac# 有限可寫條件中的一種
echo "Enter a day (mon/tue/wed/etc): "
read daycase "$day" in"mon"|"tue"|"wed"|"thu"|"fri")echo "Weekday";;"sat"|"sun")echo "Weekend";;*)echo "Invalid day: $day";;
esac
Array
數組(Array) 是一種數據結構,用于存儲多個有序的值(元素),這些值可以通過索引(下標)訪問,雖然不如其他編程語言那樣靈活強大,但也是 bash 中不可或缺的一部分
- 數組的使用
# 直接賦值
array_name=(value1 value2 value3)
# 逐個賦值
array[0]="value1"
array[1]="value2"
array[2]="value3"、
# 獲取數組長度
echo ${#array[@]}# 切片
array=(1 2 3 4 5)
echo ${array[@]:1:3} # 從索引 1 開始,取 3 個元素
for 循環
用于逐個處理一組數據(如列表、數組、文件等)。它特別適合在已知迭代次數或需要遍歷集合時使用
基本語法:
- 傳統派:
for 變量 in 列表; do# 執行的代碼
done
- 現代派:
在 Bash 3.0+ 中可以采用 C 語言風格編寫,類似 C/C++ 的 for (i=0; i<5; i++)
for (( 初始值; 條件; 步進 )); do# 執行的代碼
done
示例:
- 批量復制數組中存在的文件
#!/bin/bashfiles=("file1.txt" "file2.txt" "file3.txt")for file in "${files[@]}"; doif [ -f "$file" ]; thenecho "$file exists, copying..."cp "$file" "/tmp/"elseecho "$file does not exist"fi
done
- 通過直接遍歷命令結果來查找當前目錄下的 txt 文件
#!/bin/bashfor file in *.txt; doecho "Found TXT file: $file"
done
- 檢查多個主機是否在線
#!/bin/bashhosts=("192.168.1.1" "google.com" "8.8.8.8")for host in "${hosts[@]}"; doping -c 2 "$host" > /dev/nullif [ $? -eq 0 ]; thenecho "$host is online"elseecho "$host is offline"fi
done
ping -c 2 發送 2 個數據包,$? 檢查命令退出狀態
- 批量生成文件
#!/bin/bashfor (( i=1; i<=3; i++ )); dotouch "file$i.txt"echo "Created file$i.txt"
done#或是
for i in {1..3}; dotouch "file$i.txt"echo "Created file$i.txt"
done
【注】:遍歷列表中的元素如果帶有 空格 則該元素需要使用 " " 包裹**
循環之間的比較
循環類型 | 適用場景 | 示例 |
---|---|---|
for | 遍歷列表、數組、范圍 | for i in 1 2 3; do |
while | 條件不確定時 | while [ $x -lt 5 ]; do |
until | 條件為假時循環 | until [ $x -eq 0 ]; do |
函數
在 Bash 腳本中,函數(Function) 是一種將一組命令封裝成可重用代碼塊的方法。定義一個函數后,可以通過調用它的名稱來執行其中的代碼,并且可以傳遞參數給函數以增加靈活性
解耦合,模塊化,精簡代碼
基本語法:
- 使用
function
關鍵字
function 函數名 {# 函數體
}
- 精簡寫法
函數名() {# 函數體
}
示例:
#!/bin/bashgreet() {echo "Hello,$1! You are $2 years old,The time now is $(date +"%Y-%m-%d")"
}# 調用函數并傳遞參數
greet "Tom" 21
- 局部變量
默認情況,函數內的變量是全局的,可以用 local
關鍵字定義局部變量,避免污染外部環境
#!/bin/bashmy_function() {local name="$1" # 局部變量echo "Inside function: $name"
}name="Global"
my_function "Local"
echo "Outside function: $name"
- 返回值
- 當 Bash 函數沒有直接返回值,則可以通過
return
返回退出狀態(0~255,0表示成功)
#!/bin/bashcheck_number() {if [ "$1" -gt 0 ]; thenreturn 0 # 成功elsereturn 1 # 失敗fi
}check_number 5
if [ $? -gt 0 ]; thenecho "Positive number"
elseecho "Non-positive number"
fi
$? 獲取上一個命令(函數)的退出狀態
- 通過
echo
返回值
#!/bin/bashadd() {
# 在 (()) 內計算 $1 + $2 的和
# 通過 $() 將計算結果捕獲為字符串
# echo 將這個結果輸出echo $(($1 + $2))
}# = 是用于判斷字符串是否相同
# $() 將計算結果捕獲為字符串
result=$(add 3 4)
echo "Sum is: $result"
在 (()) 內部,可以直接進行數學運算(如加減乘除),無需額外的命令(如 expr)
$1 + $2 表示將函數的第一個參數 $1 和第二個參數 $2 相加
exit 關鍵字
只需要記住: exit 命令會立即結束當前腳本的執行,并返回一個狀態碼給調用它的環境
退出狀態碼:
- 0:表示成功(默認值)
- 非0:表示失敗或某種錯誤,通常由開發者定義具體含義
#!/bin/bashecho "Script starts"
exit
echo "This won't run"
- 帶狀態碼退出
#!/bin/bash# 檢查是否提供了參數
if [ $# -ne 1 ]; thenecho "Usage: $0 <filename>"exit 1
fi# 獲取傳入的文件名參數
filename="$1"echo "Checking file: $filename"
if [ -f "$filename" ]; thenecho "File exists"exit 0 # 成功退出
elseecho "File not found"exit 1 # 失敗退出
fi
$# 表示參數個數,-ne 1 表示 “不等于 1"
如果參數數量不對,提示用法并退出(狀態碼 1)
- exit 與 return 的區別
特征 | exit | return |
---|---|---|
作用范圍 | 終止整個腳本 | 僅退出當前函數 |
使用場景 | 腳本級別退出 | 函數級別返回 |
狀態碼 | 返回給父進程 | 返回給調用函數的地方 |
推薦文章:
Linux常用工具(LTS)_linux lts-CSDN博客
Linux手記(LTS)_linux lts-CSDN博客
bash 腳本
記錄歷史命令
#!/bin/bashLOG_DIR="$HOME/command_logs"
LOG_FILE="$LOG_DIR/command_history.log"if [ ! -d "$LOG_DIR" ]; thenmkdir -p "$LOG_DIR" || { echo "Error: Failed to create directory"; exit 1; }
fi# 確保歷史文件存在
HISTFILE=${HISTFILE:-"$HOME/.bash_history"}# 強制寫入當前會話歷史
history -atimestamp=$(date "+%Y-%m-%d %H:%M:%S")# 從 $HISTFILE 獲取前 50 條命令
commands=$(tail -n 50 "$HISTFILE")if [ -z "$commands" ]; thenecho "No commands found in history file $HISTFILE."exit 1
fiecho "Logging commands at $timestamp:" >> "$LOG_FILE"
echo "$commands" | while IFS= read -r cmd; doecho "[$timestamp] $cmd" >> "$LOG_FILE"
doneecho "Commands logged to $LOG_FILE"
exit 0
命令執行日志
cat ~/command_logs/command_history.log
查詢文件
根據指定模式查詢所有已連接服務器中包含 xx 內容或名稱的文件或目錄
#!/bin/bash# 檢查是否提供了搜索模式
if [ $# -lt 1 ]; thenecho "Usage: $0 <pattern> [content_search]"echo " <pattern>: File or directory name pattern (e.g., '*.txt')"echo " [content_search]: Optional, search for this string in file contents"exit 1
fi# 獲取參數
pattern="$1" # 文件或目錄名稱模式
content="$2" # 可選的內容搜索字符串# 定義已連接的服務器列表(假設通過 SSH 訪問)
servers=("server1.example.com" "server2.example.com" "server3.example.com")
# 替換為實際的服務器地址或從配置文件讀取# 本地搜索路徑(可根據需要修改)
search_path="/home/user"# 循環遍歷每個服務器
for server in "${servers[@]}"; doecho "Searching on $server..."# 如果沒有指定內容搜索,則只查找文件名或目錄名if [ -z "$content" ]; thenssh "$server" "find $search_path -name \"$pattern\"" 2>/dev/nullelse# 如果指定了內容搜索,則查找文件并檢查內容ssh "$server" "find $search_path -name \"$pattern\" -type f -exec grep -l \"$content\" {} +" 2>/dev/nullfi
doneecho "Search completed."
exit 0
$1:文件名或目錄名的模式(如 *.txt)
$2(可選):文件內容中要搜索的字符串(如 xx)servers 數組中列出目標服務器,需替換為實際地址
假設使用 SSH 訪問,需配置免密登錄
使用示例:
# 查找所有 .txt 文件
./search_servers.sh "*.txt"
# 查找包含 "xx" 的 .txt 文件
./search_servers.sh "*.txt" "xx"
分發內容
分發當前文件到指定服務器目錄,可選擇是否替換指定服務器原有的分發文件
#!/bin/bash# 檢查參數數量
if [ $# -lt 2 ]; thenecho "Usage: $0 <file> <dest_path> [replace]"echo " <file>: File to distribute"echo " <dest_path>: Destination directory on servers (e.g., /home/user/files)"echo " [replace]: 'yes' to replace existing files, omit or any other value for no"exit 1
fi# 獲取參數
file="$1" # 要分發的文件
dest_path="$2" # 目標路徑
replace="$3" # 是否替換(yes 或其他)# 檢查文件是否存在
if [ ! -f "$file" ]; thenecho "Error: File '$file' does not exist."exit 1
fi# 定義目標服務器列表
servers=("server1.example.com" "server2.example.com" "server3.example.com")
# 替換為實際服務器地址# 分發文件
for server in "${servers[@]}"; doecho "Distributing to $server..."# 檢查目標路徑是否已有同名文件if ssh "$server" "[ -f \"$dest_path/$(basename "$file")\" ]" 2>/dev/null; thenif [ "$replace" = "yes" ]; thenecho "Replacing existing file on $server..."scp "$file" "$server:$dest_path/" 2>/dev/nullelseecho "File exists on $server, skipping (use 'yes' to replace)."fielse# 文件不存在,直接分發scp "$file" "$server:$dest_path/" 2>/dev/nullif [ $? -eq 0 ]; thenecho "Successfully distributed to $server."elseecho "Failed to distribute to $server."fifi
doneecho "Distribution completed."
exit 0
$1:要分發的文件名
$2:目標服務器上的目錄路徑
$3(可選):yes 表示替換現有文件,否則跳過servers 數組中列出目標服務器,需替換為實際地址
假設使用 SSH 訪問,需配置免密登錄
使用示例:
# 分發文件,不替換已有文件
./distribute_file.sh myfile.txt /home/user/files
# 分發文件并替換已有文件
./distribute_file.sh myfile.txt /home/user/files yes