一篇文章讓你徹底掌握 shell 語言


目錄

一篇文章讓你徹底掌握 shell 語言

由於 bash 是 Linux 標准默認的 shell 解釋器,可以說 bash 是 shell 編程的基礎。

本文主要介紹 bash 的語法,對於 linux 指令不做任何介紹。

📓 本文已歸檔到:notes
:keyboard: 本文的源碼已歸檔到 os-tutorial

███████╗██╗  ██╗███████╗██╗     ██╗
██╔════╝██║  ██║██╔════╝██║     ██║
███████╗███████║█████╗  ██║     ██║
╚════██║██╔══██║██╔══╝  ██║     ██║
███████║██║  ██║███████╗███████╗███████╗

1. 簡介

1.1. 什么是 shell

  • Shell 是一個用 C 語言編寫的程序,它是用戶使用 Linux 的橋梁。
  • Shell 既是一種命令語言,又是一種程序設計語言。
  • Shell 是指一種應用程序,這個應用程序提供了一個界面,用戶通過這個界面訪問 Linux 內核的服務。

Ken Thompson 的 sh 是第一種 Unix Shell,Windows Explorer 是一個典型的圖形界面 Shell。

1.2. 什么是 shell 腳本

Shell 腳本(shell script),是一種為 shell 編寫的腳本程序,一般文件后綴為 .sh

業界所說的 shell 通常都是指 shell 腳本,但 shell 和 shell script 是兩個不同的概念。

1.3. Shell 環境

Shell 編程跟 java、php 編程一樣,只要有一個能編寫代碼的文本編輯器和一個能解釋執行的腳本解釋器就可以了。

Shell 的解釋器種類眾多,常見的有:

  • sh - 即 Bourne Shell。sh 是 Unix 標准默認的 shell。
  • bash - 即 Bourne Again Shell。bash 是 Linux 標准默認的 shell。
  • fish - 智能和用戶友好的命令行 shell。
  • xiki - 使 shell 控制台更友好,更強大。
  • zsh - 功能強大的 shell 與腳本語言。

指定腳本解釋器

在 shell 腳本,#! 告訴系統其后路徑所指定的程序即是解釋此腳本文件的 Shell 解釋器。#! 被稱作shebang(也稱為 Hashbang )

所以,你應該會在 shell 中,見到諸如以下的注釋:

  • 指定 sh 解釋器
#!/bin/sh
  • 指定 bash 解釋器
#!/bin/bash

注意

上面的指定解釋器的方式是比較常見的,但有時候,你可能也會看到下面的方式:

#!/usr/bin/env bash

這樣做的好處是,系統會自動在 PATH 環境變量中查找你指定的程序(本例中的bash)。相比第一種寫法,你應該盡量用這種寫法,因為程序的路徑是不確定的。這樣寫還有一個好處,操作系統的PATH變量有可能被配置為指向程序的另一個版本。比如,安裝完新版本的bash,我們可能將其路徑添加到PATH中,來“隱藏”老版本。如果直接用#!/bin/bash,那么系統會選擇老版本的bash來執行腳本,如果用#!/usr/bin/env bash,則會使用新版本。

1.4. 模式

shell 有交互和非交互兩種模式。

交互模式

簡單來說,你可以將 shell 的交互模式理解為執行命令行。

看到形如下面的東西,說明 shell 處於交互模式下:

user@host:~$

接着,便可以輸入一系列 Linux 命令,比如 lsgrepcdmkdirrm 等等。

非交互模式

簡單來說,你可以將 shell 的非交互模式理解為執行 shell 腳本。

在非交互模式下,shell 從文件或者管道中讀取命令並執行。

當 shell 解釋器執行完文件中的最后一個命令,shell 進程終止,並回到父進程。

可以使用下面的命令讓 shell 以非交互模式運行:

sh /path/to/script.sh
bash /path/to/script.sh
source /path/to/script.sh ./path/to/script.sh

上面的例子中,script.sh是一個包含 shell 解釋器可以識別並執行的命令的普通文本文件,shbash是 shell 解釋器程序。你可以使用任何喜歡的編輯器創建script.sh(vim,nano,Sublime Text, Atom 等等)。

其中,source /path/to/script.sh 和 ./path/to/script.sh 是等價的。

除此之外,你還可以通過chmod命令給文件添加可執行的權限,來直接執行腳本文件:

chmod +x /path/to/script.sh #使腳本具有執行權限 /path/to/test.sh

這種方式要求腳本文件的第一行必須指明運行該腳本的程序,比如:

:keyboard: 『示例源碼』 helloworld.sh

#!/usr/bin/env bash echo "Hello, world!"

上面的例子中,我們使用了一個很有用的命令echo來輸出字符串到屏幕上。

2. 基本語法

2.1. 解釋器

前面雖然兩次提到了#! ,但是本着重要的事情說三遍的精神,這里再強調一遍:

在 shell 腳本,#! 告訴系統其后路徑所指定的程序即是解釋此腳本文件的 Shell 解釋器。#! 被稱作shebang(也稱為 Hashbang )

#! 決定了腳本可以像一個獨立的可執行文件一樣執行,而不用在終端之前輸入shbashpythonphp等。

# 以下兩種方式都可以指定 shell 解釋器為 bash,第二種方式更好 #!/bin/bash #!/usr/bin/env bash

2.2. 注釋

注釋可以說明你的代碼是什么作用,以及為什么這樣寫。

shell 語法中,注釋是特殊的語句,會被 shell 解釋器忽略。

  • 單行注釋 - 以 # 開頭,到行尾結束。
  • 多行注釋 - 以 :<<EOF 開頭,到 EOF 結束。

:keyboard: 『示例源碼』 comment-demo.sh

#-------------------------------------------- # shell 注釋示例 # author:zp #-------------------------------------------- # echo '這是單行注釋' ########## 這是分割線 ########## :<<EOF echo '這是多行注釋' echo '這是多行注釋' echo '這是多行注釋' EOF

2.3. echo

echo 用於字符串的輸出。

輸出普通字符串:

echo "hello, world" # Output: hello, world

輸出含變量的字符串:

echo "hello, \"zp\"" # Output: hello, "zp"

輸出含變量的字符串:

name=zp
echo "hello, \"${name}\"" # Output: hello, "zp"

輸出含換行符的字符串:

# 輸出含換行符的字符串 echo "YES\nNO" # Output: YES\nNO echo -e "YES\nNO" # -e 開啟轉義 # Output: # YES # NO

輸出含不換行符的字符串:

echo "YES" echo "NO" # Output: # YES # NO echo -e "YES\c" # -e 開啟轉義 \c 不換行 echo "NO" # Output: # YESNO

輸出重定向至文件

echo "test" > test.txt

輸出執行結果

echo `pwd` # Output:(當前目錄路徑)

:keyboard: 『示例源碼』 echo-demo.sh

2.4. printf

printf 用於格式化輸出字符串。

默認,printf 不會像 echo 一樣自動添加換行符,如果需要換行可以手動添加 \n

:keyboard: 『示例源碼』 printf-demo.sh

# 單引號 printf '%d %s\n' 1 "abc" # Output:1 abc # 雙引號 printf "%d %s\n" 1 "abc" # Output:1 abc # 無引號 printf %s abcdef # Output: abcdef(並不會換行) # 格式只指定了一個參數,但多出的參數仍然會按照該格式輸出 printf "%s\n" abc def # Output: # abc # def printf "%s %s %s\n" a b c d e f g h i j # Output: # a b c # d e f # g h i # j # 如果沒有參數,那么 %s 用 NULL 代替,%d 用 0 代替 printf "%s and %d \n" # Output: # and 0 # 格式化輸出 printf "%-10s %-8s %-4s\n" 姓名 性別 體重kg printf "%-10s %-8s %-4.2f\n" 郭靖 男 66.1234 printf "%-10s %-8s %-4.2f\n" 楊過 男 48.6543 printf "%-10s %-8s %-4.2f\n" 郭芙 女 47.9876 # Output: # 姓名 性別 體重kg # 郭靖 男 66.12 # 楊過 男 48.65 # 郭芙 女 47.99

printf 的轉義符

序列 說明
\a 警告字符,通常為 ASCII 的 BEL 字符
\b 后退
\c 抑制(不顯示)輸出結果中任何結尾的換行字符(只在%b 格式指示符控制下的參數字符串中有效),而且,任何留在參數里的字符、任何接下來的參數以及任何留在格式字符串中的字符,都被忽略
\f 換頁(formfeed)
\n 換行
\r 回車(Carriage return)
\t 水平制表符
\v 垂直制表符
\\ 一個字面上的反斜杠字符
\ddd 表示 1 到 3 位數八進制值的字符。僅在格式字符串中有效
\0ddd 表示 1 到 3 位的八進制值字符

3. 變量

跟許多程序設計語言一樣,你可以在 bash 中創建變量。

Bash 中沒有數據類型,bash 中的變量可以保存一個數字、一個字符、一個字符串等等。同時無需提前聲明變量,給變量賦值會直接創建變量。

3.1. 變量命名原則

  • 命名只能使用英文字母,數字和下划線,首個字符不能以數字開頭。
  • 中間不能有空格,可以使用下划線(_)。
  • 不能使用標點符號。
  • 不能使用 bash 里的關鍵字(可用 help 命令查看保留關鍵字)。

3.2. 聲明變量

訪問變量的語法形式為:${var} 和 $var 。

變量名外面的花括號是可選的,加不加都行,加花括號是為了幫助解釋器識別變量的邊界,所以推薦加花括號。

word="hello" echo ${word} # Output: hello

3.3. 只讀變量

使用 readonly 命令可以將變量定義為只讀變量,只讀變量的值不能被改變。

rword="hello" echo ${rword} readonly rword # rword="bye" # 如果放開注釋,執行時會報錯

3.4. 刪除變量

使用 unset 命令可以刪除變量。變量被刪除后不能再次使用。unset 命令不能刪除只讀變量。

dword="hello" # 聲明變量 echo ${dword} # 輸出變量值 # Output: hello unset dword # 刪除變量 echo ${dword} # Output: (空)

3.5. 變量類型

  • 局部變量 - 局部變量是僅在某個腳本內部有效的變量。它們不能被其他的程序和腳本訪問。
  • 環境變量 - 環境變量是對當前 shell 會話內所有的程序或腳本都可見的變量。創建它們跟創建局部變量類似,但使用的是 export 關鍵字,shell 腳本也可以定義環境變量。

常見的環境變量:

變量 描述
$HOME 當前用戶的用戶目錄
$PATH 用分號分隔的目錄列表,shell 會到這些目錄中查找命令
$PWD 當前工作目錄
$RANDOM 0 到 32767 之間的整數
$UID 數值類型,當前用戶的用戶 ID
$PS1 主要系統輸入提示符
$PS2 次要系統輸入提示符

這里 有一張更全面的 Bash 環境變量列表。

3.6. 變量示例源碼

⌨️ 『示例源碼』 variable-demo.sh

4. 字符串

4.1. 單引號和雙引號

shell 字符串可以用單引號 '',也可以用雙引號 “”,也可以不用引號。

  • 單引號的特點
    • 單引號里不識別變量
    • 單引號里不能出現單獨的單引號(使用轉義符也不行),但可成對出現,作為字符串拼接使用。
  • 雙引號的特點
    • 雙引號里識別變量
    • 雙引號里可以出現轉義字符

綜上,推薦使用雙引號。

4.2. 拼接字符串

# 使用單引號拼接 name1='white' str1='hello, '${name1}'' str2='hello, ${name1}' echo ${str1}_${str2} # Output: # hello, white_hello, ${name1} # 使用雙引號拼接 name2="black" str3="hello, "${name2}"" str4="hello, ${name2}" echo ${str3}_${str4} # Output: # hello, black_hello, black

4.3. 獲取字符串長度

text="12345" echo ${#text} # Output: # 5

4.4. 截取子字符串

text="12345" echo ${text:2:2} # Output: # 34

從第 3 個字符開始,截取 2 個字符

4.5. 查找子字符串

#!/usr/bin/env bash text="hello" echo `expr index "${text}" ll` # Execute: ./str-demo5.sh # Output: # 3

查找 ll 子字符在 hello 字符串中的起始位置。

4.6. 字符串示例源碼

⌨️ 『示例源碼』 string-demo.sh

5. 數組

bash 只支持一維數組。

數組下標從 0 開始,下標可以是整數或算術表達式,其值應大於或等於 0。

5.1. 創建數組

# 創建數組的不同方式 nums=([2]=2 [0]=0 [1]=1) colors=(red yellow "dark blue")

5.2. 訪問數組元素

  • 訪問數組的單個元素:
echo ${nums[1]} # Output: 1
  • 訪問數組的所有元素:
echo ${colors[*]} # Output: red yellow dark blue echo ${colors[@]} # Output: red yellow dark blue

上面兩行有很重要(也很微妙)的區別:

為了將數組中每個元素單獨一行輸出,我們用 printf 命令:

printf "+ %s\n" ${colors[*]} # Output: # + red # + yellow # + dark # + blue

為什么darkblue各占了一行?嘗試用引號包起來:

printf "+ %s\n" "${colors[*]}" # Output: # + red yellow dark blue

現在所有的元素都在一行輸出 —— 這不是我們想要的!讓我們試試${colors[@]}

printf "+ %s\n" "${colors[@]}" # Output: # + red # + yellow # + dark blue

在引號內,${colors[@]}將數組中的每個元素擴展為一個單獨的參數;數組元素中的空格得以保留。

  • 訪問數組的部分元素:
echo ${nums[@]:0:2} # Output: # 0 1

在上面的例子中,${array[@]} 擴展為整個數組,:0:2取出了數組中從 0 開始,長度為 2 的元素。

5.3. 訪問數組長度

echo ${#nums[*]} # Output: # 3

5.4. 向數組中添加元素

向數組中添加元素也非常簡單:

colors=(white "${colors[@]}" green black) echo ${colors[@]} # Output: # white red yellow dark blue green black

上面的例子中,${colors[@]} 擴展為整個數組,並被置換到復合賦值語句中,接着,對數組colors的賦值覆蓋了它原來的值。

5.5. 從數組中刪除元素

unset命令來從數組中刪除一個元素:

unset nums[0] echo ${nums[@]} # Output: # 1 2

5.6. 數組示例源碼

:keyboard: 『示例源碼』 array-demo.sh

6. 運算符

6.1. 算術運算符

下表列出了常用的算術運算符,假定變量 x 為 10,變量 y 為 20:

運算符 說明 舉例
+ 加法 expr $x + $y 結果為 30。
- 減法 expr $x - $y 結果為 -10。
* 乘法 expr $x * $y 結果為 200。
/ 除法 expr $y / $x 結果為 2。
% 取余 expr $y % $x 結果為 0。
= 賦值 x=$y 將把變量 y 的值賦給 x。
== 相等。用於比較兩個數字,相同則返回 true。 [ $x == $y ] 返回 false。
!= 不相等。用於比較兩個數字,不相同則返回 true。 [ $x != $y ] 返回 true。

注意:條件表達式要放在方括號之間,並且要有空格,例如: [$x==$y] 是錯誤的,必須寫成 [ $x == $y ]

:keyboard: 『示例源碼』 operator-demo.sh

x=10
y=20

echo "x=${x}, y=${y}" val=`expr ${x} + ${y}` echo "${x} + ${y} = $val" val=`expr ${x} - ${y}` echo "${x} - ${y} = $val" val=`expr ${x} \* ${y}` echo "${x} * ${y} = $val" val=`expr ${y} / ${x}` echo "${y} / ${x} = $val" val=`expr ${y} % ${x}` echo "${y} % ${x} = $val" if [[ ${x} == ${y} ]] then echo "${x} = ${y}" fi if [[ ${x} != ${y} ]] then echo "${x} != ${y}" fi # Execute: ./operator-demo.sh # Output: # x=10, y=20 # 10 + 20 = 30 # 10 - 20 = -10 # 10 * 20 = 200 # 20 / 10 = 2 # 20 % 10 = 0 # 10 != 20

6.2. 關系運算符

關系運算符只支持數字,不支持字符串,除非字符串的值是數字。

下表列出了常用的關系運算符,假定變量 x 為 10,變量 y 為 20:

運算符 說明 舉例
-eq 檢測兩個數是否相等,相等返回 true。 [ $a -eq $b ]返回 false。
-ne 檢測兩個數是否相等,不相等返回 true。 [ $a -ne $b ] 返回 true。
-gt 檢測左邊的數是否大於右邊的,如果是,則返回 true。 [ $a -gt $b ] 返回 false。
-lt 檢測左邊的數是否小於右邊的,如果是,則返回 true。 [ $a -lt $b ] 返回 true。
-ge 檢測左邊的數是否大於等於右邊的,如果是,則返回 true。 [ $a -ge $b ] 返回 false。
-le 檢測左邊的數是否小於等於右邊的,如果是,則返回 true。 [ $a -le $b ]返回 true。

:keyboard: 『示例源碼』 operator-demo2.sh

x=10
y=20

echo "x=${x}, y=${y}" if [[ ${x} -eq ${y} ]]; then echo "${x} -eq ${y} : x 等於 y" else echo "${x} -eq ${y}: x 不等於 y" fi if [[ ${x} -ne ${y} ]]; then echo "${x} -ne ${y}: x 不等於 y" else echo "${x} -ne ${y}: x 等於 y" fi if [[ ${x} -gt ${y} ]]; then echo "${x} -gt ${y}: x 大於 y" else echo "${x} -gt ${y}: x 不大於 y" fi if [[ ${x} -lt ${y} ]]; then echo "${x} -lt ${y}: x 小於 y" else echo "${x} -lt ${y}: x 不小於 y" fi if [[ ${x} -ge ${y} ]]; then echo "${x} -ge ${y}: x 大於或等於 y" else echo "${x} -ge ${y}: x 小於 y" fi if [[ ${x} -le ${y} ]]; then echo "${x} -le ${y}: x 小於或等於 y" else echo "${x} -le ${y}: x 大於 y" fi # Execute: ./operator-demo2.sh # Output: # x=10, y=20 # 10 -eq 20: x 不等於 y # 10 -ne 20: x 不等於 y # 10 -gt 20: x 不大於 y # 10 -lt 20: x 小於 y # 10 -ge 20: x 小於 y # 10 -le 20: x 小於或等於 y

6.3. 布爾運算符

下表列出了常用的布爾運算符,假定變量 x 為 10,變量 y 為 20:

運算符 說明 舉例
! 非運算,表達式為 true 則返回 false,否則返回 true。 [ ! false ] 返回 true。
-o 或運算,有一個表達式為 true 則返回 true。 [ $a -lt 20 -o $b -gt 100 ] 返回 true。
-a 與運算,兩個表達式都為 true 才返回 true。 [ $a -lt 20 -a $b -gt 100 ] 返回 false。

:keyboard: 『示例源碼』 operator-demo3.sh

x=10
y=20

echo "x=${x}, y=${y}" if [[ ${x} != ${y} ]]; then echo "${x} != ${y} : x 不等於 y" else echo "${x} != ${y}: x 等於 y" fi if [[ ${x} -lt 100 && ${y} -gt 15 ]]; then echo "${x} 小於 100 且 ${y} 大於 15 : 返回 true" else echo "${x} 小於 100 且 ${y} 大於 15 : 返回 false" fi if [[ ${x} -lt 100 || ${y} -gt 100 ]]; then echo "${x} 小於 100 或 ${y} 大於 100 : 返回 true" else echo "${x} 小於 100 或 ${y} 大於 100 : 返回 false" fi if [[ ${x} -lt 5 || ${y} -gt 100 ]]; then echo "${x} 小於 5 或 ${y} 大於 100 : 返回 true" else echo "${x} 小於 5 或 ${y} 大於 100 : 返回 false" fi # Execute: ./operator-demo3.sh # Output: # x=10, y=20 # 10 != 20 : x 不等於 y # 10 小於 100 且 20 大於 15 : 返回 true # 10 小於 100 或 20 大於 100 : 返回 true # 10 小於 5 或 20 大於 100 : 返回 false

6.4. 邏輯運算符

以下介紹 Shell 的邏輯運算符,假定變量 x 為 10,變量 y 為 20:

運算符 說明 舉例
&& 邏輯的 AND [[ ${x} -lt 100 && ${y} -gt 100 ]] 返回 false
|| 邏輯的 OR [[ ${x} -lt 100 || ${y} -gt 100 ]] 返回 true

:keyboard: 『示例源碼』 operator-demo4.sh

x=10
y=20

echo "x=${x}, y=${y}" if [[ ${x} -lt 100 && ${y} -gt 100 ]] then echo "${x} -lt 100 && ${y} -gt 100 返回 true" else echo "${x} -lt 100 && ${y} -gt 100 返回 false" fi if [[ ${x} -lt 100 || ${y} -gt 100 ]] then echo "${x} -lt 100 || ${y} -gt 100 返回 true" else echo "${x} -lt 100 || ${y} -gt 100 返回 false" fi # Execute: ./operator-demo4.sh # Output: # x=10, y=20 # 10 -lt 100 && 20 -gt 100 返回 false # 10 -lt 100 || 20 -gt 100 返回 true

6.5. 字符串運算符

下表列出了常用的字符串運算符,假定變量 a 為 "abc",變量 b 為 "efg":

運算符 說明 舉例
= 檢測兩個字符串是否相等,相等返回 true。 [ $a = $b ] 返回 false。
!= 檢測兩個字符串是否相等,不相等返回 true。 [ $a != $b ] 返回 true。
-z 檢測字符串長度是否為 0,為 0 返回 true。 [ -z $a ] 返回 false。
-n 檢測字符串長度是否為 0,不為 0 返回 true。 [ -n $a ] 返回 true。
str 檢測字符串是否為空,不為空返回 true。 [ $a ] 返回 true。

:keyboard: 『示例源碼』 operator-demo5.sh

x="abc" y="xyz" echo "x=${x}, y=${y}" if [[ ${x} = ${y} ]]; then echo "${x} = ${y} : x 等於 y" else echo "${x} = ${y}: x 不等於 y" fi if [[ ${x} != ${y} ]]; then echo "${x} != ${y} : x 不等於 y" else echo "${x} != ${y}: x 等於 y" fi if [[ -z ${x} ]]; then echo "-z ${x} : 字符串長度為 0" else echo "-z ${x} : 字符串長度不為 0" fi if [[ -n "${x}" ]]; then echo "-n ${x} : 字符串長度不為 0" else echo "-n ${x} : 字符串長度為 0" fi if [[ ${x} ]]; then echo "${x} : 字符串不為空" else echo "${x} : 字符串為空" fi # Execute: ./operator-demo5.sh # Output: # x=abc, y=xyz # abc = xyz: x 不等於 y # abc != xyz : x 不等於 y # -z abc : 字符串長度不為 0 # -n abc : 字符串長度不為 0 # abc : 字符串不為空

6.6. 文件測試運算符

文件測試運算符用於檢測 Unix 文件的各種屬性。

屬性檢測描述如下:

操作符 說明 舉例
-b file 檢測文件是否是塊設備文件,如果是,則返回 true。 [ -b $file ] 返回 false。
-c file 檢測文件是否是字符設備文件,如果是,則返回 true。 [ -c $file ] 返回 false。
-d file 檢測文件是否是目錄,如果是,則返回 true。 [ -d $file ] 返回 false。
-f file 檢測文件是否是普通文件(既不是目錄,也不是設備文件),如果是,則返回 true。 [ -f $file ] 返回 true。
-g file 檢測文件是否設置了 SGID 位,如果是,則返回 true。 [ -g $file ] 返回 false。
-k file 檢測文件是否設置了粘着位(Sticky Bit),如果是,則返回 true。 [ -k $file ]返回 false。
-p file 檢測文件是否是有名管道,如果是,則返回 true。 [ -p $file ] 返回 false。
-u file 檢測文件是否設置了 SUID 位,如果是,則返回 true。 [ -u $file ] 返回 false。
-r file 檢測文件是否可讀,如果是,則返回 true。 [ -r $file ] 返回 true。
-w file 檢測文件是否可寫,如果是,則返回 true。 [ -w $file ] 返回 true。
-x file 檢測文件是否可執行,如果是,則返回 true。 [ -x $file ] 返回 true。
-s file 檢測文件是否為空(文件大小是否大於 0),不為空返回 true。 [ -s $file ] 返回 true。
-e file 檢測文件(包括目錄)是否存在,如果是,則返回 true。 [ -e $file ] 返回 true。

:keyboard: 『示例源碼』 operator-demo6.sh

file="/etc/hosts" if [[ -r ${file} ]]; then echo "${file} 文件可讀" else echo "${file} 文件不可讀" fi if [[ -w ${file} ]]; then echo "${file} 文件可寫" else echo "${file} 文件不可寫" fi if [[ -x ${file} ]]; then echo "${file} 文件可執行" else echo "${file} 文件不可執行" fi if [[ -f ${file} ]]; then echo "${file} 文件為普通文件" else echo "${file} 文件為特殊文件" fi if [[ -d ${file} ]]; then echo "${file} 文件是個目錄" else echo "${file} 文件不是個目錄" fi if [[ -s ${file} ]]; then echo "${file} 文件不為空" else echo "${file} 文件為空" fi if [[ -e ${file} ]]; then echo "${file} 文件存在" else echo "${file} 文件不存在" fi # Execute: ./operator-demo6.sh # Output:(根據文件的實際情況,輸出結果可能不同) # /etc/hosts 文件可讀 # /etc/hosts 文件可寫 # /etc/hosts 文件不可執行 # /etc/hosts 文件為普通文件 # /etc/hosts 文件不是個目錄 # /etc/hosts 文件不為空 # /etc/hosts 文件存在

7. 控制語句

7.1. 條件語句

跟其它程序設計語言一樣,Bash 中的條件語句讓我們可以決定一個操作是否被執行。結果取決於一個包在[[ ]]里的表達式。

[[ ]]sh中是[ ])包起來的表達式被稱作 檢測命令 或 基元。這些表達式幫助我們檢測一個條件的結果。這里可以找到有關bash 中單雙中括號區別的答案。

共有兩個不同的條件表達式:ifcase

if

(1)if 語句

if在使用上跟其它語言相同。如果中括號里的表達式為真,那么thenfi之間的代碼會被執行。fi標志着條件代碼塊的結束。

# 寫成一行 if [[ 1 -eq 1 ]]; then echo "1 -eq 1 result is: true"; fi # Output: 1 -eq 1 result is: true # 寫成多行 if [[ "abc" -eq "abc" ]] then echo ""abc" -eq "abc" result is: true" fi # Output: abc -eq abc result is: true

(2)if else 語句

同樣,我們可以使用if..else語句,例如:

if [[ 2 -ne 1 ]]; then echo "true" else echo "false" fi # Output: true

(3)if elif else 語句

有些時候,if..else不能滿足我們的要求。別忘了if..elif..else,使用起來也很方便。

x=10
y=20
if [[ ${x} > ${y} ]]; then echo "${x} > ${y}" elif [[ ${x} < ${y} ]]; then echo "${x} < ${y}" else echo "${x} = ${y}" fi # Output: 10 < 20

:keyboard: 『示例源碼』 if-demo.sh

case

如果你需要面對很多情況,分別要采取不同的措施,那么使用case會比嵌套的if更有用。使用case來解決復雜的條件判斷,看起來像下面這樣:

:keyboard: 『示例源碼』 case-demo.sh

exec case ${oper} in "+") val=`expr ${x} + ${y}` echo "${x} + ${y} = ${val}" ;; "-") val=`expr ${x} - ${y}` echo "${x} - ${y} = ${val}" ;; "*") val=`expr ${x} \* ${y}` echo "${x} * ${y} = ${val}" ;; "/") val=`expr ${x} / ${y}` echo "${x} / ${y} = ${val}" ;; *) echo "Unknown oper!" ;; esac

每種情況都是匹配了某個模式的表達式。|用來分割多個模式,)用來結束一個模式序列。第一個匹配上的模式對應的命令將會被執行。*代表任何不匹配以上給定模式的模式。命令塊兒之間要用;;分隔。

7.2. 循環語句

循環其實不足為奇。跟其它程序設計語言一樣,bash 中的循環也是只要控制條件為真就一直迭代執行的代碼塊。

Bash 中有四種循環:forwhileuntilselect

for循環

for與它在 C 語言中的姊妹非常像。看起來是這樣:

for arg in elem1 elem2 ... elemN do ### 語句 done

在每次循環的過程中,arg依次被賦值為從elem1elemN。這些值還可以是通配符或者大括號擴展

當然,我們還可以把for循環寫在一行,但這要求do之前要有一個分號,就像下面這樣:

for i in {1..5}; do echo $i; done

還有,如果你覺得for..in..do對你來說有點奇怪,那么你也可以像 C 語言那樣使用for,比如:

for (( i = 0; i < 10; i++ )); do echo $i done

當我們想對一個目錄下的所有文件做同樣的操作時,for就很方便了。舉個例子,如果我們想把所有的.bash文件移動到script文件夾中,並給它們可執行權限,我們的腳本可以這樣寫:

DIR=/home/zp
for FILE in ${DIR}/*.sh; do mv "$FILE" "${DIR}/scripts" done # 將 /home/zp 目錄下所有 sh 文件拷貝到 /home/zp/scripts

:keyboard: 『示例源碼』 for-demo.sh

while循環

while循環檢測一個條件,只要這個條件為 ,就執行一段命令。被檢測的條件跟if..then中使用的基元並無二異。因此一個while循環看起來會是這樣:

while [[ condition ]] do ### 語句 done

for循環一樣,如果我們把do和被檢測的條件寫到一行,那么必須要在do之前加一個分號。

比如下面這個例子:

### 0到9之間每個數的平方 x=0 while [[ ${x} -lt 10 ]]; do echo $((x * x)) x=$((x + 1)) done # Output: # 0 # 1 # 4 # 9 # 16 # 25 # 36 # 49 # 64 # 81

:keyboard: 『示例源碼』 while-demo.sh

until循環

until循環跟while循環正好相反。它跟while一樣也需要檢測一個測試條件,但不同的是,只要該條件為  就一直執行循環:

x=0
until [[ ${x} -ge 5 ]]; do echo ${x} x=`expr ${x} + 1` done # Output: # 0 # 1 # 2 # 3 # 4

:keyboard: 『示例源碼』 until-demo.sh

select循環

select循環幫助我們組織一個用戶菜單。它的語法幾乎跟for循環一致:

select answer in elem1 elem2 ... elemN do ### 語句 done

select會打印elem1..elemN以及它們的序列號到屏幕上,之后會提示用戶輸入。通常看到的是$?PS3變量)。用戶的選擇結果會被保存到answer中。如果answer是一個在1..N之間的數字,那么語句會被執行,緊接着會進行下一次迭代 —— 如果不想這樣的話我們可以使用break語句。

一個可能的實例可能會是這樣:

#!/usr/bin/env bash PS3="Choose the package manager: " select ITEM in bower npm gem pip do echo -n "Enter the package name: " && read PACKAGE case ${ITEM} in bower) bower install ${PACKAGE} ;; npm) npm install ${PACKAGE} ;; gem) gem install ${PACKAGE} ;; pip) pip install ${PACKAGE} ;; esac break # 避免無限循環 done

這個例子,先詢問用戶他想使用什么包管理器。接着,又詢問了想安裝什么包,最后執行安裝操作。

運行這個腳本,會得到如下輸出:

$ ./my_script
1) bower
2) npm
3) gem
4) pip
Choose the package manager: 2
Enter the package name: gitbook-cli

:keyboard: 『示例源碼』 select-demo.sh

break 和 continue

如果想提前結束一個循環或跳過某次循環執行,可以使用 shell 的breakcontinue語句來實現。它們可以在任何循環中使用。

break語句用來提前結束當前循環。

continue語句用來跳過某次迭代。

:keyboard: 『示例源碼』 break-demo.sh

# 查找 10 以內第一個能整除 2 和 3 的正整數 i=1 while [[ ${i} -lt 10 ]]; do if [[ $((i % 3)) -eq 0 ]] && [[ $((i % 2)) -eq 0 ]]; then echo ${i} break; fi i=`expr ${i} + 1` done # Output: 6

:keyboard: 『示例源碼』 continue-demo.sh

# 打印10以內的奇數 for (( i = 0; i < 10; i ++ )); do if [[ $((i % 2)) -eq 0 ]]; then continue; fi echo ${i} done # Output: # 1 # 3 # 5 # 7 # 9

8. 函數

bash 函數定義語法如下:

[ function ] funname [()] { action; [return int;] }

💡 說明:

  1. 函數定義時,function 關鍵字可有可無。
  2. 函數返回值 - return 返回函數返回值,返回值類型只能為整數(0-255)。如果不加 return 語句,shell 默認將以最后一條命令的運行結果,作為函數返回值。
  3. 函數返回值在調用該函數后通過 $? 來獲得。
  4. 所有函數在使用前必須定義。這意味着必須將函數放在腳本開始部分,直至 shell 解釋器首次發現它時,才可以使用。調用函數僅使用其函數名即可。

:keyboard: 『示例源碼』 function-demo.sh

#!/usr/bin/env bash calc(){ PS3="choose the oper: " select oper in + - \* / # 生成操作符選擇菜單 do echo -n "enter first num: " && read x # 讀取輸入參數 echo -n "enter second num: " && read y # 讀取輸入參數 exec case ${oper} in "+") return $((${x} + ${y})) ;; "-") return $((${x} - ${y})) ;; "*") return $((${x} * ${y})) ;; "/") return $((${x} / ${y})) ;; *) echo "${oper} is not support!" return 0 ;; esac break done } calc echo "the result is: $?" # $? 獲取 calc 函數返回值

執行結果:

$ ./function-demo.sh 1) + 2) - 3) * 4) / choose the oper: 3 enter first num: 10 enter second num: 10 the result is: 100

8.1. 位置參數

位置參數是在調用一個函數並傳給它參數時創建的變量。

位置參數變量表:

變量 描述
$0 腳本名稱
$1 … $9 第 1 個到第 9 個參數列表
${10} … ${N} 第 10 個到 N 個參數列表
$* or $@ 除了$0外的所有位置參數
$# 不包括$0在內的位置參數的個數
$FUNCNAME 函數名稱(僅在函數內部有值)

:keyboard: 『示例源碼』 function-demo2.sh

#!/usr/bin/env bash x=0 if [[ -n $1 ]]; then echo "第一個參數為:$1" x=$1 else echo "第一個參數為空" fi y=0 if [[ -n $2 ]]; then echo "第二個參數為:$2" y=$2 else echo "第二個參數為空" fi paramsFunction(){ echo "函數第一個入參:$1" echo "函數第二個入參:$2" } paramsFunction ${x} ${y}

執行結果:

$ ./function-demo2.sh 第一個參數為空 第二個參數為空 函數第一個入參:0 函數第二個入參:0 $ ./function-demo2.sh 10 20 第一個參數為:10 第二個參數為:20 函數第一個入參:10 函數第二個入參:20

執行 ./variable-demo4.sh hello world ,然后在腳本中通過 $1$2 ... 讀取第 1 個參數、第 2 個參數。。。

8.2. 函數處理參數

另外,還有幾個特殊字符用來處理參數:

參數處理 說明
$# 返回參數個數
$* 返回所有參數
$$ 腳本運行的當前進程 ID 號
$! 后台運行的最后一個進程的 ID 號
$@ 返回所有參數
$- 返回 Shell 使用的當前選項,與 set 命令功能相同。
$? 函數返回值

:keyboard: 『示例源碼』 function-demo3.sh

runner() { return 0 } name=zp paramsFunction(){ echo "函數第一個入參:$1" echo "函數第二個入參:$2" echo "傳遞到腳本的參數個數:$#" echo "所有參數:" printf "+ %s\n" "$*" echo "腳本運行的當前進程 ID 號:$$" echo "后台運行的最后一個進程的 ID 號:$!" echo "所有參數:" printf "+ %s\n" "$@" echo "Shell 使用的當前選項:$-" runner echo "runner 函數的返回值:$?" } paramsFunction 1 "abc" "hello, \"zp\"" # Output: # 函數第一個入參:1 # 函數第二個入參:abc # 傳遞到腳本的參數個數:3 # 所有參數: # + 1 abc hello, "zp" # 腳本運行的當前進程 ID 號:26400 # 后台運行的最后一個進程的 ID 號: # 所有參數: # + 1 # + abc # + hello, "zp" # Shell 使用的當前選項:hB # runner 函數的返回值:0

9. Shell 擴展

擴展 發生在一行命令被分成一個個的 記號(tokens) 之后。換言之,擴展是一種執行數學運算的機制,還可以用來保存命令的執行結果,等等。

感興趣的話可以閱讀關於 shell 擴展的更多細節

大括號擴展

大括號擴展讓生成任意的字符串成為可能。它跟 文件名擴展 很類似,舉個例子:

echo beg{i,a,u}n ### begin began begun

大括號擴展還可以用來創建一個可被循環迭代的區間。

echo {0..5} ### 0 1 2 3 4 5 echo {00..8..2} ### 00 02 04 06 08

命令置換

命令置換允許我們對一個命令求值,並將其值置換到另一個命令或者變量賦值表達式中。當一個命令被`$()`包圍時,命令置換將會執行。舉個例子:

now=`date +%T`
### or now=$(date +%T) echo $now ### 19:08:26

算數擴展

在 bash 中,執行算數運算是非常方便的。算數表達式必須包在$(( ))中。算數擴展的格式為:

result=$(( ((10 + 5*3) - 7) / 2 ))
echo $result ### 9

在算數表達式中,使用變量無需帶上$前綴:

x=4
y=7
echo $(( x + y )) ### 11 echo $(( ++x + y++ )) ### 12 echo $(( x + y )) ### 13

單引號和雙引號

單引號和雙引號之間有很重要的區別。在雙引號中,變量引用或者命令置換是會被展開的。在單引號中是不會的。舉個例子:

echo "Your home: $HOME" ### Your home: /Users/<username> echo 'Your home: $HOME' ### Your home: $HOME

當局部變量和環境變量包含空格時,它們在引號中的擴展要格外注意。隨便舉個例子,假如我們用echo來輸出用戶的輸入:

INPUT="A string with strange whitespace." echo $INPUT ### A string with strange whitespace. echo "$INPUT" ### A string with strange whitespace.

調用第一個echo時給了它 5 個單獨的參數 —— $INPUT 被分成了單獨的詞,echo在每個詞之間打印了一個空格。第二種情況,調用echo時只給了它一個參數(整個$INPUT 的值,包括其中的空格)。

來看一個更嚴肅的例子:

FILE="Favorite Things.txt" cat $FILE ### 嘗試輸出兩個文件: `Favorite` 和 `Things.txt` cat "$FILE" ### 輸出一個文件: `Favorite Things.txt`

盡管這個問題可以通過把 FILE 重命名成Favorite-Things.txt來解決,但是,假如這個值來自某個環境變量,來自一個位置參數,或者來自其它命令(findcat, 等等)呢。因此,如果輸入 可能 包含空格,務必要用引號把表達式包起來。

10. 流和重定向

Bash 有很強大的工具來處理程序之間的協同工作。使用流,我們能將一個程序的輸出發送到另一個程序或文件,因此,我們能方便地記錄日志或做一些其它我們想做的事。

管道給了我們創建傳送帶的機會,控制程序的執行成為可能。

學習如何使用這些強大的、高級的工具是非常非常重要的。

10.1. 輸入、輸出流

Bash 接收輸入,並以字符序列或 字符流 的形式產生輸出。這些流能被重定向到文件或另一個流中。

有三個文件描述符:

代碼 描述符 描述
0 stdin 標准輸入
1 stdout 標准輸出
2 stderr 標准錯誤輸出

10.2. 重定向

重定向讓我們可以控制一個命令的輸入來自哪里,輸出結果到什么地方。這些運算符在控制流的重定向時會被用到:

Operator Description
> 重定向輸出
&> 重定向輸出和錯誤輸出
&>> 以附加的形式重定向輸出和錯誤輸出
< 重定向輸入
<< Here 文檔 語法
<<< Here 字符串

以下是一些使用重定向的例子:

### ls的結果將會被寫到list.txt中 ls -l > list.txt ### 將輸出附加到list.txt中 ls -a >> list.txt ### 所有的錯誤信息會被寫到errors.txt中 grep da * 2> errors.txt ### 從errors.txt中讀取輸入 less < errors.txt

10.3. /dev/null 文件

如果希望執行某個命令,但又不希望在屏幕上顯示輸出結果,那么可以將輸出重定向到 /dev/null:

$ command > /dev/null

/dev/null 是一個特殊的文件,寫入到它的內容都會被丟棄;如果嘗試從該文件讀取內容,那么什么也讀不到。但是 /dev/null 文件非常有用,將命令的輸出重定向到它,會起到"禁止輸出"的效果。

如果希望屏蔽 stdout 和 stderr,可以這樣寫:

$ command > /dev/null 2>&1

11. Debug

shell 提供了用於 debug 腳本的工具。

如果想采用 debug 模式運行某腳本,可以在其 shebang 中使用一個特殊的選項:

#!/bin/bash options

options 是一些可以改變 shell 行為的選項。下表是一些可能對你有用的選項:

Short Name Description
-f noglob 禁止文件名展開(globbing)
-i interactive 讓腳本以 交互 模式運行
-n noexec 讀取命令,但不執行(語法檢查)
-t 執行完第一條命令后退出
-v verbose 在執行每條命令前,向stderr輸出該命令
-x xtrace 在執行每條命令前,向stderr輸出該命令以及該命令的擴展參數

舉個例子,如果我們在腳本中指定了-x例如:

#!/bin/bash -x for (( i = 0; i < 3; i++ )); do echo $i done

這會向stdout打印出變量的值和一些其它有用的信息:

$ ./my_script
+ (( i = 0 ))
+ (( i < 3 ))
+ echo 0 0 + (( i++ )) + (( i < 3 )) + echo 1 1 + (( i++ )) + (( i < 3 )) + echo 2 2 + (( i++ )) + (( i < 3 ))

有時我們值需要 debug 腳本的一部分。這種情況下,使用set命令會很方便。這個命令可以啟用或禁用選項。使用-啟用選項,+禁用選項:

:keyboard: 『示例源碼』 debug-demo.sh

# 開啟 debug set -x for (( i = 0; i < 3; i++ )); do printf ${i} done # 關閉 debug set +x # Output: # + (( i = 0 )) # + (( i < 3 )) # + printf 0 # 0+ (( i++ )) # + (( i < 3 )) # + printf 1 # 1+ (( i++ )) # + (( i < 3 )) # + printf 2 # 2+ (( i++ )) # + (( i < 3 )) # + set +x for i in {1..5}; do printf ${i}; done printf "\n" # Output: 12345

12. 更多內容

📓 本文已歸檔到:notes

最后,Stack Overflow 上 bash 標簽下有很多你可以學習的問題,當你遇到問題時,也是一個提問的好地方。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM