在開發的過程中,經常需要處理一些重複的工作,或者邏輯相當簡單但耗時的功能,這時我們可能會考慮到用腳本來自動化完成這些工作。而 Bash 腳本是我們最容易接觸到和上手的腳本語言。
這篇博客匯總一些常用的 Bash 語法,方便日後查閱學習。
不管寫啥,上來先輸出個hello world。
#!/bin/bash
echo "hello world"創建一個文件hello.sh 包含以上內容,同時賦予執行權限,然後執行,一個hello world 就好了。
# 添加執行權限
$ chmod +x hello.sh
$ ./hello.sh
hello world
解釋器我們看到這個hello.sh 腳本,第一行有個 #!/bin/bash 。這個是用來指定該腳本在 UNIX/Linux 下執行時用到的解釋器。
執行cat /etc/shells 我們可以看到自己的系統中都有哪些解釋器。如我的:
$ cat /etc/shells
# /etc/shells: valid login shells
/bin/sh
/bin/bash
/usr/bin/bash
/bin/rbash
/usr/bin/rbash
/bin/dash
/usr/bin/dash
/bin/zsh
/usr/bin/zsh
/usr/bin/tmux
注釋用 # 來注釋。
#!/bin/bash
# 這是注釋
echo "hello world"
變量聲明Bash 中變量命名是大小寫敏感的,很多人喜歡全大寫。當然你也可以使用 小寫英文字母,數字和下劃線,但不能以數字開頭。給變量賦值的時候 = 號前後不能有空格。
# 有效的
FIRSTLETTERS="ABC"
FIRST_THREE_LETTERS="ABC"
firstThreeLetters="ABC"
MY_SHELL="bash"
my_another_shell="my another shell"
My_Shell="My shell"
_myshell="My shell"
# 無效的
3LETTERS="ABC"
first-three-letters="ABC"
first@Thtree@Letters="ABC"
ABC = "ABC "
MY_SHELL = "bash"
My-SHELL="bash"
1MY_SHELL="My shell"
變量引用當你要使用變量的時候,用 $ 來引用, 如果後面要接一些其他字符,可以用{} 括起來。
#!/bin/bash
WORLD="world world"
echo "hello $WORLD" # hello world world
echo "hello ${WORLD}2" # hello world world2
在 Bash 中要注意 單引號 ' , 雙引號 " ,反引號 ` 的區別。
單引號,雙引號都能用來保留引號內的為文字值,其差別在於,雙引號在遇到 $(參數替換) , 反引號 `(命令替換) 的時候有例外,單引號則剝奪其中所有字符的特殊含義。
而反引號的作用 和 $() 是差不多的。在執行一條命令的時候,會先執行其中的命令,再把結果放到原命令中。
#!/bin/bash
var="music"
sports='sports'
echo "I like $var" # I like music
echo "I like ${var}" # I like music
echo I like $var # I like music
echo 'I like $var' # I like $var
echo "I like \$var" # I like $var
echo 'I like \$var' # I like \$var
echo `bash -version` # GNU bash, version 5.0.17(1)-release (x86_64-pc-linux-gnu)...
echo 'bash -version' # bash -version
環境變量Linux 的環境變量包含了存儲在系統中的信息。我們可以在終端中找到一些環境變量。
$ env
# 或
$ printenv你可以在腳本中引用這些環境變量。
#!/bin/bash
echo $SHELL, $USER, $HOME
# /usr/bin/zsh, razeen, /home/razeen
內部變量Bash 的內部變量也不少,有時我們可能會用到,如 $BASHPID $IFS $PWD 等。
將命令輸出分配給變量可以使用 $(command) 將命令輸出存儲在變量中。例如這是一個info.sh腳本內容:
#!/bin/bash
LIST=$(ls -l)
echo "File information: $LIST"執行(別忘了給執行權限)
$ ./info.sh
File information: total 8
-rwxrwxr-x 1 razeen razeen 85 2月 5 07:35 hello.sh
-rwxrwxr-x 1 razeen razeen 58 2月 5 07:36 info.sh
下面的腳本會將時間和日期,用戶名以及系統正常運行時間保存到日誌文件中。
其中 > 是重定向之一,它將覆蓋文件。使用 >> 可以將輸出追加到文件。
#!/bin/bash
DATE=$(date -u) # UTC 時間#!/bin/bash
DATE=$(date -u) # UTC 時間
WHO=$(whoami) # 用戶名
UPTIME=$(uptime) # 系統運行時間
echo "Today is $DATE. You are $WHO. Uptime info: $UPTIME" > logfile
WHO=$(whoami) # 用戶名
UPTIME=$(uptime) # 系統運行時間
echo "Today is $DATE. You are $WHO. Uptime info: $UPTIME" > logfile
內建命令 Shell 內建命令是可以直接在Shell中運行的命令。可以這麼查看內建命令:
$ compgen -b | sort
-
.
:
[
alias
autoload
bg
bindkey
break
builtin
bye
cd
也可以用 type 查看命令的類型。
$ type cd
cd is a shell builtin可以用 which 命令查看可執行文件的文件路徑:
# which sort
/usr/bin/sort可通過 man builtins 查看內建命令的詳細描述。
測試IF條件表達式if 後面需要接者then:
if [ condition-for-test ]
then
command
...
fi或者,
if [ condition-for-test ]; then
command
...
fi如:
#!/bin/bash
VAR=myvar
if [ $VAR = myvar ]; then
echo "1: \$VAR is $VAR" # 1: $VAR is myvar
fi
if [ "$VAR" = myvar ]; then
echo "2: \$VAR is $VAR" # 2: $VAR is myvar
fi
if [ $VAR = "myvar" ]; then
echo "3: \$VAR is $VAR" # 3: $VAR is myvar
fi
if [ "$VAR" = "myvar" ]; then
echo "4: \$VAR is $VAR" # 4: $VAR is myvar
fi上面,我們在比較時,可以用雙引號把變量引用起來。
但要注意單引號的使用。
#!/bin/bash
VAR=myvar
if [ '$VAR' = 'myvar' ]; then
echo '5a: $VAR is $VAR'
else
echo "5b: Not equal."
fibas
# Output:
# 5b: Not equal.上面這個就把 '$VAR' 當一個字符串了。
但如果變量是多個單詞,我們就必須用到雙引號了,如
#!/bin/bash
# 這樣寫就有問題
VAR1="my var"
if [ $VAR1 = "my var" ]; then
echo "\$VAR1 is $VAR1"
fi
# Output
# error [: too many arguments
# 用雙引號
if [ "$VAR1" = "my var" ]; then
echo "\$VAR1 is $VAR1"
fi總的來說,雙引號可以一直加上。
空格問題比較表達式中,如果=前後沒有空格,那麼整個表法式會被認為是一個單詞,其判斷結果為True.
#!/bin/bash
VAR2=2
# 由於被識別成一個單詞, [] 裡面為 true
if [ "$VAR2"=1 ]; then
echo "$VAR2 is 1."
else
echo "$VAR2 is not 1."
fi
# Output
# 2 is 1.
# 前後加上空格就好了
if [ "$VAR2" = 1 ]; then
echo "$VAR2 is 1."
else
echo "$VAR2 is not 1."
fi
# Output
# 2 is not 1.
另外需要注意的是, 在判斷中,中括號 [ 和變量之間一定要有一個空格,= 或者 ==。如果缺少了空格,你可能會到這類似這樣的錯誤:unary operator expected』 or missing]` 。
# 正確, 符號前後有空格
if [ $VAR2 = 1 ]; then
echo "\$VAR2 is 1."
else
echo "It's not 1."
fi
# Output
# 2 is 1.
# 錯誤, 符號前後無空格
if [$VAR2=1]; then
echo "$VAR2 is 1."
else
echo "It's not 1."
fi
# Output
# line 3: =1: command not found
# line 5: [=1]: command not found
# It's not 1.
文件測試表達式對文件進行相關測試,判斷的表達式如下:
表達式Truefile1 -nt file2file1 比 file2 新。file1 -ot file2file1 比 file2 老。-d file文件file存在,且是一個文件夾。-e file文件 file 存在。-f file文件file存在,且為普通文件。-L file文件file存在,且為符號連接。-O file文件 flle 存在, 且由有效用戶ID擁有。-r file文件 flle 存在, 且是一個可讀文件。-s file文件 flle 存在, 且長度大於0。-w file文件 flle 可寫入。-x file文件 flle 可寫執行。可以使用man test查看更詳細的說明。
當表達式為True時,測試命令返回退出狀態 0,而表達式為False時返回退出狀態1。
#!/bin/bash
FILE="/etc/resolv.conf"
if [ -e "$FILE" ]; then
if [ -f "$FILE" ]; then
echo "$FILE is a file."
fi
if [ -d "$FILE" ]; then
echo "$FILE is a directory."
fi
if [ -r "$FILE" ]; then
echo "$FILE is readable."
fi
fi
字符串比較表達式表達式Truestring1 = string2 或 string1 == string2兩字符相等string1 != string2兩個字符串不相等string1 > string2string1 大於 string2. string1 < string2string1 小於string2.-n string字符串長度大於0-z string字符串長度等於0#!/bin/bash
STRING=""
if [ -z "$STRING" ]; then
echo "There is no string." >&2
exit 1
fi
# Output
# There is no string.其中>&2將錯誤信息定位到標準錯誤輸出。
數字比較表達式下面這些是用來比較數字的一些表達式。
[...]((...))True[ "int1" -eq "int2" ](( "int1" == "int2" ))相等.[ "int1" -nq "int2" ](( "int1" != "int2" ))不等.[ "int1" -lt "int2" ](( "int1" < "int2" ))int2 大於 int1.[ "int1" -le "int2" ](( "int1" <= "int2" ))int2 大於等於 int1.[ "int1" -gt "int2" ](( "int1 > "int2" ))int1 大於 int2[ "int1" -ge "int2" ](( "int1 >= "int2" ))int1 大於等於 int2雙括號 (())數值的比較或者計算可以用((... ))。
#!/bin/bash
a=3
b=4
c=3
if (("$a" < "$b")); then
echo "$a is less than $b."
else
echo "$a is not less than $b."
fi
if (("$a" != "$c")); then
echo "$a is not equal to $c."
else
echo "$a is equal to $c."
fi
# 計算
echo "$a + $b = $(($a + $b))"
# Output
# 3 is less than 4.
# 3 is equal to 3.
# 3 + 4 = 7
怎麼使用 if/else 和 if/elif/else其實上面已經展示了不少了,這裡總結下if...else 和 if...elif...else 語句。
if/else 語句格式如下:
if [ condition-is-true ]
then
command A
else
command B
fi
# 或
if [ condition-is-true ]; then
command A
else
command B
fi例如:
#!/bin/bash
MY_SHELL="csh"
if [ "$MY_SHELL" = "bash" ]
then
echo "You are using the bash shell."
else
echo "You are not using the bash shell."
fi
if/elif/else 語句格式如下:
if [ condition-is-true ]
then
command A
elif [ condition-is-true ]
then
command B
else
command C
fi
# or
if [ condition-is-true ]; then
command A
elif [ condition-is-true ]; then
command B
else
command C
fi如:
#!/bin/bash
MY_SHELL="csh"
if [ "$MY_SHELL" = "bash" ]; then
echo "You are using the bash shell."
elif [ "$MY_SHELL" = "csh" ]; then
echo "You are using csh."
else
echo "You are not using the bash shell."
fi
雙中括號的使用[[]]如用用於比較的變量不是單個單詞,就需要[[]] , 或者用單中括號(這時需要加雙引號)。在平常的使用中,最好都使用[[]]。
與單中括號相比,雙中括號具有其他功能。如,可以對其中正則使用邏輯&&和||和=〜。
#!/bin/bash
VAR1="variable"
VAR2="variable 2"
if [[ (VAR1 == "variable") ]]; then
echo "They are the same."
else
echo "Not the same."
fi
# 使用 &&
[[ ($VAR1 == variable) && (
$VAR2 == "variable 2") ]] && echo "They are the same again."
#!/bin/bash
digit=4
if [[ $digit =~ [0-9] ]]; then
echo "$digit is a digit"
else
echo "$digit isn't a digit"
fi
letter="abc"
if [[ $letter =~ [0-9] ]]; then
echo "$letter is a digit"
else
echo "$letter isn't a digit"
fi
# Output
# 4 is a digit
# abc isn't a digit
怎麼使用 For 循環for循環的使用如下:
for VARIABLE_NAME in ITEM_1 ITEM_N
do
command A
done例如:
#!/bin/bash
for COLOR in red green blue
do
echo "COLOR: $COLOR"
done
# Output
# COLOR: red
# COLOR: green
# COLOR: blue
可以在其中使用變量,如下:
#!/bin/bash
COLORS="red green blue"
for COLOR in $COLORS
do
echo "COLOR: $COLOR"
done
用 for 循環重命名文件我們舉個簡單的例子,用for循環重命名當前目錄下的jpg圖片。
#!/bin/bash
IMGS=$(ls *jpg)
DATE=$(date +%F)
for IMG in $IMGS
do
echo "Renaming ${IMG} to ${DATE}-${IMG}"
mv ${IMG} ${DATE}-${IMG}
done
怎麼傳參執行腳本的時候,後面可以跟著很多參數,如:
$ scriptname param1 param2 param3param1 到 param3 稱為可選參數, 可以在腳本中用 $0, $1, $2等,來引用這些參賽。例如:
#!/bin/bash
echo "'\$0' is $0"
echo "'\$1' is $1"
echo "'\$2' is $2"
echo "'\$3' is $3"輸出:
$ ./param.sh
'$0' is ./param.sh
'$1' is
'$2' is
'$3' is $0 參數0返回的是當前執行文件的名字,包括路徑。
可以用 $@ 接受所以的參數。
#!/bin/bash
for PARAM in $@
do
echo "Param is: $PARAM"
doneUsing this script:
$ ./params.sh a b c d e f
Param is: a
Param is: b
Param is: c
Param is: d
Param is: e
Param is: f
怎麼接收用戶輸入用戶輸入稱為STDIN。可以將read命令與-p(提示)選項一起使用來讀取用戶輸入,它將輸出提示字符串。-r 選項不允許反斜槓轉義任何字符。
read -rp "PROMPT" VARIABLE例如:
#!/bin/bash
read -rp "Enter your programming languages: " PROGRAMMES
echo "Your programming languages are: "
for PROGRAMME in $PROGRAMMES; do
echo "$PROGRAMME "
done運行:
$ ./read.sh
Enter your programming languages: go py
Your programming languages are:
go
py
用大括號來表示範圍 {}如下所示,我們可以用大括號來表所一個數字或字母的範圍。
$ echo {0..3}
$ echo {a..d}
# output:
# 0 1 2 3
# a b c d你也可以在 for 循環中這麼使用:
#!/bin/bash
for i in {0..9};
do
touch file_"$i".txt;
doneThis will create different file names with different modification times.
$ ls -al file_*
-rw-rw-r-- 1 razeen razeen 0 2月 14 09:54 file_0.txt
-rw-rw-r-- 1 razeen razeen 0 2月 14 09:54 file_1.txt
-rw-rw-r-- 1 razeen razeen 0 2月 14 09:54 file_2.txt
...
怎麼使用While當 While 後的表達式結果為 true時,執行循環內語句。
#!/bin/bash
i=1
while [ $i -le 5 ]; do
echo $i
((i++))
doneOutput:
1
2
3
4
5
退出碼/返回碼 是什麼?每個命令都返回退出狀態,範圍為0-255。0代表成功,非0代表錯誤。可以用來進行錯誤檢查。
數值含義0成功2返回內置命令,從而提示錯誤126命令找到了,但不是可執行的127沒有找到命令128+N由於接收到信號N,命令退出怎麼檢查退出碼$? 包含了上一條命令執行的返回碼。
$ ls ./no/exist
ls: cannot access './no/exist': No such file or directory
$ echo "$?"
2如,在if表達式中檢查返回碼:
#!/bin/bash
HOST="razeen.cn"
ping -c 1 $HOST
RETURN_CODE=$?
if [ "$RETURN_CODE" -eq "0" ]; then
echo "$HOST reachable."
else
echo "$HOST unreachable."
fi-c 1 參數表示發送一個可達包就停止發送。然後我們檢查一下ping執行的返回碼。
輸出:
$ ./ex.sh
PING razeen.cn (47.108.161.7) 56(84) bytes of data.
64 bytes from 47.108.161.7 (47.108.161.7): icmp_seq=1 ttl=50 time=38.5 ms
--- razeen.cn ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 38.541/38.541/38.541/0.000 ms
razeen.cn reachable.
怎麼連接多個命令邏輯運算符和命令退出狀態執行命令後都有退出狀態,我們可以使用 && 和 ||去決定下一步。
退出命令你可以使用 exit 來決定退出碼:
exit 0
exit 1
exit 2
etc.例如:
#!/bin/bash
HOST="razeen.cn"
ping -c 1 $HOST
if [ "$?" -ne "0" ]
then
echo "$HOST unreachable."
exit 1
fi
exit 0我們可以將該腳本通過&&與其他腳本/命令連接。
$ ./ex2.sh && ls
....
2021-02-13-132m93.jpg ex2.sh file_1.txt file_4.txt 如果./ex2.sh返回狀態碼非0,後面的就不會執行。
邏輯與 (&&)當&&前面的語句返回的狀態碼為0時,執行後面的語句。
mkdir /tmp/bak && cp test.txt /tmp/bak
邏輯或 (||)當||前面的語句返回的狀態碼非0時(也就是執行失敗),執行後面的語句。
cp test.txt /tmp/bak/ || cp test.test.txt /tmp例如:
如果ping通了,就執行後面的輸出。
#!/bin/bash
host="razeen.cn"
ping -c 1 $host && echo "You can reach ${host}."如果ping失敗了,就執行後面的輸出。
#!/bin/bash
host="google.com"
ping -c 1 $host || echo "You can't reach ${host}."分號 (;)分號不是一個邏輯運算符,但你可以用它來分割語句。
cp text.txt /tmp/bak/ ; cp test.txt /tmp
# 等同於
cp text.txt /tmp/bak/
cp test.txt /tmp
管道 |管道|兩側的命令在各自的子shell中運行,並且兩者同時啟動。
如下:
第一個命令將目錄更改為主目錄,並列出文件和目錄。
第二個命令僅顯示執行該命令的文件和目錄。
$ echo "$(cd ~ && ls)"
$ echo "$(cd ~ | ls)"
函數在Bash中,你可以使用function或者直接定義一個函數。
function function-name(){}
# 或
function-name(){}當你調用函數的時候,只需要函數名,不用帶()。
#!/bin/bash
function hello(){
echo "Hello!"
}
# 正確
hello
# 錯誤
# hello()在函數中,可以調用其他函數。
#!/bin/bash
function hello(){
echo "Hello!"
now
bye
}
function now(){
echo "It's $(date +%r)"
}
function bye(){
echo "Bye bye."
}
hello
# Output
# Hello!
# It's 09:29:44 PM
# Bye bye.但,需要注意函數的定義順序。如果你在函數聲明前就去調用函數,函數就不會執行。如下, 在hello中執行now函數,但now是定義hello執行下面的,結果就會出錯。
#!/bin/bash
# this won't work
function hello(){
echo "Hello!"
now
}
hello
function now(){
echo "It's $(date +%r)"
}輸出:
$ ./hello2.sh
Hello!
./hello2.sh: line 5: now: command not found
函數傳參和腳本執行的時候傳參一樣,函數的參數也用$1...,$@ 來輸出。
注意$0這裡並不是函數的名字,而是當前腳本的名字。
$N是第N個參數,$@表示所有的參數。
#!/bin/bash
function fullname(){
echo "$0"
echo "My name is $1 $2"
}
fullname Razeen Cheng
# Output
# ./func.sh
# My name is Razeen Cheng#!/bin/bash
function greeting(){
for NAME in $@
do
echo "Hi $NAME."
done
}
greeting Tom Jerry
變量的作用域默認變量的作用域是全局的,必須先聲明,後使用。當然,最好在最上面就把需要的變量聲明好。
#!/bin/bash
my_func() {
GLOBAL_VAR=1
}
# 這時,變量還是空的
echo "Calling GLONAL_VAR before calling function my_func"
# echo $GLOBAL_VAR
# 聲明後,就可以輸出了
my_func
echo "Calling GLONAL_VAR after calling function my_func"
echo $GLOBAL_VAR
局部變量可以用local來定義局部變量,且只能在函數中使用。
#!/bin/bash
MY_VAR=1
my_func () {
local MY_VAR=2
echo "my_func: MY_VAR=$MY_VAR"
}
echo "global: MY_VAR=$MY_VAR"
my_func
函數返回碼你可以在函數中,指定返回碼:
return 0函數中最後執行的命令的退出狀態將隱式返回。有效代碼範圍為0-255。0代表成功,$?可以顯示退出碼。
$ my_function
$ echo $?
0可以在if 判斷中用$? :
#!/bin/bash
# 該函數用來創建一個備份文件
function backup_file () {
local BACK # 聲明局部變量
if [[ -f $1 ]];then # 檢查參數(是否是文件)
BACK="/tmp/$(basename "$1").$(date +%F).$$"
echo "Backing up $1 to $BACK"
cp "$1" "$BACK"
else
# 文件不存在.
return 1
fi
}
# 調用函數
if [[ "$1" ]]; then
backup_file "$1"
# if [[ $? -eq 0 ]]; then
if [[ $(backup_file "$1") -eq 0 ]]; then
echo "Backup succeeded."
exit 0
else
echo "Backup failed."
# 備份失敗,中斷,並返回非0狀態.
exit 1
fi
else
backup_file /etc/hosts
echo "/etc/hosts Backup succeeded."
exit 0
fi上面這個腳本默認備份/etc/hosts文件,除非你制定一個文件外。如果你指定一個文件參數,他會先檢查文件,然後備份到/tmp目錄。
$$ 返回 當前腳本執行的PID. 每次運行PID都會發生變化。當你需要多次運行腳本時,或許對你有幫助。
basename ${1} 可以從你輸入的路徑中提取文件的名字. 如 basename /etc/hosts 是 hosts.
$ ls /tmp
$ ex1
Backing up /etc/hosts to /tmp/hosts.2020-10-04.77124
Backup succeeded.
$ ls /tmp
hosts.2020-10-04.77124
關鍵字 exit 和 returnreturn 會跳出當前函數, exit會結束當前腳本。
總結這篇博客總結了常用的,我們需要了解的一些腳本語法與知識。如果向更好的使用bash, 我們還需要進一步學習更多的命令等。希望這篇博客能對你有所幫助。
參考Variables, Internal variables
shellcheck
shellcheck wiki
shell-format
The Bash Hackers Wiki
Advanced Bash-Scripting Guide