使用 Bash 簡單入門 Linux Shell 腳本撰寫指南

曾經想要了解更多有關Linux shell腳本編寫,但不確定從何開始嗎?您是否對基於Unix的操作系統相對陌生,想要擴展自己的技能以進行一些基本的shell編程?這份針對初學者的教程將介紹Linux shell腳本編寫基礎,包括創建和運行腳本,以及處理字符串和循環。

Shell腳本用於自動化常見管理任務

無論是哪種操作系統,shell腳本都用於自動執行重複的管理任務。例如,在Windows中,您可以使用文件管理器來重新命名文件。但如果您需要重新命名許多文件,使用圖形界面的shell將是一個耗時任務。PowerShell可以讓您自動執行任務並可靠地重複執行。

廣告

在基於Linux的操作系統中,Bash和其他shell被用於自動執行工作,如處理文件、修改系統配置以及許多其他通常由輸入單個命令可以執行的任務。

您需要學習Bash shell腳本編寫的東西

要編寫和執行Bash腳本,您只需要三樣東西:

  • 任何純文本編輯器,如記事本、文字編輯器、TextEdit、vi、emacs或Visual Studio Code。
  • terminal emulator, an application that comes preinstalled with most operating systems and is often called Terminal, Console, or Command Prompt.
  • Bash 本身。

終端模擬器是您輸入命令並通過按Enter或Return鍵運行它們的地方。至於Bash,您是否已預先安裝取決於您的平台:

  • macOS中,Bash是預先安裝的。在更新版本中,Z shell(zsh)是默認Shell,這沒問題。只要安裝了Bash,您也可以在zsh中運行Bash腳本。
  • Linux發行版一般都提供了Bash。 (您可以通過查看系統是否包含/bin/bash文件來檢查。)Android是不提供Bash的特殊情況。有方法將Bash安裝到Android上,但本文不涉及這一點。
  • Windows並不內建Bash。PowerShell是Windows的預設命令列殼​​。您需要安裝Linux發行版在Windows子系統用於Linux(WSL)下執行Bash。

欲查找您的Bash版本,執行bash –version命令。即使是較舊的Bash版本也提供大量功能,Bash 3和4都引入了對特定基本命令的簡潔表示法。如果命令需要這些Bash版本之一,將在下面提到。

廣告

什麼是殼層?

在計算世界中,殼層是一個用作底層操作系統​​介面的程式。殼層可以是圖形用戶界面(GUI),就像Windows殼​​一樣。

殼層腳本語言

然而,人們通常使用這個術語來特指命令行界面(CLI)—— 一種由文本行組成的界面,只能使用鍵盤與之互動。以下是一些*nix操作系統的shell腳本語言示例:

  • Bash shell(簡稱“Bourne Again Shell”)
  • C shell
  • Korn shell

在這裡,我們將專注於Bash shell。這是一個流行的免費Unix shell,在大多數Linux發行版和macOS上預先安裝。

什麼是shell腳本?

Shell擁有自己的編程語言。您可以使用這種語言向shell發送命令,然後shell執行它們。您可以直接將這些命令輸入shell,也可以將它們保存到一個文件 — 一個腳本 — 然後從shell執行該文件。兩種情況下寫命令的語法是一樣的。

廣告

本文將介紹shell腳本的基礎知識以創建這個文件。

基本shell腳本

讓我們從一些基本的shell腳本開始。要撰寫一個簡單的腳本,我們將學習在Linux中使用一些簡單的shell腳本命令:

  1. 在文本编辑器中創建一個新的空白文本檔
  2. #!/bin/bash寫在第一行。
  3. 在其下方輸入您的命令
  4. 保存文件,最好使用“.sh”擴展名或根本不使用擴展名。

這行#!/bin/bash被稱為“shebang。”它告訴您的shell這個腳本應該在Bash中執行,並且應該是您腳本的第一行。如果切換到其他shell,仍將在Bash中運行您的腳本。

為了自己嘗試這個過程,您可以在家目錄中創建一個名為“hello_world”的文件:

#!/bin/bash
echo "hello world"

就是這樣——您已經創建了一個Bash腳本!

Our “hello world” script is just a simple text file

在運行之前,您可能需要更改文件的權限。

使用chmod設置運行shell腳本的權限

要修改我們的“hello_world”文件的權限,您應該在終端仿真器中運行此特定命令。這將授予擁有文件權限的用戶執行文件的權限。

chmod u+x 'hello_world'
Running our script without vs. with the “execute” permission

如果您只想运行您的shell脚本,您可以跳到下一节。对于那些对chmod命令感到好奇的人,chmod是“change mode”的缩写,用于更改Unix中的文件“模式”(或权限)。在类Unix操作系统中,您可以为3类用户设置文件权限:

  • 拥有文件的用户(在chmod中由u表示)。
  • 拥有文件的组(g)。
  • 其他人(o)。

使用chmod命令,您还可以使用a来引用所有这些。

每个文件有3种类型的权限(或“模式”):

  • 读取(r)
  • 写入(w)
  • 执行(x)

而且您还可以添加(+)或删除(-)权限。

chmod中的第一个参数是这三个的组合——首先是用户,其次是操作,再次是模式。以下是一些命令示例:

  • chmod gu+rw 'hello_world'将为所有者和拥有组添加读取和写入权限。
  • chmod a-x 'hello_world'将删除所有人的可执行权限。
  • chmod u+rwx 'hello_world' 'hello_world_2' 將賦予擁有者對”hello_world”和”hello_world_2″文件的讀取、寫入和執行權限。

我們僅介紹了chmod命令的基礎知識。還有一種更複雜但不那麼冗長的定義這些模式的方法(”數值表示法”),以及一種不同的命令可用於查看文件具有的權限(ls -l)。我們不會在這裡深入討論這些話題。

執行shell腳本

現在該執行我們的第一個腳本了。一般來說,要運行一個腳本,只需將其路徑輸入終端仿真器並按enter鍵。

./hello_world

您可以使用相對路徑或絕對路徑。當使用相對路徑時,始終在命令的開頭使用./:這告訴終端仿真器查找當前文件夾(以'.'表示),而不是查找PATH環境變量中定義的目錄。

Just typing the script name doesn’t work, but running its relative or absolute paths does

使用註釋來注釋您的腳本

在Bash腳本中,一旦在單行上的#之後的所有內容都被認為是一個註釋。這些註釋可用於說明複雜行的作用或列出腳本中較大部分的功能。

例如:

#!/bin/bash

#
# This shell script prints "hello world".
#

echo "hello world" # This line prints "hello world".

介紹變量

在撰寫腳本時,定義變數是很有用的。在Bash中,您可以通過輸入變數名稱和值,用等號分隔來完成:VARIABLENAME='VALUE'

不應該在等號旁邊加空格 — 否則Bash會認為您想運行一個進程。

使用單引號來包住值,以避免Bash將其解釋為其他內容。在Bash中,變數沒有類型 — 一切基本上都是字符串。Bash程序可以將字符串解析為不同類型,比如數字。

要引用變數的值,使用變數名稱前加上美元符號:$VARIABLENAME

要實際嘗試這個,可以將您的腳本更改為這樣:

#!/bin/bash
HELLO="hello variable world"
echo $HELLO # should print "hello variable world"

接收引數

在輸入命令時,您寫的個別單詞被稱為引數。在我們的chmod u+x 'hello_world'範例中,chmodu+x'hello_world'是三個不同的引數。chmod是命令名,而u+xhello_world稱為參數 — 提供給命令的額外信息。

在您的腳本中,您可以通過變數訪問這些引數。為了避免與局部變數衝突,這些變數以數字命名 — $0指的是命令名,$1是其後的下一個引數,$2是其後的下一個引數,以此類推。

讓我們試試這個:

#!/bin/bash
HELLO="hello $1 world"
echo $HELLO

現在,使用這些參數運行這個腳本:

./hello_world bash script

輸出應該是hello bash world,使用第一個參數並忽略第二個。

如果您希望bash script被視為一個參數,您需要將其放在引號中:

./hello_world 'bash script'
Words separated by a space are considered as several arguments, except when in quotes

使用if語句在條件情況下運行程式碼

程序員在腳本中想要做的核心之一是只有在滿足某個條件時運行一段程式碼。Bash 使用if語句來實現這一點:

NUM=$RANDOM
if (( $NUM % 2 )) # if CONDITION
then
    echo "$NUM is odd"
fi # this is how you end an if statement

提示: 從現在開始,這些示例被假定為更大腳本的一部分,並省略了開頭的#!/bin/bash。不過不要忘記將它作為您腳本的第一行!

您還可以在if語句中使用else來指定如果條件不滿足時該做什麼,或使用elif(簡寫為“else if“)語句指定另一個條件如果第一個條件未滿足:

NUM=$RANDOM
if [ $NUM -eq 12 ]
then
    echo "$NUM is my favorite number"
elif (( $NUM % 2 ))
then
    echo "$NUM is odd"
else
    echo "$NUM is even"fi

fi’用於關閉if語句。

小貼士:如果你不确定如何 write the condition itself,請查看test、單一括號([])和雙重括號((()))的表示法。

The output of our script depends on the value of a random variable

使用 for 迴圈重複一系列命令

既然我們已經 covered running code conditionally,讓我們看一下如何只要滿足某個條件就運行代碼一定次數。

for迴圈正是這種任務的完美選擇 — 特別是它的“三 expressions 語法”。其背后的想法是要指派一個特定的 loop-specific variable 並逐漸改變它,直到達到某個條件。以下是它的結構:

for (( ASSIGNMENT_EXPRESSION ; CONDITION_EXPRESSION ; UPDATE_EXPRESSION ))
do
    COMMANDS
done

例如,如果你想要一個 loop 運行 10 次,並為 i 設定值從 0 到 9,你的for迴圈可能看起来像这样:

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

讓我們來分解一下:

  • i=0 is the assignment expression here. It’s run only once before the loop is executed, which is why it’s useful for initializing a variable.
  • i<10 is our condition expression. This expression is evaluated before each iteration of a loop. If it is equal to zero (which means the same as “true” in Bash), the next iteration is not run.
  • i++ is our update expression. It’s run after each iteration of a loop.
The structure of our for loop

列表中元素的遍歷

除了三 expressions 語法,你也可以使用in关键词來定義for迴圈。這種 alternative syntax 用於迭代一系列的项目。

最基本的例子就是僅僅在in关键词後列出你想迭代的物品集合,用空格分隔。例如:

for i in 0 1 2 3 4 5 6 7 8 9 # space-separated list items
do
    echo $i
done

您也可以遍歷命令輸出的項目:

for i in $(seq 0 1 9)

通常情況下,$() 符號用於命令替換 – 執行一個命令,並將其輸出用作其周圍父級命令的輸入。

如果要遍歷整數,最好使用Bash的內建範圍語法,它比seq 命令更有效。不過,這種語法僅在較新的Bash版本中可用:

  • for i in {0..9},在Bash 3中可用。
  • for i in {0..9..1},在Bash 4中可用,其中最後一個數字表示增量。

同樣地,您也可以遍歷字符串:

for s in 'item1' 'item2' 'item3'

使用通配符匹配模式來獲取符合條件的文件

在前一節討論的for循環中遍歷單獨文件是更常見的用例之一。

為了應對這一點,我們需要先介紹所謂的 “通配符展開”。這是Bash中的一個功能,讓您可以使用模式匹配指定文件名。您可以使用稱為 通配符 的特殊字符來定義這些模式。

在深入研究之前,讓我們看幾個具體的例子:

  • echo *:返回當前目錄中所有文件的名稱,除了隱藏的文件。
  • echo *.txt:返回當前目錄中所有具有 txt 擴展名的非隱藏文件的名稱。
  • echo ????:返回當前目錄中所有四個字母的文件名。

我們在這裡使用的是 * 和 ? 這兩個萬用字符。還有一個我們沒有使用的萬用字符。以下是一個概述:

  • 星號(*)代表文件或目錄名中的任意數量的字符(包括0)。
  • 問號(?)代表文件或目錄名中的單個字符。
  • 雙星號(**)代表完整文件路徑中的任意數量的字符。這是 Bash 4 及以上版本的功能,必須通過運行 shopt -s globstar 來啟用。
  • 方括號([])用於表示文件或目錄名中一組符號中的一個字符。例如,[st]ake 可以找到名為 saketake 的文件,但不能找到 stake

請注意,在使用全局擴展時,所有隱藏文件(即名稱以句號 . 開頭的文件)都將被忽略。

方括號表示法允許更多的複雜性,包括:

  • 字首和末尾值所定義的範圍—例如[1-8]
  • 使用!作為括號內第一個字符來消除特定字符—例如[!3]

如果要將其中一個特殊字符視為沒有任何意義的正常字符,只需在其前面加上反斜線—例如\?

A few examples of globbing

使用for循環遍歷文件

現在我們已經介紹了glob擴展的基礎知識,讓我們看看如何使用它來遍歷文件。

我們可以直接在for循環中使用glob運算符。這裡是一個簡單的例子,循環打印當前目錄中每個文件的名稱:

for f in *
do
    echo $f
done

要打印出當前目錄中每個文件以及其子目錄的名稱,請確保您正在運行Bash 4.0或更高版本,方法是運行bash --version,然後您可以運行以下命令:

shopt -s globstar # enables using **
for f in **
do
    echo $f
done

提示: 如果您正在運行舊版本的Bash,則無法在循環中使用globbing。您最好的做法是使用find命令,但在本文中我們不會詳細介紹這點。

這些當然只是您可以運行的一些最簡單的循環,但您還可以做很多其他事情。例如,要將文件夾中的所有JPG文件重新命名為給它們提供一致的連續文件名,您可以運行:

i=1
for f in *.jpg
do
    mv -i -- "$f" "image_$i.jpg"
    let i=i+1
done
Checking the Bash version, then running our script

在條件為真時運行代碼

for循環不是我們在Bash中可以使用的唯一類型的循環 — 我們還有while:這種類型的循環在特定條件為真時運行。

語法與for循環語法類似:

while CONDITION
do
    COMMANDS
done

舉個實際例子,這是如何逐行讀取文件(除了前導或尾隨空格)直到文件結尾的:

while read -r line
do
    echo "$line"
done < FILENAME # Replace FILENAME with the path to a text file you'd like to read
Using a while loop inside our script to have it print itself

您還可以用while循環替換for循環,例如從0到9進行迭代:

i=0
while [ $i -lt 10 ]
do
    echo $i
    let i=i+1
done

您也可以在理論上運行一個無限循環。這裡有一個命令示例:

while true
do
    echo "running forever"
done

提示: 要結束腳本,只需按Ctrl+C

雖然這個無限循環乍一看似乎毫無用處,但實際上可以非常有用,特別是與break語句結合使用時。

退出循環

break語句用於退出循環。

這使您可以運行一個無限循環,並在出現任何終止條件時退出。

舉個簡單的例子,我們可以用這樣的無限循環來復制從0到9運行的循環:

i=0while true
do
    if [ $i -eq 10 ]
    then
        break
    fi
    echo $i
    let i=i+1
done

如果您有幾個巢狀while迴圈,則可以在break語句後添加一個數字,以指明要從哪個層級的迴圈中跳出:break 1break相同,將跳出最接近的周圍迴圈,break 2將跳出上面一級的迴圈,依此類推。

讓我們來看一個快速的例子,這次使用for迴圈,遍歷每個四個字母的單詞組合,直到遇到單詞“bash”為止:

for l4 in {a..z}
do
    for l3 in {a..z}
    do
        for l2 in {a..z}
        do
            for l1 in {a..z}
            do
                echo "$l4$l3$l2$l1"
                if [ $l4 = "b" -a $l3 = "a" -a $l2 = "s" -a $l1 = "h" ]
                then
                    break 4
                fi
            done
        done
    done
done

A related keyword that’s also worth a mention is continue, which skips to the next iteration of the loop. Just like break, it also takes an optional numeric argument that corresponds to the loop level.

這是一個愚蠢的例子,我們在簡短的四個字母單詞列表中跳過所有帶有E的單詞:

for l4 in {a..z}
do
    if [ $l4 = "e" ]
    then
        continue
    fi
    for l3 in {a..z}
    do
        if [ $l3 = "e" ]
        then
            continue
        fi
        for l2 in {a..z}
        do
            if [ $l2 = "e" ]
            then
                continue
            fi
            for l1 in {a..z}
            do
                if [ $l1 = "e" ]
                then
                    continue
                fi
                echo "$l4$l3$l2$l1"
                if [ $l4 = "b" -a $l3 = "a" -a $l2 = "s" -a $l1 = "h" ]
                then
                    break 4
                fi
            done
        done
    done
done

我們還可以在最深層級的迴圈中執行所有這些continue語句:

for l4 in {a..z}
do
    for l3 in {a..z}
    do
        for l2 in {a..z}
        do
            for l1 in {a..z}
            do
                if [ $l4 = "e" ]
                then
                    continue 4
                fi
                if [ $l3 = "e" ]
                then
                    continue 3
                fi
                if [ $l2 = "e" ]
                then
                    continue 2
                fi
                if [ $l1 = "e" ]
                then
                    continue
                fi
                echo "$l4$l3$l2$l1"
                if [ $l4 = "b" -a $l3 = "a" -a $l2 = "s" -a $l1 = "h" ]
                then
                    break 4
                fi
            done
        done
    done
done
Our script stops once it generates the word “bash”

如何在shell腳本中獲取用戶輸入

有時,您希望用戶直接與您的腳本進行交互,而不僅僅使用初始腳本參數。這就是read命令的用途。

要獲取用戶輸入並將其保存到名為NAME的變量中,您可以使用此命令:

read NAME 

這是該命令的最簡單形式,僅包括命令名稱和要保存輸入的變量。

但更常見的情況是,您需要提示用戶知道應該輸入什麼。您可以使用-p參數來進行提示,然後寫下您的首選提示。

這是您在問名字時可能如何寫,並將其指定給一個名為NAME的變數:

read -p "Your name: " NAME
echo "Your name is $NAME." # this line is here just to show that the name has been saved to the NAME variable
The script saves our input to a variable and then prints it

在字串中列印特殊字元

在Bash中有一些字元您必須小心使用。例如,在檔名中的空格、字串中的引號,或是幾乎任何地方的反斜線。

要告知Bash在某些地方忽略它們的特殊意義,您可以將它們「轉義」或是用文字引號包起來。

轉義」一個特殊字元是告訴Bash將其視為沒有特殊意義的字元。要做到這一點,只需在該字元前寫上一個反斜線。

假設我們有一個名為img \ 01 *DRAFT*的檔案。在Bash中,我們可以這樣參考它:

img\ \\\ 01\ \*DRAFT\*

這裡是Bash中一些特殊字元的非詳盡清單:

  • 空白:空格、定位字元、空行
  • 引號:”,“”
  • 括號和方括號:()、{}、[]
  • 管道符號和重新導向:|、<、>
  • 萬用字元:*、?
  • 其他:!、#、;、=、&、~、‘
  • 用於轉義的字元本身:\

然而,如果您是Bash新手,記住哪些字元具有特殊意義可能有點麻煩。因此,在許多情況下,使用文字引號可能更容易:只需用單引號括住包含任何特殊字元的文字,那些引號內的所有特殊字元將被忽略。

這是我們示例的呈現方式:

'img \ 01 *DRAFT*'

如果你想要使用特殊字符,但希望轉義其他字符呢?你可以使用反斜線來逐個轉義每個字符,但你也可以省事地將除了該特殊字符以外的所有內容都用單引號包裹。

例如,假設你有幾個文件名為img \ 01 *v1 DRAFT*img \ 01 *v2 DRAFT*img \ 01 *ROUGH DRAFT*等等,並且你想要使用通配符來匹配所有這些文件名。你可以這樣寫:

'img \ 01 *'*' DRAFT*'
This glob expansion finds all of our 3 oddly-named files

如果你需要寫入的內容包含單引號 — 例如img \ 01 *’FINAL’* — 你可以使用一種類似的策略,結合字面字符串和轉義:

'img \ 01 '\''FINAL'\'

如何在 Bash 中連接字符串

假設你有兩個或更多字符串變量 — 比如名字和姓氏:

FIRST_NAME="Johnny"LAST_NAME="Appleseed"

要將這些變量合併成一個字符串,也許還帶有自定義定界符,只需創建一個新字符串,包含這兩個變量:

NAME="$LAST_NAME"', '"$FIRST_NAME"

你也可以使用雙引號與內嵌變量來實現這一點,使用花括號將變量名與周圍文本分隔:

NAME="${LAST_NAME}, ${FIRST_NAME}"

Bash 也允許使用+=運算符將文本附加到一個字符串,就像這樣:

NAME='Appleseed'NAME+=', 'NAME+='Johnny'
Examples of different ways of stringing together text

在字符串內運行命令

Bash 也允許您在字串中使用命令的輸出,這也被稱為 命令替換。只需將您的命令用 $() 包圍起來即可。例如,要打印一個當前的時間戳,您可以運行:

echo "Current timestamp: $(date)"
Using command substitution to print the current timestamp

您可能還記得這個語法,從前一個 for 循環示例中,當我們遍歷從 0 到 9 的整數序列時:

for i in $(seq 0 1 9)
do
    echo $i
done

替換的命令在子shell中運行。這意味著,例如,在命令過程中創建的任何變量不會影響您正在運行脚本的環境。

在 shell 腳本中設置和返回退出代碼

對於一個更複雜的腳本,通常要有它返回一個 退出 代碼 — 一个介於 0 和 255 之間的數字,告訴人們脚本是否成功地運行了還是遇到錯誤。

至於使用哪個數字,Bash 官方手冊 specify 這些:

  • 0: 程序成功地執行了。
  • 2: 程序用法不正確(例如,由於無效或缺少參數)。
  • 1 和 3-124: 用戶定義的錯誤。
  • 126: 命令不可執行。
  • 127: 找不到命令。
  • 125 和 128-255: shell 使用的錯誤狀態。如果一個进程被信號 N 杀死,退出狀態是 128 + N。

如您所见,在除了0以外的所有数字都表示某种错误。退出代码1通常用于一般错误。在大多数情况下,您不需要使用任何大于2的退出代码。

要从您的脚本中返回一个退出代码,只需使用exit命令。例如,要以代码2退出,您应该编写exit 2

如果您的脚本中没有使用exit命令或使用命令时没有指定代码,则脚本中最后执行的命令的退出状态将被返回。

要获取您的shell中最后运行命令的退出代码,请使用$?变量:echo $?

如何调用函数

在编写较长或具有重复代码块的脚本时,您可能希望将一些代码分离成函数。有两种格式可以用来定义函数:在这两种情况下,所有函数代码都包含在大括号内,只有函数声明不同。

更紧凑的格式使用跟在函数名称后面的括号声明函数:

function_name () {
    echo "This is where your function code goes"
}

另一种格式使用function关键字在函数名称前面。

function function_name {
    echo "This is where your function code goes"
}

函式必須在腳本中被調用之前宣告。你調用一個函式就像執行一個常規命令一樣,使用函式名稱作為命令:

function_name

使用變量將數據傳遞給函式

在Bash中,函式不能接受參數。要將信息傳遞給函式,你需要在腳本中使用全局變量。

例如:

is_your_name_defined () {
    if [ -z "$YOUR_NAME" ]
    then
        echo "It doesn't seem like I have your name."
    else
        echo "Is this your name: ${YOUR_NAME}?"
    fi
}
read -p "Your name: " YOUR_NAME

is_your_name_defined

不像其他語言中你所熟悉的,你在函式內定義的變量是全局的,並且可以被腳本外部的代碼所見。如果需要定義一個僅在函式內部區域可訪問的變量(也就是對於所有代碼外部都不可訪問),使用local關鍵字:例如local i=0

如果想要從函式中返回值,也需要使用全局變量。在Bash中,函式只能返回退出碼,使用return關鍵字。讓我們看一個例子:

is_your_name_defined () {
    if [ -z "$YOUR_NAME" ]
    then
        MESSAGE="It doesn't seem like I have your name."
    else
        MESSAGE="Is this your name: ${YOUR_NAME}?"
    fi
}
read -p "Your name: " YOUR_NAME

is_your_name_defined

echo $MESSAGE
The output of our script depends on the values entered

摘要

Bash腳本允許你在基於UNIX的操作系統上執行許多操作。本文涵蓋了一些Bash腳本的基礎知識,包括創建和運行腳本、處理字符串以及在代碼中使用循環。希望這將作為你撰寫功能強大的Bash腳本的良好起點。

當然,關於Bash還有很多知識要學習,包括一些最有用的命令、文件系統導航等。請在評論中告訴我們應該下一步涵蓋哪些主題。

相關文章:

Source:
https://petri.com/shell-scripting-bash/