
前言
雖然一直都在用,但有些命令仍是半知半懂的,所以就好好學一下吧。
一些輔助工具:
- shellcheck - Shell 腳本靜態(tài)檢查工具,主流編輯器都有插件。類似 ESLint 的工具。
- zx - Google 出品,用 JavaScript 寫 Shell 腳本。
本文大部分內容來自阮一峰老師的 Bash 腳本教程。
一、Shell 命令格式
$ command [ arg1 ... [ argN ] ]
其中 command 是一個具體的命令或者一個可執(zhí)行文件,arg1... argN 是傳遞給命令的參數(shù),是可選的。
命令與參數(shù),參數(shù)與參數(shù)之間通過「一個空格」隔開。若有「多個空格」,多余空格會被自動忽略,作用相當于一個空格。
$ ls -l
其中 ls 是命令,-l 是參數(shù)。有些參數(shù)是命令的配置項,它們一般以一個「短橫線」開頭,比如上面的 -l。通常配置項參數(shù)有短形式和長形式兩種形式,比如 -l 是短形式,--list 是長形式。兩種寫法作用完全相同,短形式便于輸入,長形式可讀性、語義更好。
通常命令都是一行的,可有些命令較長,寫成多行有利于閱讀和編輯,只要在每行結尾處加上反斜杠 \ 可以,Shell 會將下一行跟當前行一起解析。
$ echo Hello World
# 等同于
$ echo Hello \
World
二、命令的組合與繼發(fā)
命令組合符 &&,前一個命令執(zhí)行成功,才會接著執(zhí)行第二個命令。
$ command1 && command2
命令組合符 ||,前一個命令執(zhí)行失敗,才會接著執(zhí)行第二個命令。
$ command1 || command2
命令結束符 ;(分號),前一個命令執(zhí)行結束后(無論成功與否),接著執(zhí)行第二個命令。命令結束符可使得一行中放置多個命令。
$ clear; ls
管道符 |,前一個命令的輸出作為第二個命令的輸入。
$ command1 | command2
# 相當于
$ command1 > tempfile
$ command2 < tempfile
$ rm tempfile
三、引號
- 單引號:單引號用于保留字符的字面含義,各種特殊字符在單引號里面,都會變?yōu)槠胀ㄗ址?/li>
- 雙引號:比單引號寬松,大部分特殊字符在雙引號里面,都會失去特殊含義,變成普通字符。但是,三個特殊字符除外:美元符號(
$)、反引號(`)和反斜杠(\)。這三個字符在雙引號之中,依然有特殊含義,會被 Bash 自動擴展。
$ echo '$USER'
$USER
$ echo "$USER"
frankie
換行符在雙引號之中,會失去特殊含義,Bash 不再將其解釋為命令的結束,只是作為普通的換行符。所以可以利用雙引號,在命令行輸入多行文本。
$ echo "hello
world"
hello
world
echo 發(fā)音 [?ekō](才發(fā)現(xiàn)原來一直讀錯了,慚愧)。其參數(shù) -e 會解析引號中的特殊字符(比如換行符 \n)。若在 CLI 中直接輸入 echo 命令 \n 也會解析為換行符,而不是普通的 \n 字符串。
$ echo -e "Hello\nShell"
Hello
Shell
四、子命令擴展
$(...) 可以擴展成另一個命令的運行結果,該命令的所有輸出都會作為返回值。還有另一種較老的語法,子命令放在反引號之中,也可以擴展成命令的運行結果。
$ echo $(date)
2022年 6月27日 星期一 00時31分14秒 CST
$ echo `date`
2022年 6月27日 星期一 00時32分01秒 CST
五、讀取變量
- 在變量名前加上
$,比如$SHELL。 - 讀取變量時,變量名可以使用花括號
{}包圍,比如$SHELL可以寫成${SHELL}。 - 如果變量的值本身也是變量,可以使用
${!varname}語法,讀取最終的值。(好像不太對,待進一步驗證)
六、算術運算
- 除法運算符的返回結果總是為「整數(shù)」,比如
$(( 5 / 2 ))的結果為2,而不是2.5。 -
$(( ... ))的圓括號之中,不需要在變量名之前加上$,不過加上也不報錯。 - 如果
$((...))里面使用不存在的變量,也會當作0處理。 -
$[...]是以前的語法,也可以做整數(shù)運算,不建議使用。
小數(shù)運算,需借助 bc 命令,其中 scale 表示小數(shù)位,ibase 和 obase 進行其他進制數(shù)運算。比如:
$ var1=3
$ var2=6
$ result=$(echo "scale=2; $var1 / $var2" | bc)
$ echo $result
.50
七、目錄堆棧
cd - 命令可以返回前一次的目錄。默認情況下,只記錄上一次所在的目錄。
$ cd ~/Desktop/
$ cd -
~
八、腳本
8.1 Shebang 行
腳本的第一行通常是指定解釋器,即這個腳本必須通過什么解釋器執(zhí)行。這一行以 #! 字符開頭,這個字符稱為 Shebang,所以這一行就叫做 Shebang 行。
#! 后面就是腳本解釋器的位置,Bash 腳本的解釋器一般是 /bin/sh 或 /bin/bash。
#!/bin/sh
# 或者
#!/bin/bash
#! 與腳本解釋器之間有沒有空格,都是可以的。
如果 Bash 解釋器不放在目錄 /bin,腳本就無法執(zhí)行了。為了保險,可以寫成下面這樣。
#!/usr/bin/env bash
上面命令使用 env 命令(這個命令總是在 /usr/bin 目錄),返回 Bash 可執(zhí)行文件的位置。env 命令的詳細介紹,請看后文。
Shebang 行不是必需的,但是建議加上這行。如果缺少該行,就需要手動將腳本傳給解釋器。
舉例來說,腳本是 script.sh,有 Shebang 行的時候,可以直接調用執(zhí)行。
$ ./script.sh
上面例子中,script.sh 是腳本文件名。腳本通常使用 .sh 后綴名,不過這不是必需的。
如果沒有 Shebang 行,就只能手動將腳本傳給解釋器來執(zhí)行。
$ /bin/sh ./script.sh
# 或者
$ bash ./script.sh
8.2 執(zhí)行權限和路徑
前面說過,只要指定了 Shebang 行的腳本,可以直接執(zhí)行。這有一個前提條件,就是腳本需要有執(zhí)行權限??梢允褂孟旅娴拿睿x予腳本執(zhí)行權限。
給所有用戶執(zhí)行權限
$ chmod +x script.sh
給所有用戶讀權限和執(zhí)行權限
$ chmod +rx script.sh
# 或者
$ chmod 755 script.sh
只給腳本擁有者讀權限和執(zhí)行權限
$ chmod u+rx script.sh
腳本的權限通常設為 755(擁有者有所有權限,其他人有讀和執(zhí)行權限)或者 700(只有擁有者可以執(zhí)行)。
除了執(zhí)行權限,腳本調用時,一般需要指定腳本的路徑(比如 path/script.sh)。如果將腳本放在環(huán)境變量 $PATH 指定的目錄中,就不需要指定路徑了。因為 Bash 會自動到這些目錄中,尋找是否存在同名的可執(zhí)行文件。
建議在主目錄新建一個 ~/bin 子目錄,專門存放可執(zhí)行腳本,然后把 ~/bin 加入 $PATH。
export PATH=$PATH:~/bin
上面命令改變環(huán)境變量 $PATH,將 ~/bin 添加到 $PATH 的末尾。可以將這一行加到 ~/.zshrc 文件里面,然后重新加載一次 .zshrc,這個配置就可以生效了。
$ source ~/.zshrc
以后不管在什么目錄,直接輸入腳本文件名,腳本就會執(zhí)行。
$ script.sh
上面命令沒有指定腳本路徑,因為 script.sh 在 $PATH 指定的目錄中。
上面的配置文件,取決于你當前所用的 Shell。比如我這里是 zsh,配置文件為
~/.zshrc,如果你是 bash,可能是~/.bash_profile、~/.bashrc等。
九、條件判斷
if 關鍵字后面跟的是一個命令。這個命令可以是 test 命令,也可以是其他命令。命令的返回值為 0 表示判斷成立,否則表示不成立。
if commands; then
commands
[elif commands; then
commands...]
[else
commands]
fi
判斷條件 commands 可以是一條命令,這條命令執(zhí)行成功(返回值為 0),就意味著判斷條件成立。
但更多地是使用 test 命令,語法如下:
# 寫法一
test expression
# 寫法二
[ expression ]
# 寫法三
[[ expression ]]
以上三種形式是等價的,第三種形式支持正則判斷,前兩種不支持。需要注意的是,后兩種寫法中 [ 和 ] 與內部命令之間必須要有「空格」。因為 [ 是 test 命令的簡寫形式,因此它后面必須要有空格。舉個例子,使用 if 語句判斷一個文件是否存在:
# 寫法一
if test -e /tmp/foo.txt ; then
echo "Found foo.txt"
fi
# 寫法二
if [ -e /tmp/foo.txt ] ; then
echo "Found foo.txt"
fi
# 寫法三
if [[ -e /tmp/foo.txt ]] ; then
echo "Found foo.txt"
fi
9.1 文件判斷
以下表達式用來判斷文件狀態(tài):
-
[ -a file ]:如果file存在,則為true。 -
[ -b file ]:如果file存在,并且是一個塊(設備)文件,則為true。 -
[ -c file ]:如果file存在,并且是一個字符(設備)文件,則為true。 -
[ -d file ]:如果file存在,并且是目錄,則為true。 -
[ -e file ]:如果file存在,則為true。 -
[ -f file ]:如果file存在,并且是一個普通文件,則為true。 -
[ -g file ]:如果file存在,并且設置了組 ID,則為true。 -
[ -G file ]:如果file存在,并且屬于有效的組 ID,則為true。 -
[ -h file ]:如果file存在,并且是符號鏈接(軟鏈接),則為true。 -
[ -k file ]:如果file存在,并且設置了它的 sticky bit,則為true。 -
[ -L file ]:如果file存在,并且是一個符號鏈接(軟鏈接),則為true。 -
[ -N file ]:如果file存在,并且自上次讀取后已被修改,則為true。 -
[ -O file ]:如果file存在,并且屬于有效的用戶 ID,則為true。 -
[ -p file ]:如果file存在,并且是一個命名管道,則為true。 -
[ -r file ]:如果file存在,并且可讀(當前用戶有可讀權限),則為true。 -
[ -s file ]:如果file存在,并且其長度大于零,則為true。 -
[ -S file ]:如果file存在,并且是一個網(wǎng)絡 socket,則為true。 -
[ -t fd ]:如果fd是一個文件描述符,并且重定向到終端,則為true。 這可以用來判斷是否重定向了標準輸入/輸出/錯誤。 -
[ -u file ]:如果file存在,并且設置了 setuid 位,則為true。 -
[ -w file ]:如果file存在,并且可寫(當前用戶擁有可寫權限),則為true。 -
[ -x file ]:如果file存在,并且可執(zhí)行(當前用戶擁有可執(zhí)行/搜索權限),則為true。 -
[ file1 -nt file2 ]:如果file1比file2的更新時間最近,或者file2存在而file1不存在,則為true。 -
[ file1 -ot file2 ]:如果file1比file2的更新時間更舊,或者file2存在而file1不存在,則為true。 -
[ file1 -ef file2 ]:如果file1和file2引用相同的設備和 inode 編號,則為true。
if [ -f "$FILE" ]; then
echo "$FILE is a regular file."
fi
上面代碼中,$FILE 要放在雙引號之中,這樣可以防止變量 $FILE 為空,從而出錯。因為 $FILE 如果為空,這時 [ -e $FILE ] 就變成 [ -e ],這會被判斷為真。而 $FILE 放在雙引號之中,[ -e "$FILE" ] 就變成 [ -e "" ],這會被判斷為假。
未完待續(xù)...