軌道
/
Bash
Bash
/
課程大綱
/
指令與引數
指令

指令與引數 在 Bash

{other: "%{count} 個練習"}

關於 指令與引數

Shell

shell 是一個程式,讓你輸入指令與作業系統互動。Shell 也可以是圖形化的;你用滑鼠或觸控板操作的桌面環境,其實就是一種 shell。這裡要聚焦的是命令列介面:你輸入指令,結果(通常)會印在你的終端機上。

命令列的 shell 有很多種。 Windows 提供 cmd 和 powershell。 Mac 和 Linux 則提供「Unix shell」,種類同樣很多。 Mac 預設的 shell 是zsh。 csh是語法類似 C 的舊式 shell。 bash是最受歡迎的 shell 之一,在 Linux 上尤其如此。

最早的 shell 之一,是 Bourne shell,也就是sh。 後來這個 shell 被標準化,成為每個類 Unix 系統都必須提供的 shell。 許多 shell 都以 Bourne shell 為基礎,通常還會提供額外的互動或程式設計功能。

Bash 就是以 Bourne shell 為基礎。 這也是它被稱為「Bourne-again shell」的原因。 它是許多 Linux 系統的預設 shell。 Bash 提供了許多機制,讓它成為一個好用的互動式 shell,包括:

  • 可完全自訂的提示字元,
  • 能從歷史紀錄中取出並修改先前的指令,
  • 能操控正在執行的行程,
  • 可以少打很多字的別名,
  • 以及更多其他功能。

這個 track 要談的不是如何把 bash 當成互動式 shell 來用,而是要學習用 bash 寫程式。 重點會放在只用 bash 來解決問題,不過並不限制你呼叫外部工具。

Bash 基礎:指令與引數

Bash 會從它的輸入讀取指令(輸入通常是終端機或檔案)。 它讀進來的每一行都會被當成一條_指令_,也就是一個要執行的指示。 (有幾種進階情況,例如橫跨多行的指令,之後會再討論。)

Bash 會把每一行拆成以空白字元(空格與定位字元)分隔的字詞。 這一行的第一個字詞,就是要執行的那個_指令_的名稱。 其餘的字詞都會成為這個指令的_引數_(選項、檔名等)。

Note

下面的範例會示範一些你可以實際執行的互動式指令。

  • #字元代表_註解_的開始。 註解之後的任何字詞都會被 shell 忽略;註解只是寫給人看的。
  • $字元代表 shell 的提示字元。 如果你要複製指令,請不要把提示字元一起複製進去。
# Note: It is always a good idea to run `ls` before `rm`
# to make sure you know what you are deleting.

$ rm *            # Remove all files in the current directory.
                  # Make sure you run this in a temp directory or you can lose files!
$ ls              # List files in the current directory (no output, no files).
$ touch a   b c   # Create files 'a', 'b' and 'c'.
$ ls              # List files again; this time the outputs: 'a', 'b' and 'c'.
a  b  c

rm是一個應用程式,會刪除交給它的檔案。 *是一個 glob 字元,也就是「萬用字元」模式,會比對目前目錄中的所有檔案。 之後我們會再詳細談談 glob。

你注意到了嗎?a和b之間有好幾個空格,b和c之間卻只有一個。 引數之間有多少空白,完全沒有影響! 例如:

$ echo This is a test.
This is a test.
$ echo This    is    a    test.
This is a test.

echo是一個指令,會把它的引數印到標準輸出(在這裡就是終端機)。 Bash 會把這道指令拆成五個字詞,再以四個引數呼叫echo。 第二個例子裡多出來的空格不會造成任何差別。 echo並不知道字詞之間原本有多少空格。

如果想保留多出來的空白,就得把整句話當成單一引數傳進去。 做法是使用引號:

$ echo "This    is    a    test."
This    is    a    test.

引號會把它們裡面的所有內容組成_單一引數_。 這個引數就是This is a test.,包含那些特定的空格字元。 注意,引號本身不屬於引數的一部分,bash 會先移除引號,再把這個單一引數交給echo。

引數與指令名稱之間,以及引數彼此之間,都是用空白分隔的。 這一點很重要,請記住。 例如,下面這樣寫是錯的:

$ [-f file]
bash: [-f: command not found

這段指令原本是想測試有沒有名為file的檔案存在。 你很可能會以為[和]周圍的空白不重要,但[其實是一個指令,而且它要求最後一個引數必須是]。 (我們之後會更詳細地介紹[這個指令。) 因此,我們必須把[和-f分開,把]和file分開。 否則 bash 會以為我們想執行一個名叫[-f的指令,並帶入單一引數file]。 正確的指令會用空白把所有引數分開:

$ [ -f file ]

(我們看過很多人被這個行為搞混;他們以為[...]只是「語法」,所以可以把空白省略掉;但[是一個指令,空白是必要的。)

如果檔名含有空白或其他特殊字元,也應該用引號括起來:

$ [ -f "my file" ]

指令的種類

Bash 認得幾種不同類型的指令:別名、函式、內建指令、關鍵字和執行檔。 它們全都是_指令_,行為方式也非常相似。

別名預設只在互動式 shell 中啟用,在 shell 腳本裡則不會。 這裡不討論別名。

函式

函式裡面包含 shell 指令,運作起來很像一支小腳本;函式甚至可以接受引數、建立區域變數。 呼叫函式時,裡面的指令就會被執行。 函式之後會深入介紹。

內建指令

Bash 內建了一些基本指令,例如cd(切換目錄)、echo(寫入輸出)等等。 可以把它們想成是已經幫你準備好的函式。

關鍵字

關鍵字很像內建指令,主要差別在於關鍵字其實是 bash 的語法,可能會用特殊規則來解析。 例如,[是 bash 的內建指令,而[[是 bash 的關鍵字;兩者都用來測試各種條件。 但[[這個關鍵字還有一些額外的功能。 例如,它提供了=~運算子,可以用來做正規表示式的比對。

$ [[ "hello world" =~ e.*o ]]   # returns a "success" status
$ [ "hello world" =~ e.*o ]
bash: [: =~: binary operator expected

字串比較運算子<和>,則是另一個關鍵字以不同方式解析程式的例子。 在[[...]]裡面,這些運算子不需要特別處理。

$ if [[ "$USER" < "n" ]]; then
    echo "$USER is in the first half of the alphabet"
fi
# => glennj is in the first half of the alphabet

但在[...]裡面,就必須幫它們加上引號或跳脫字元,以免 shell 把它們解析成重新導向運算子。

$ if [ "$USER" < "n" ]; then
    echo "$USER is in the first half of the alphabet"
fi
# => bash: n: No such file or directory

$ if [ "$USER" \< "n" ]; then
    echo "$USER is in the first half of the alphabet"
fi
# => glennj is in the first half of the alphabet

執行檔

Bash 可以執行的最後一種指令是_執行檔_。 它們是你電腦檔案系統上可以執行的檔案(程式)。 (執行檔有時也稱為_外部指令_或_應用程式_。) 執行檔通常只要輸入名稱就能呼叫,一般不需要完整的檔案路徑。 之所以可以這樣,是因為 bash 使用了一個預先定義好的變數,幫它找出常見執行檔的位置。 這個變數叫做PATH。 它是一組以冒號分隔的目錄名稱(例如/usr/bin:/bin)。 當你指定一個指令(例如myprogram或ls)、卻沒有給檔案路徑時(而且它不是別名、函式、內建指令或關鍵字),bash 就會逐一搜尋PATH裡的目錄。 搜尋會從左到右依序進行,看看這些目錄裡有沒有指定名稱的執行檔。

如果執行檔不在已知的路徑中,就必須明確指定它的檔案路徑。 如果myprogram這個執行檔就在目前目錄,請用./myprogram;如果它在/opt/somedirectory目錄裡,就用/opt/somedirectory/myprogram。

提示:type指令可以用來取得指令的詳細資訊:

$ type rm
rm is hashed (/bin/rm)
# or it might return: `rm is /bin/rm`

$ type cd
cd is a shell builtin

腳本

_腳本_是存放在檔案裡的一連串指令。 Bash 會讀取檔案,並依序處理裡面的指令。 目前的指令結束之後,它才會繼續處理下一道指令。 例外情況是指令被指定要以非同步(在背景)的方式執行。 這種情況現在還不用太擔心;之後我們會學到它的運作方式。

這份指南裡的任何命令列範例,也都可以寫進腳本裡使用。

製作腳本很簡單。 先建立一個新檔案,並在第一行寫上:

#!/bin/bash

這一行叫做_翻譯員指令_(也有人稱它為_hashbang_或**shebang**)。 它指定當這個檔案被當成指令裡的執行檔時,要用/bin/bash作為翻譯員。 當作業系統的核心要執行_非二進位檔案_時,會先讀取檔案的第一行。 如果這一行以#!開頭,核心就會根據這一行,決定要把檔案轉交給哪個翻譯員。 (還有其他可行的做法,請見下文。) #!必須位於檔案的最開頭,前面不能有任何空格或空行。 我們腳本裡的指令則會寫在它的下方,一行一道。

提示:除了#!/bin/bash,你也可以改用

#!/usr/bin/env bash

env會拿它第一個引數指定的名稱(在這裡是「bash」),到$PATH裡搜尋對應的執行檔。 關於這個技巧的詳細說明,以及它和單純的#!/bin/bash有何不同,請參考 StackOverflow 上的這個問題。

網路上有些腳本或範例會用/bin/sh當作翻譯員,請不要被它們騙了。 sh不是bash! Bash 本身就是一個「與 sh 相容」的 shell,意思是它可以執行大多數sh腳本,也沿用了許多相同的語法。 不過,反過來並不成立;bash 的某些功能在sh裡會失效,或造成非預期的行為。 事實上,並不存在單一的sh shell。 支援 POSIX-sh 語法的 shell 實作有很多種。 依你的作業系統而定,當你執行sh時,實際上可能是在執行 bash、ash、dash 或其他的 shell。

腳本檔案建立好之後,可以用這種方式執行:

$ bash myscript

在這個例子裡,我們執行bash這個_指令_,並要它讀取我們當成_引數_傳入的腳本。 用這種方式執行時,#!這一行就只是一行_註解_。

另一種做法,是給我們的腳本加上可執行權限。 這樣一來,腳本就能像其他執行檔一樣直接執行,不必手動呼叫 bash:

$ chmod +x myscript  # Mark the file as executable.
$ ./myscript         # Now, myscript can be executed directly
                     # instead of having to specify bash.

用這種方式執行指令時,作業系統(OS)會利用#!這一行,決定要用哪個翻譯員來執行你的腳本。

出處

本單元的內容取自 Bash Guide 的 Commands and Arguments。

透過 GitHub 編輯 連結會在新視窗或分頁中開啟