由於 bash 是 Linux 標準預設的 shell 直譯器,可以說 bash 是 shell 程式設計的基礎。
本文主要介紹 bash 的語法,對於 linux 指令不做任何介紹。
:notebook: 本文已歸檔到:「blog」 :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 命令,比如 ls
,grep
,cd
,mkdir
,rm
等等。
非互動模式
簡單來說,你可以將 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 直譯器可以識別並執行的命令的普通文字檔案,sh
和bash
是 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 )。
#!
決定了指令碼可以像一個獨立的可執行檔案一樣執行,而不用在終端之前輸入sh
, bash
, python
, php
等。
# 以下兩種方式都可以指定 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
複製程式碼
為什麼dark
和blue
各佔了一行?嘗試用引號包起來:
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 中單雙中括號區別的答案。
共有兩個不同的條件表示式:if
和case
。
if
(1)if
語句
if
在使用上跟其它語言相同。如果中括號裡的表示式為真,那麼then
和fi
之間的程式碼會被執行。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 中有四種迴圈:for
,while
,until
和select
。
for
迴圈
for
與它在 C 語言中的姊妹非常像。看起來是這樣:
for arg in elem1 elem2 ... elemN
do
### 語句
done
複製程式碼
在每次迴圈的過程中,arg
依次被賦值為從elem1
到elemN
。這些值還可以是萬用字元或者大括號擴充套件。
當然,我們還可以把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 的break
和continue
語句來實現。它們可以在任何迴圈中使用。
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;]
}
複製程式碼
:bulb: 說明:
- 函式定義時,
function
關鍵字可有可無。- 函式返回值 - return 返回函式返回值,返回值型別只能為整數(0-255)。如果不加 return 語句,shell 預設將以最後一條命令的執行結果,作為函式返回值。
- 函式返回值在呼叫該函式後通過
$?
來獲得。- 所有函式在使用前必須定義。這意味著必須將函式放在指令碼開始部分,直至 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
來解決,但是,假如這個值來自某個環境變數,來自一個位置引數,或者來自其它命令(find
, cat
, 等等)呢。因此,如果輸入 可能 包含空格,務必要用引號把表示式包起來。
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. 更多內容
:notebook: 本文已歸檔到:「blog」
- awesome-shell,shell 資源列表
- awesome-bash,bash 資源列表
- bash-handbook
- bash-guide ,bash 基本用法指南
- bash-it,為你日常使用,開發以及維護 shell 指令碼和自定義命令提供了一個可靠的框架
- dotfiles.github.io,上面有 bash 和其它 shell 的各種 dotfiles 集合以及 shell 框架的連結
- Runoob Shell 教程
- shellcheck 一個靜態 shell 指令碼分析工具,本質上是 bash/sh/zsh 的 lint。
最後,Stack Overflow 上 bash 標籤下有很多你可以學習的問題,當你遇到問題時,也是一個提問的好地方。