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 會把每一行拆成以空白字元(空格與定位字元)分隔的字詞。 這一行的第一個字詞,就是要執行的那個_指令_的名稱。 其餘的字詞都會成為這個指令的_引數_(選項、檔名等)。
下面的範例會示範一些你可以實際執行的互動式指令。
#字元代表_註解_的開始。
註解之後的任何字詞都會被 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。