寫給前端的 shell 腳本編程詳解

個人博客原文連接:https://chenfangxu.lap.360.cn/assistive-tools/shell/script.htmlhtml


Shell 腳本(shell script),是一種爲 Shell 編寫的腳本程序,通常文件後綴爲 .shlinux

腳本解釋器

#! 是一個約定的標記,它告訴系統這個腳本須要什麼解釋器來運行,即:使用哪種 shell。#!被稱爲shebang(也稱爲 Hashbang),例如使用 bash:#! /bin/bashshell

新建一個 test.sh 的文件,內容以下:express

#!/bin/bash

echo "Hello World!"

運行 Shell 腳本

第一種方式:做爲可執行程序

一、當前 test.sh 是沒有可執行權限的,首先使腳本文件具備執行權限。npm

# 使腳本文件具備執行權限
chmod +x ./test.sh

二、執行腳本centos

# 執行腳本,需注意要加目錄的標識
./test.sh

# 也能夠用 source 來執行腳本,跟上面的寫法是等價的,可是不須要腳本具備執行權限
source ./test.sh

注意:必定要寫成 ./test/sh ,而不是 test.sh 。運行其餘二進制的程序也是同樣,直接寫 test.sh,Linux 系統會去 PATH 中尋找有沒有叫 test.sh 的,而只有 /bin, /sbin, /usr/bin, /usr/sbin 等在 PATH 中。你的當前目錄一般不在 PATH 中,因此寫成 test.sh 是找不到命令的,要用./test.sh 告訴系統,就在當前目錄找。數組

經過這種方式運行 bash 腳本,第一行必定要寫對,好讓系統(Shell 程序)查找到正確的解釋器。若是是使用標準默認的 shell,能夠省去第一行。bash

第二種方式:做爲解釋器參數

直接運行解釋器,其參數就是 Shell 腳本的文件名。app

/bin/bash test.sh

這種方式運行的腳本,不須要在第一行指定解釋器信息,寫了也沒用。ssh

語法

一、註釋

  • 單行註釋:以 # 開頭,到行尾結束。
  • 多行註釋:以 :<<EOF 開頭,到 EOF 結束
# 這是單行註釋

:<<EOF
這是多行註釋
這是多行註釋
EOF

若是有段代碼要頻繁的註釋和取消註釋,能夠用花括號括起來,定義成一個函數,沒有地方調用這個函數,這塊代碼就不會執行,達到了和註釋同樣的效果。

二、變量

變量類型

  • 局部變量:局部變量是僅在某個腳本內部有效的變量。它們不能被其餘的程序和腳本訪問。
  • 環境變量:環境變量是從父進程中繼承而來的變量,對當前 Shell 會話內全部的程序和腳本均可見。建立它們跟建立局部變量相似,但使用的是 export 關鍵字,shell 腳本也能夠定義環境變量。
  • shell 變量(系統變量):shell 變量是由 shell 程序設置的特殊變量。shell 變量中有一部分是環境變量,有一部分是局部變量,這些變量保證了 shell 的正常運行。

變量語法

一、聲明變量

可使用等號操做符爲變量賦值:varName=value,varName 是變量名,value 是賦值給變量的值。

變量名的命名規則:

  • 首字母必須爲字母(a-z,A-Z),剩下的部分只能使用英文字母,數字下劃線
  • 中間不能有空格,可使用下劃線,若是有空格,必須使用單引號或雙引號
  • 不能使用標點符號
  • 不能使用 shell 關鍵字
#!/bin/bash

fruit=apple
count=5

注意:varName=value的等號兩邊沒有空格,變量值若是有空格,須要用引號包住。

二、訪問變量

訪問變量的語法形式爲:${varName}$varName,變量名外面的花括號是可選的,加不加都行,加花括號是爲了幫助解釋器識別變量的邊界(推薦加花括號)。

#!/bin/bash

fruit=apple
count=5
echo "We have $count ${fruit}s"
#輸出:We have 5 apples

由於 Shell 使用空白字符來分隔單詞,因此上面的例子中須要加上花括號來告訴 Shell 這裏的變量名是 fruit,而不是 fruits

注意:使用單引號時,變量不會被擴展(expand),仍依照原樣顯示。這意味着 echo '$var'會顯示 $var。使用雙引號時,若是$var 已經定義過,那麼 echo "$var"會顯示出該變量的值,若是沒有定義過,則什麼都不顯示。

三、只讀變量

使用 readonly 命令能夠將變量定義爲只讀變量,只讀變量的值不能被改變

#!/bin/bash

fruit=apple
echo $fruit
readonly fruit
#fruit=banana  #若是放開註釋,執行時會報錯

四、刪除變量

使用 unset 命令能夠刪除變量,變量被刪除後不能再次使用。unset 命令不能刪除只讀變量

#!/bin/bash

fruit=apple
echo $fruit
#輸出: apple

unset fruit
echo $fruit
#輸出: (空)

Shell 特殊變量(系統變量)

上面講過變量名的命名規則,可是還有一些包含其餘字符的變量有特殊含義,這樣的變量被稱爲特殊變量。

變量 含義
$0 當前腳本的文件名
$n 傳遞給腳本或函數的參數。n 是一個數字,表示第幾個參數。例如,第一個參數是\$1
$# 傳遞給腳本或函數的參數個數
$* 傳遞給腳本或函數的全部參數
$@ 傳遞給腳本或函數的全部參數,被雙引號("")包含時,與\$\*稍有不一樣
$FUNCNAME 函數名稱(僅在函數內值)
$? 上個命令的退出狀態,或函數的返值
$- 顯示 shell 使用的當前選項(flag),後面擴展中檢測是否爲交互模式時會用到
$$ 當前 Shell 進程 ID。對於 Shell 腳本,就是這些腳本所在的進程 ID
$! 最後一個後臺運行的進程 ID 號
命令行參數:運行腳本時傳遞給腳本的參數成爲命令行參數,命令行參數用 $n 表示。
#!/bin/bash

# ./test.sh

echo "文件名:$0"
echo "第一個命令行參數:$1"
echo "第二個命令行參數:$2"
echo "傳入的所有參數:$@"
echo "傳入的所有參數:$*"
echo "所有參數的數量:$#"

執行命令:./test.sh Linux Shell,結果爲:

文件名:./test.sh
第一個命令行參數:Linux
第二個命令行參數:Shell
傳入的所有參數:Linux Shell
傳入的所有參數:Linux Shell
所有參數的數量:2

$? 能夠獲取上一個命令的退出狀態。所謂退出狀態,就是上一個命令執行後的返回結果。退出狀態是一個數字,通常狀況下,大部分命令執行成功會返回 0,失敗會返回 1。$? 也能夠表示函數的返回值。

三、字符串

字符串引號

shell 字符串可使用單引號 '' ,也可使用雙引號"" , 也能夠不用引號。

  • 單引號:不識別變量,單引號中間不能出現單獨的單引號(使用轉義字符轉義也不行),能夠成對出現實現字符串拼接。
name='world'

before='hello,'${name}'!' #使用單引號拼接字符串
after='hello,${name}!'  #單引號中變量不解析

echo ${before}_${after}
# 輸出:hello,world!_hello,${name}!
  • 雙引號:能夠識別變量,雙引號中能夠出現用轉義字符轉義的雙引號
name="\"shell\""  #雙引號內容許出現被轉義的雙引號

before="hello,"${name}"!" #使用雙引號拼接字符串
after="hello,${name}!" #雙引號中變量會解析

echo ${before}_${after}
# 輸出:hello,"shell"!_hello,"shell"!
設置一個字符串變量,下面的都是對這個變量的操做
file=/dir1/dir2/dir3/my.file.txt

${#var}:得到變量值的長度

echo ${#file}
# 輸出:27

${varx}:經過索引位置截取子字符串

echo ${file:0:5} #截取最左側的5個字符
# 輸出:/dir1

echo ${file:5:5} #從第6個字符開始,截取5個字符
# 輸出:/dir2

${var#}${var##}:刪除字符串左側的值

echo ${file#*/} #刪除第一個 / 及其左側的字符串
# 輸出:dir1/dir2/dir3/my.file.txt

echo ${file##*/} #刪除最後一個 / 及其左側的字符串
# 輸出:my.file.txt

echo ${file#*.} #刪除第一個 . 及其左側的字符串
# 輸出:file.txt

echo ${file##*.} #刪除最後一個 . 及其左側的字符串
# 輸出:txt

${var%}${var%%}:刪除字符串右側的值

echo ${file%/*} #刪除最後一個 / 及其右側的字符串
# 輸出:/dir1/dir2/dir3

echo ${file%%/*} #刪除第一個 / 及其右側的字符串
# 輸出:(空值)

echo ${file%.*} #刪除最後一個 . 及其右側的字符串
# 輸出:/dir1/dir2/dir3/my.file

echo ${file%%.*} #刪除第一個 . 及其右側的字符串
#輸出:/dir1/dir2/dir3/my

${var:-word}:若是變量 var 爲空、沒有定義或已被刪除(unset),那麼返回 word,但不改變 var 的值。

echo ${var:-"var is not set"}
#輸出:var is not set

echo "var is ${var}"
#此時 var 仍是沒有定義,因此輸出:var is

${var:=word}:若是變量 var 爲空、沒有定義或者已被刪除,那麼返回 word,並將 var 的值設置爲 word。

echo ${var:="var is not set"}
#輸出:var is not set

echo "var is ${var}"
#此時 var 已經定義爲var is not set 了,因此輸出:var is var is not set

${var:?message}:若是變量 var 爲空、沒有定義或者已被刪除,那麼將消息 message 送到標準錯誤輸出。

能夠用來檢測變量 var 是否能夠被正常賦值。若此替換出如今 shell 腳本中,那麼腳本將中止運行。

${var:+word}:若是變量 var 被定義,那麼返回 word,但不改變 var 的值。

數組

數組是能夠存儲多個值的變量,這些值能夠單獨引用,也能夠做爲整個數組來引用。數組的下標從 0 開始,下標能夠是整數或算數表達式,其值應該大於等於 0。

建立數組

numbers=(one two three four five)

#建立數組時指明下標
colors=([1]=red [0]=yello [2]=blue)

訪問數組

訪問單個元素

echo ${numbers[2]}
#輸出:three

訪問數組的全部元素

echo ${colors[*]}
#輸出:yello red blue

echo ${colors[@]}
#輸出:yello red blue

${colors[*]}${colors[@]}有些細微的差異,在將數組中的每一個元素單獨一行輸出的時候,有沒有被引號包住會有不一樣的差異,在引號內,${colors[@]}將數組中的每一個元素擴展爲一個單獨的參數,數組元素中的空格得以保留。

訪問數組部分元素

# :0:2 去除數組中從0開始,長度爲2的數組元素
echo ${colors[@]:0:2}
#輸出:yello red

數組的長度

echo ${#colors[@]}
#輸出:3

數組中添加元素

colors=(white "${colors[@]}" green black)

echo ${colors[@]}
#輸出:white yello red blue green black
echo ${#colors[@]}
#輸出:6

數組中刪除元素

unset colors[2]

echo ${colors[@]}
#輸出:white yello blue green black
echo ${#colors[@]}
#輸出:5

完整的代碼示例:

#!/bin/bash


numbers=(one two three four five)

colors=([1]=red [0]=yello [2]=blue)

echo ${numbers[2]}

echo ${colors[*]}

echo ${colors[@]}

echo ${colors[@]:0:2}

echo ${#colors[@]}

colors=(white "${colors[@]}" green black)

echo ${colors[@]}

echo ${#colors[@]}

unset colors[2]

echo ${colors[@]}

echo ${#colors[@]}

運算符

Shell 中有不少運算符,包括算數運算符、關係運算符、布爾運算符、字符串運算符和文件測試符。

算數運算符

原生 bash 不支持簡單的數學運算,較爲經常使用的是藉助 expr 來實現數學運算。

算數運算符列表,變量 a 是 10 變量 b 是 50

運算符 說明 舉例
+ 加法 expr ${a} + ${b} 結果爲 60
- 減法 expr ${b} - ${a} 結果爲 40
\* 乘法 expr ${a} \\* ${b} 結果爲 500
/ 除法 expr ${b} / ${a} 結果爲 5
% 取餘 expr ${b} % ${a} 結果爲 0
= 賦值 a=$b 就是正常的變量賦值

示例代碼以下:

#!/bin/bash

a=10
b=50

value=`expr ${a} + ${b}`
echo "a + b = ${value}"

value=`expr ${b} - ${a}`
echo "b - a = ${value}"

value=`expr ${a} \* ${b}`
echo "a * b = ${value}"

value=`expr ${b} / ${a}`
echo "b / a = ${value}"

value=`expr ${b} % ${a}`
echo "b % a = ${value}"

#輸出
#a + b = 60
#b - a = 40
#a * b = 500
#b / a = 5
#b % a = 0

注意:

  1. 表達式和運算符之間要有空格,例如1+1是錯誤的,必須寫成1 + 1
  2. 完整的表達式要用反引號 ` 包住
  3. 條件表達式要放在方括號之間,而且要有空格,例如 [${a}==${b}]是錯誤的,必須寫成 [ ${a} == ${b} ]

條件運算符(關係運算符)

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

關係運算符列表,變量 a 是 10 變量 b 是 50

運算符 說明 舉例
-eq 檢測兩個數是否相等,相等返回 true [ ${a} -eq ${b} ] 返回 false
-ne 檢測兩個數是否不相等,不相等返回 true [ ${a} -ne ${b} ] 返回 true
-gt 檢測左邊的數是否大於右邊的數,若是是,返回 true [ ${a} -gt ${b} ] 返回 false
> 跟 -gt 同樣,不過由於兼容性問題,可能要在 [[]] 表達式中使用 [[ ${a} > ${b} ]] 返回 false
-lt 檢測左邊的數是否小於右邊的數,若是是,返回 true [ ${a} -lt ${b} ] 返回 true
< 跟 -lt 同樣,不過由於兼容性問題,可能要在 [[]] 表達式中使用 [[ ${a} < ${b} ]] 返回 true
-ge 檢測左邊的數是否大於等於右邊的數,若是是,返回 true [ ${a} -ge ${b} ] 返回 false
-le 檢測左邊的數是否小於等於右邊的數,若是是,返回 true [ ${a} -le ${b} ] 返回 true

實例代碼以下:

!/bin/bash

a=10
b=50

if [ ${a} -eq ${b} ]; then
  echo "${a} -eq ${b} : a 等於 b"
else
  echo "${a} -eq ${b} : a 不等於 b"
fi
#輸出:10 -eq 50 : a 不等於 b

if [ ${a} -ne ${b} ]; then
  echo "${a} -ne ${b} : a 不等於 b"
else
  echo "${a} -ne ${b} : a 等於 b"
fi
#輸出:10 -ne 50 : a 不等於 b

if [ ${a} -gt ${b} ]; then
  echo "${a} -gt ${b} : a 大於 b"
else
  echo "${a} -gt ${b} : a 小於 b"
fi
#輸出:10 -gt 50 : a 小於 b
if [[ ${a} > ${b} ]]; then
  echo "${a} > ${b} : a 大於 b"
else
  echo "${a} > ${b} : a 小於 b"
fi
#輸出:10 > 50 : a 小於 b

if [ ${a} -lt ${b} ]; then
  echo "${a} -lt ${b} : a 小於 b"
else
  echo "${a} -lt ${b} : a 大於 b"
fi
#輸出:10 -lt 50 : a 小於 b
if [[ ${a} < ${b} ]]; then
  echo "${a} < ${b} : a 小於 b"
else
  echo "${a} < ${b} : a 大於 b"
fi
#輸出:10 < 50 : a 小於 b

if [ ${a} -ge ${b} ]; then
  echo "${a} -ge ${b} : a 大於等於 b"
else
  echo "${a} -ge ${b} : a 小於等於 b"
fi
#輸出:10 -ge 50 : a 小於等於 b

if [ ${a} -le ${b} ]; then
  echo "${a} -le ${b} : a 小於等於 b"
else
  echo "${a} -le ${b} : a 大於等於 b"
fi
#輸出:10 -le 50 : a 小於等於 b

條件運算符(布爾運算符、邏輯運算符、字符串運算符)

條件運算符列表,變量 a 是 10, 變量 b 是 50,變量 x 是 "abc",變量 y 是 "efg"

運算符 說明 舉例
! 非運算 [ ! false ] 返回 true
-o 或運算 [ ${a} -eq 10 -o ${b} -eq 100 ] 返回 true
-a 與運算 [ ${a} -eq 10 -a ${b} -eq 50 ] 返回 true
&& 跟-a 相似,邏輯的 AND,不過須要使用 [[]] 表達式 [[ ${a} -eq 10 && ${b} -eq 50 ]] 返回 true
= 檢測兩個數字或字符串是否相等,相等返回 true [ ${a} = ${b} ] 返回 false
!= 檢測兩個數字或字符串是否相等,不相等返回 true [ ${a} != ${b} ]返回 true
== 相等。比較兩個數字或字符串,若是相等返回 true(不推薦使用,有兼容性問題) [ ${a} == ${b} ] 返回 false
-z 檢測字符串長度是否爲 0,爲 0 返回 true [ -z ${x} ] 返回 false
-n 檢測字符串長度是否爲 0,不爲 0 返回 true [ -n ${x} ] 返回 true
var 檢測變量是否存在或不爲空,存在或不爲空返回 true [ $s ] 返回 false

代碼示例以下:

#!/bin/bash

a=10
b=50
x="abc"
y="edf"

#單 []
if [ ${a} -eq 10 -a ${b} -eq 50 ]; then
  echo "${a} -eq 10 -a ${b} -eq 50 : 返回 true"
else
  echo "${a} -eq 10 -a ${b} -eq 50 : 返回 false"
fi
#輸出:10 -eq 10 -a 50 -eq 50 : 返回 true

#雙 []
if [[ ${a} -eq 10 && ${b} -eq 50 ]]; then
  echo "${a} -eq 10 && ${b} -eq 50 : 返回 true"
else
  echo "${a} -eq 10 && ${b} -eq 50 : 返回 false"
fi
#輸出:10 -eq 10 && 50 -eq 50 : 返回 true

if [ ${a} = ${b} ]
then
  echo "a 和 b 相等"
fi

if [ ${a} != ${b} ]
then
  echo "a 和 b 不相等"
fi
#a 和 b 不相等

if [ -z ${x} ]; then
  echo "-z ${x}:字符串長度爲0 "
else
  echo "-z ${x}:字符串長度不爲0 "
fi
#輸出:-z abc:字符串長度不爲0

if [ -n ${y} ]; then
  echo "-z ${y}:字符串長度不爲0 "
else
  echo "-z ${y}:字符串長度爲0 "
fi
#輸出:-z edf:字符串長度不爲0

if [ $x ];then
  echo "${x}:不是空字符串"
else
  echo "${x}:是空字符串"
fi
#輸出:abc:不是空字符串

if [ $s ];then
  echo '${s}:存在'
else
  echo '${s}:不存在'
fi
#輸出:${s}:不存在

文件目錄判斷運算符

文件目錄判斷運算符列表

運算符 說明
-f filename 判斷文件是否存在,當 filename 存在且是正規文件時(既不是目錄,也不是設備文件)返回 true
-d pathname 判斷目錄是否存在,當 pathname 存在且是目錄時返回 true
-e pathname 判斷【某個東西】是否存在,當 pathname 指定的文件或目錄存在時返回 true
-a pathname 同上,已通過時,並且使用的時候還有另一個與的邏輯,容易混淆
-s filename 判斷是不是一個非空文件,當 filename 存在而且文件大小大於 0 時返回 true
-r pathname 判斷是否可讀,當 pathname 指定的文件或目錄存在而且可讀時返回 true
-x pathname 判斷是否可執行,當 pathname 指定的文件或目錄存在而且可執行時返回 true
-w pathname 判斷是否可寫,當 pathname 指定的文件或目錄存在而且可寫時返回 true
-b filename 判斷是不是一個塊文件,當 filename 存在且是塊文件時返回 true
-c filename 判斷是不是一個字符文件,當 filename 存在且是字符文件時返回 true
-L filename 判斷是不是一個符號連接,當 filename 存在且是符號連接時返回 true
-u filename 判斷文件是否設置 SUID 位,SUID 是 Set User ID
-g filename 判斷文件是否設置 SGID 位,SGID 是 Set Group ID

示例代碼以下:

#!/bin/bash

file="/etc/hosts"

if [ -f ${file} ]; then
  echo "${file}:是一個普通文件"
else
  echo "${file}:不是一個普通文件"
fi
#輸出:/etc/hosts:是一個普通文件

if [ -d ${file} ]; then
  echo "${file}:是個目錄"
else
  echo "${file}:不是目錄"
fi
#輸出:/etc/hosts:不是目錄

if [ -e ${file} ]; then
  echo "${file}:文件存在"
else
  echo "${file}:文件不存在"
fi
#輸出:/etc/hosts:文件存在

if [ -s ${file} ]; then
  echo "${file}:文件不爲空"
else
  echo "${file}:文件爲空"
fi
#輸出:/etc/hosts:文件不爲空

if [ -r ${file} ]; then
  echo "${file}:文件可讀"
else
  echo "${file}:文件不可讀"
fi
#輸出:/etc/hosts:文件可讀

條件語句

在條件語句中,由 [][[]] 包起來的表達式被稱爲檢測命令基元

if...fi 語句

語法:

if [ expression ]
then
  expression 是 true ,這裏會被執行
fi
#!/bin/bash

if [ 1 = 1 ]
then
  echo "相等"
fi
#輸出:相等

#也能夠寫成一行
if [ "a" = "a" ]; then echo "相等"; fi
#輸出:相等

if...else 常常跟 test 命令結合使用,test命令用於檢查某個條件是否成立,與方括號[]相似(它們兩個在/usr/bin 中是用軟鏈接指向的)。

#!/bin/bash

a=10
b=50

if test ${a} -eq ${b}
then
  echo "a 等於 b"
else
  echo "a 不等於 b"
fi
#輸出:a 不等於 b

if...else...fi

語法:

if [ expression ]
then
  expression 是 true ,這裏會被執行
else
  expression 是 false , 這裏會被執行
fi
#!/bin/bash

if [ 1 = 2 ]
then
  echo "相等"
else
  echo "不相等"
fi
#輸出:不相等

if...elif...fi

語法:

if [ expression1 ]
then
  expression 是 true ,這裏會被執行
elif [ expression2 ]
then
  expression1 是 true ,這裏會被執行
else
  上面的 expression 都是 false , 這裏會被執行
fi
#!/bin/bash

a=10
b=50

if [ ${a} -eq ${b} ]
then
  echo "a 等於 b"
elif [ ${a} -gt ${b} ]
then
  echo "a 大於 b"
else
  echo "a 小於 b"
fi
#輸出:a 小於 b

case...esac

case...esac 與其餘語言中的 switch...case 相似,是一種多分支選擇結構。

case 語句匹配一個值或一個模式,若是匹配成功,執行想匹配的命令。適用於須要面對不少狀況,分別要採起不一樣的措施的狀況。

case 值 in
模式1)
  command1
  command2
  command3
  ;;
模式2)
  command1
  command2
  command3
  ;;
*)
  command1
  command2
  command3
  ;;
esac
#!/bin/bash

echo "輸入1-4的一個數字"
echo "你輸入的數字是:"

read number

case $number in
  1)
  echo "你輸入了1"
  ;;
  2)
  echo "你輸入了2"
  ;;
  3)
  echo "你輸入了3"
  ;;
  4)
  echo "你輸入了4"
  ;;
  *)
  echo "你輸入的不是1-4的數字"
  ;;
esac

#運行後能夠本身輸入數字體驗

注意:能夠在 ) 前用 | 分割多個模式。

循環語句

bash 中有四種循環:for , while , until , select

for 循環

語法:

for 變量 in 列表
do
  command1
  command2
  ...
  commandN
done

語法中的列表是一組值(數字、字符串)組成的序列,每一個值經過空格分隔。這些值還能夠是通配符或大括號擴展,例如 *.sh{1..5}

#!/bin/bash

for i in 1 2 3 4 5
do
  echo $i
done

# 寫在一行
for i in {1..5}; do echo $i ; done

while 循環

while 循環會不斷的檢測一個條件,只要這個條件返回 true,就執行一段命令。被檢測的條件跟 if 中的同樣。while 也可用於從輸入文件中讀取數據。

語法:

while [[ condition ]]
do
  若是 condition 是 true ,這裏的命令會執行
done
#!/bin/bash

x=0
y=10

while [ ${x} -lt 5 ]
do
  echo $x
  x=`expr ${x} + 1`
done

# do 也跟條件寫在一行,前面須要加分號 ;
while [ ${y} -gt 5 ]; do echo $y; y=`expr ${y} - 1`; done

until 循環

until 循環是檢測一個條件,只要條件是 false 就會一直執行循環,直到條件條件爲 true 時中止。它跟 while 正好相反。

#!/bin/bash

x=0

until [ ${x} -eq 5 ]; do
  echo ${x}
  x=`expr ${x} + 1`
done

select 循環

select 循環的語法跟 for 循環基本一致。它幫助咱們組織一個用戶菜單。

語法:

select 變量 in 列表
do
  執行相應的命令
done

select 會打印列表的值以及他們的序列號到屏幕上,以後會提示用戶選擇,用戶一般看到的提示是\$?,用戶輸入相應的信號,選擇的結果會被保存到變量中。

#!/bin/bash

#PS3——shell腳本中使用select時的提示符
PS3="選擇你要安裝的包管理工具,輸入序號:"

select ITEM in bower npm gem pip
do
  echo "輸入的包名稱是:\c" && read PACKAGE
  case ${ITEM} in
    bower) echo "模擬 bower install ${PACKAGE}" ;;
    npm) echo "模擬 npm install ${PACKAGE}" ;;
    gem) echo "模擬 gem install ${PACKAGE}" ;;
    pip) echo "模擬 pip install ${PACKAGE}" ;;
    *) echo "包管理工具選擇錯誤" ;;
  esac
  break #跳出循環
done

break 命令

break 命令容許跳出全部循環(終止執行後面的全部循環)。在嵌套循環中 break 命令後面還能夠跟一個整數,表示跳出幾層循環。

#!/bin/bash

# 當 x 等於 2,而且 y 等於 0,就跳出循環
for x in 1 2 3
do
  for y in 0 5
  do
   if [ ${x} -eq 2 -a ${y} -eq 0 ]
   then
     echo "x 等於 ${x},y 等於 ${y}"
     break 2
   else
    echo "${x} ${y}"
   fi
  done
done

continue 命令

continue 命令跟 break 命令相似,只有一點差異,它不會跳出全部循環,僅僅跳出當前循環。一樣,continue 後面也能夠跟一個數字,表示跳出第幾層循環。

#!/bin/bash

# 當 x 等於 2,而且 y 等於 0,就跳出循環
for x in 1 2 3
do
  for y in 0 5
  do
   if [ ${x} -eq 2 -a ${y} -eq 0 ]
   then
     continue 2
   else
    echo "${x} ${y}"
   fi
  done
done

函數

  • shell 函數必須先定義後使用,調用函數僅使用其函數名便可。
  • 函數定義時,function關鍵字無關緊要
  • 函數返回值:能夠顯式的使用 return 語句,返回值類型只能爲整數(0-255)。若是不加 return 語句,會默認將最後一條命令運行結果做爲返回值。
  • 函數返回值在調用該函數後,經過 $? 得到。
語法:中括號內表示可選

[function] function_name () {
  在這裏執行命令
  [return value]
}
#!/bin/bash

hello () {
  echo "hello"
  world #函數嵌套
}

world () {
  echo "world"
}

hello

參數

位置參數是在調用一個函數並傳給它參數時建立的變量,見上文 Shell 特殊變量。

#!/bin/bash

funWithParam () {
  echo "第1個參數:$1"
  echo "第2個參數:$2"
  echo "第3個參數:$3"
  echo "錯誤的獲取第10個參數:$10"
  # $10 不能獲取第10個參數,須要用 ${10},當 n>=10 時,須要使用 ${n} 獲取參數。(其中有兼容性,某些Shell解釋器兩種都能獲取到)
  echo "正確的獲取第10個參數:${10}"
  echo "獲取第11個參數:${11}"
  echo "獲取傳參的個數:$#"
  echo "獲取全部的傳參:$*"
  echo "當前函數的名稱是:$FUNCNAME"
}

funWithParam 1 2 3 4 5 6 7 8 9 34 73

輸入輸出重定向

Unix 命令默認從標準輸入設備(stdin)獲取輸入,將結果輸出到標準輸出設備(stdout)顯示。通常狀況下,標準輸入設備就是鍵盤,標準輸出設備就是顯示器。

輸入輸出流

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

通常狀況下,每一個 Unix/Linux 命令都會打開三個文件:標準輸入文件、標準輸出文件、標準錯誤文件,三個文件描述符:

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

重定向

重定向讓咱們能夠控制一個命令的輸入來自哪裏,輸出結果到什麼地方。

輸出重定向:命令的輸出不只能夠是顯示器,還能夠很容的轉義到文件,這被稱爲輸出重定向。

語法:

command > file  此語法會覆蓋文件內容

command >> file 若是不但願文件被覆蓋,可使用 >> 追加到文件末尾

輸入重定向:使 Unix 命令也能夠從文件獲取輸入,這樣原本要從鍵盤獲取輸入的命令會轉移到文件讀取內容。

語法:
command < file

有一個文件是 test.sh,用兩種方式輸出文件的行數

wc -l ./test.sh
#輸出:14 ./test.sh

wc -l < ./test.sh
#輸出:14  沒有文件名

第一個例子會輸出文件名,第二個不會,由於它僅僅知道從標準輸入讀取的內容。

如下操做符在控制流的重定向時會被用到:

操做符 描述
> 重定向輸出
>> 將輸出已追加的方式重定向
>& 將兩個輸出文件合併
<& 將兩個輸入文件合併
< 重定向輸入
<< Here 文檔語法(見下文擴展),將開始標記 tag 和結束標記 tag 之間的內容做爲輸入
<<< Here 字符串

若是但願 stderr 重定向到 file,能夠這樣寫:

command 2 > file

若是但願將 stdout 和 stderr 合併後重定向的 file,能夠這樣寫:

#&[n] 表明是已經存在的文件描述符,&1 表明輸出 &2表明錯誤輸出 &-表明關閉與它綁定的描述符
command > file 2 >&1

若是但願 stdin 和 stdout 都重定向,能夠這樣寫:

command < file1 > file2

#例如:
cat < test.sh  > catfile

#提一下 << 這個連續兩個小符號, 他表明的是『結束的輸入字符』的意思。這樣當空行輸入eof字符,輸入自動結束,不用ctrl+D
cat <<eof >catfile

若是但願執行某個命令,但又不但願在屏幕上顯示輸出結果,那麼能夠將輸出重定向到 /dev/null。

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

#test1.sh 沒有的狀況下,將錯誤輸出信息關閉掉
ls test.sh test1.sh 2>/dev/null

ls test.sh test1.sh 2>&-

#關閉全部輸出
ls test.sh test1.sh  1>&- 2>&-
ls test.sh test1.sh  2>/dev/null 1>/dev/null

#將錯誤輸出2 綁定給 正確輸出 1,而後將 正確輸出 發送給 /dev/null設備  這種經常使用
ls test.sh test1.sh >/dev/null 2>&1
#& 表明標準輸出 ,錯誤輸出 將全部標準輸出與錯誤輸出 輸入到/dev/null文件
ls test.sh test1.sh &>/dev/null

加載外部腳本

像其餘語言同樣,Shell 也能夠加載外部腳本,將外部腳本的內容合併到當前腳本。shell 中加載外部腳本有兩種寫法。

第一種:. filename

第二種:source filename

兩種方式效果相同,簡單起見,通常使用點號(.),可是!注意點號(.)和文件名中間有一個空格

#!/bin/bash

. ./pre_test.sh

echo $a
# 輸出:100

Debug

全局 Debug

shell 提供了用於 debug 腳本的工具。若是想採用 debug 模式運行某腳本,能夠在其 shebang 中使用一個特殊的選項。(有些 shell 不支持)

#!/bin/bash [options]

或者在執行 Bash 腳本的時候,從命令行傳入這些參數

bash -euxo pipefail test.sh

局部 Debug

有時咱們只須要 debug 腳本的一部分。這種狀況下,使用 set 命令會很方便。這個命令能夠啓用或禁用選項。 使用 - 啓用選項,使用 + 禁用選項。

一、用來在運行結果以前,先輸出執行的那一行命令

set -x
#或者
set -o xtrace

二、執行腳本時,若是遇到不存在的變量會報錯,並中止執行。(默認是忽略報錯的)

set  -u
#或者
set  -o nounset

順便說一下,若是命令行下不帶任何參數,直接運行set,會顯示全部的環境變量和 Shell 函數。

三、執行腳本時,發生錯誤就終止執行。(默認是繼續執行的)

set  -e
#或者
set -o errexit

#能夠用下面是方法
command || exit 1
#或者
command1 && command2

set -e 根據返回值來判斷,一個命令是否運行失敗。可是,某些命令的非零返回值可能不表示失敗,或者開發者但願在命令失敗的狀況下,腳本繼續執行下去。這時能夠暫時關閉 set -e,該命令執行結束後,再從新打開 set -e。

set +e
command1
command2
set -e

#也能夠用下面的方法
command || true

四、管道命令執行失敗,腳本終止執行

管道命令就是多個子命令經過管道運算符(|)組合成爲一個大的命令。Bash 會把最後一個子命令的返回值,做爲整個命令的返回值。最後一個子命令不失敗,管道命令就老是會執行成功的,所以 set -e 會失效,後面的命令會繼續執行。

set -o pipefail 用來解決這個狀況,只要一個子命令失敗,整個管道命令就會失敗,腳本就會終止執行。

set -eo pipefail

上面的命令能夠放在一塊兒使用:

set -euxo pipefail
#或者
set -eux
set -o pipefail

擴展

腳本解釋器在環境變量中指定

除了比較常見的用路徑指定腳本解釋器的方式,還有一種是指定環境變量中的腳本解釋器。

指定腳本解釋器的路徑
#!/bin/bash`

指定環境變量中的腳本解釋器
#!/usr/bin/env bash

這樣作的好處是,系統會自動在 PATH 環境變量中查找指定的程序(如例子中的 bash)。由於程序的路徑是不肯定的,好比安裝完新版本的 bash 後,咱們有可能會把這個新的路徑添加到PATH中,來「隱藏」老版本的 bash。因此操做系統的PATH變量有可能被配置爲指向程序的另外一個版本,若是仍是直接用 #!/bin/bash,那麼系統仍是會選擇老版本的 bash 來執行腳本,若是用#!/usr/bin/env bash,就會使用新版本了。

環境變量

全部的程序,包括 Shell 啓動的程序運行時均可以訪問的變量就是環境變量。在 shell 腳本中使用 export 能夠定義環境變量,可是隻在當前運行的 shell 進程中有效,結束進程就沒了。若是想持久化,須要將環境變量定義在一些列配置文件中。

配置文件的加載順序和 shell 進程是否運行在 Interactive 和 Login 模式有關。

交互和非交互模式(Interactive & Non-Interactive)

  • Interactive 模式:一般是指讀寫數據都是從用戶的命令行終端(terminal),用戶輸入命令,並在回車後當即執行的 shell。
  • Non-Interactive 模式:一般是指執行一個 shell 腳本,或 bash -c 執行命令

檢測當前 shell 運行的環境是否是 Interactive 模式

[[ $- == *i* ]] && echo "Interactive" || echo "Non-interactive"

登陸和非登陸模式(Login & Non-Login)

  • Login 模式:應用在終端登錄時,ssh 鏈接時,su --login <username> 切換用戶時,指的是用戶成功登陸後開啓的 Shell 進程,此時會讀取 /etc/passwd 下用戶所屬的 shell 執行。
  • Non-Login 模式:應用在直接運行 bash 時,su <username> 切換用戶時(前面沒有加 --login)。指的是非登陸用戶狀態下開啓的 shell 進程。

檢測當前 shell 運行的環境是否是 Login 模式

shopt -q login_shell && echo "Login shell" || echo "Not login shell"

#若是是zsh,沒有shopt命令
[[ -o login ]] && echo "Login shell" || echo "Not login shell"

進入 bash 交互模式時也能夠用 --login 參數來決定是不是登陸模式:

$> bash
$> shopt -q login_shell && echo "Login shell" || echo "Not login shell"
Not login shell
$> exit
$> bash --login
$> shopt -q login_shell && echo "Login shell" || echo "Not login shell"
Login shell
$> exit

Login 模式模式下能夠用 logout 和 exit 退出,Non-Login 模式下只能用 exit 退出。

配置文件(啓動文件)加載順序

bash 支持的配置文件有 /etc/profile、~/.bash.rc 等。

如上圖加載順序所示

  • Interactive&Login 模式:/etc/profile —>( ~/.bash_profile, ~/.bash_login, ~/.profile)其中之一 —>~/.bash_loginout(退出 shell 時調用)
  • Interactive&Non-Login 模式:/etc/bash.bashrc —>~/.bashrc
  • Non-Interactive 模式:一般就是執行腳本(script)的時候,此時配置項是從環境變量中讀取和執行的,也就是 env 或者 printenv 命令輸出的配置項。

如今的系統通常都沒有 ~/.bash_profile 文件了,只保留 ~/.bashrc 文件,全部的系統裏,~/.bash_profile 都會有這樣的邏輯,避免登錄時 ~/.bashrc 被跳過的狀況:

# login shell will execute this
if [ -n "$BASH_VERSION" ]; then
    # include .bashrc if it exists
    if [ -f "$HOME/.bashrc" ]; then
        . "$HOME/.bashrc"
    fi
fi

在發行版的 Linux 系統中,Interactive&Login 模式下的 ~/.bash_profile, ~/.bash_login, ~/.profile 並不必定是三選一,看一下這三個腳本的內容會發現他們會繼續調用下一個它想調用的配置文件,這樣就能夠避免配置項可能須要在不一樣的配置文件屢次配置。如 centos7.2 中 ~/.bash_profile 文件中實際調用了 ~/.bashrc 文件。

# .bash_profile

# Get the aliases and functions
if [ -f ~/.bashrc ]; then
    . ~/.bashrc
fi

# User specific environment and startup programs

PATH=$PATH:$HOME/.local/bin:$HOME/bin

export PATH

如上圖所示,開啓一個 Shell 進程時,有一些參數的值也會影響到配置文件的加載。如--rcfile,--norc 等。

經常使用的 shell 環境變量:

變量名 描述
PATH 命令搜索路徑,以冒號爲分隔符
HOME 用戶主目錄的路徑名,是 cd 命令的默認參數
SHELL 當前運行的 Shell 的全路徑名
TERM 終端類型
LOGNAME 當前的登陸名
PWD 當前工做目錄
#輸出個別的環境變量值的兩種方式

printenv HOME

echo $HOME

全局變量是對全部用戶都須要使用的變量,能夠將新的變量或修改過的變量設置放在/etc/profile文件中,但升級了發行版該文件也會更新,因此這點要注意 (對全部用戶)。

最好是在/etc/profile.d目錄中建立一個以.sh結尾的文件,把全部新的變量或修改過的變量所有放在此文件中(對全部用戶)。

對於存儲我的用戶永久性bash shell變量的地方是$HOME/.bashrc文件。這一點適用於全部類型的shell進程(僅對當前用戶)。

$*$@ 的區別

$*$@ 都表示傳遞給函數或腳本的全部參數,不被雙引號("")包含時,都是以"$1" "$2" ... "\\$n"形式把全部參數一個一個單獨輸出。

可是當他們被雙引號包含是,"$*" 會將全部的參數做爲一個總體,以"$1 $2 ... $n"的形式輸出全部參數。"$@" 仍是跟以前同樣,把全部參數分開,一個一個的輸出。

例如:./test.sh a b c d

#/bin/bash

echo "打印出沒有引號的 $*"
for var in $*
do
echo "$var"
done
#輸出:打印出沒有引號的 $*
# a
# b
# c
# d

echo "打印出有引號的 \"$*\""
for var in "$*"
do
echo "$var"
done
#輸出:打印出有引號的 "$*"
# a b c d


echo "打印出沒有引號的 $@"
for var in $@
do
echo "$var"
done
#輸出:打印出沒有引號的 $@
# a
# b
# c
# d

echo "打印出有引號的 \"$@\""
for var in "$@"
do
echo "$var"
done
#輸出:打印出有引號的 "$@"
# a
# b
# c
# d

Shell 中的替換

轉義字符替換

使用 echo 命令時,使用 -e 能夠對轉義字符進行替換。使用 -E 能夠禁止轉義,默認也是不轉義的;使用 -n 選項能夠禁止插入換行符。

轉義字符 含義
\b 退格(刪除鍵)
\f 換頁(FF),將當前位置移到下頁開頭
\n 換行
\c 顯示不換行
\r 回車
\t 水平製表符(tab 鍵)
\v 垂直製表符
#/bin/bash

a=1
b=2

echo -e "${a}\n${b}" #輸出:1

# 2

命令替換

命令替換是指 Shell 能夠先執行命令,將輸出結果暫時保存,在適當的地方輸出。

命令替換的語法是:反引號 ``。

#!/bin/bash

DATE=`date`
echo "日期是:\$DATE" #輸出:日期是:Sun Oct 18 16:27:42 CST 2020

() 和 (())

先說一下 ()

在 bash 中,\$()與 ``(反引號)都是用來做命令替換的。先完成引號裏的命令行,而後將其結果替換出來,再重組成新的命令行。

相同點:\$() 與 `` 在操做上,這二者都是達到相應的效果

不一樣點:`` 很容易與''搞混亂,尤爲對初學者來講,而\$( )比較直觀;不過 \$() 有兼容性問題,有些類 Unix 系統不支持。

echo $(expr 1 + 2)

再說 (())

一、(()) 可直接用於整數計算

echo $((1 + 2))

二、(()) 可從新定義變量值,用於判斷條件或計算等

#!/bin/bash

a=10
b=50

((a++))
echo $a
#輸出:11

((a > b)) && echo "a > b"

((a < b)) && echo "a < b"

# 輸出:a < b

三、(()) 可用於進制轉換

\$(())能夠將其餘進制轉成十進制數顯示出來。語法:$((N#xx)),其中,N 爲進制,xx 爲該進制下某個數值,命令執行後能夠獲得該進制數轉成十進制後的值。

echo $((2#110))
#輸出:6
echo $((8#11))
#輸出:9
echo $((16#1a))
#輸出:26

test 、[] 和 [[]]

type 命令檢查

type "test" "[" "[["
#輸出:
#test is a shell builtin
#[ is a shell builtin
#[[ is a reserved word

從上面能夠看出,test[屬於 Shell 的內置命令,[[屬於 Shell 的保留關鍵字。

在使用上,test[是等價的,由於是命令,因此須要跟它的參數使用空格隔開。

test -f /etc/hosts && echo True
#輸出:True

[ -f /etc/hosts ] && echo True
#輸出:True

由於 ] 做爲最後一個參數表示條件結束,而像<>符號會被理解爲重定向,致使錯誤

[ 1 < 2 ]
#輸出:line 13: 2: No such file or directory

[[是關鍵字,可以按照常規的語義理解其中的內容,雙中括號中的表達式看做一個單獨的語句,並返回其狀態碼。

[[ 1 < 2 ]] && echo True || echo False
#輸出:True

推薦使用[[ 來進行各類判斷,能夠避免不少錯誤。

以下展現單中括號會引起的錯誤

[ $a == 1 && $b == 1 ] && echo True || echo False
#輸出:[: missing `]'

#例如$a爲空,就會報語法錯誤,由於 [ 命令拿到的實際上只有 ==、一、] 三個參數
[ $a == 1 ]
#輸出:[: ==: unary operator expected

Here Document

Here Document 能夠理解爲「嵌入文檔」。Here Document 是 Shell 中的一種特殊的重定向方式,它的基本形式以下:

command <<delimiter
document
delimiter

做用是將兩個 delimiter 之間的內容(document)做爲輸入傳遞給 command。

注意:

  • 結尾的 delimiter 必定要頂格寫,前面不能有任何字符,後面也不能有任何字符,包括空格和 tab 縮進。
  • 開始的 delimiter 先後的空格會被忽略掉。
#!/bin/bash

wc -l << EOF
line 1
line 2
line 3
EOF #輸出:3

參考文檔