Bash

Bash，Unix shell的一種，在1987年由布萊恩·福克斯為了GNU計劃而編寫。1989年釋出第一個正式版本，原先是計劃用在GNU作業系統上，但能運行於大多數類Unix系統的作業系統之上，包括Linux與Mac OS X v10.4起至macOS Mojave都將它作為預設shell，而自macOS Catalina，預設Shell以zsh取代。

Bash是Bourne shell的後繼相容版本與開放原始碼版本，它的名稱來自Bourne shell（sh）的一個雙關語（Bourne again / born again）：Bourne-Again SHell。

Bash是一個命令處理器，通常運行於文本窗口中，並能執行使用者直接輸入的命令。Bash還能從文件中讀取命令，這樣的文件稱為腳本。和其他Unix shell 一樣，它支持文件名替換（通配符匹配）、管道、here文檔、命令替換、變量，以及條件判斷和循環遍歷的結構控制語句。包括關鍵字、語法在內的基本特性全部是從sh借鑑過來的。其他特性，例如歷史命令，是從csh和ksh借鑑而來。總的來說，Bash雖然是一個滿足POSIX規範的shell，但有很多擴展。

一個名為Shellshock的安全漏洞在2014年9月初被發現，並迅速導致網際網路上的一系列攻擊。這個漏洞可追溯到1989年發布的1.03版本。

## 歷史

由於理察·斯托曼對於之前一位開發者的進度不滿，布萊恩·福克斯從1988年1月10日開始開發Bash。斯托曼和自由軟體基金會希望到一個能夠運行已有的shell腳本的自由軟體。他們把這看作是建成一個基於BSD和GNU的完全自由的作業系統的戰略的重要部分。這是他們自己注資的幾個項目之一。福克斯作為自由軟體基金會的雇員承擔這項工作。1989年6月8日，福克斯發布Bash的beta版本，版本號為.99。在福克斯離開於1992年中期到1994年中期的某個時候離開自由軟體基金會之前，他一直擔任Bash的主要維護者。之後，他的工作被傳遞給另一個早期貢獻者，切特·雷米（Chet Ramey）。

從那時起，在Linux使用者當中sh在很大度上成為最流行的shell，並成為許多Linux發行版默認的交互式shell（不過Almquist shell可能是默認的腳本shell）。在蘋果公司的 OS X 作業系統上也是如此。Bash 也被移植到 Microsoft Windows（通過Cygwin和MinGW）。通過DJGPP項目，Bash被移植到了DOS。通過許多終端模擬軟體，Bash被移植到Novell NetWare和Android。微軟在2016年的Build大會上宣布，Windows 10 添加一個Linux子系統，完全支持Bash和其他Ubuntu下的二進位程序。

2014年9月24日，Stephane Chazelas，一位工作於英國，致力於Unix/Linux和網路通信方面的專家，發現Bash的一個安全漏洞。這個漏洞被命名為Shellshock，並被分配編號 CVE-2014-6271、CVE-2014-6277、CVE-2014-7169。這個漏洞非常嚴重，因為使用Bash的CGI腳本會變得脆弱，使得攻擊者可以執行任意的代碼。這個漏洞與Bash通過環境變量把函數定義傳遞給shell子進程的方式有關。

## 語法與特性

bash的命令語法是Bourne shell命令語法的超集。數量龐大的Bourne shell腳本大多不經修改即可以在bash中執行，只有那些引用了Bourne特殊變量或使用了Bourne的內置命令的腳本才需要修改。bash的命令語法很多來自Korn shell（ksh）和C Shell（csh），例如命令行編輯，命令歷史，目錄棧，$RANDOM和$PPID變量，以及POSIX的命令置換語法：$(...)。作為一個交互式的shell，按下TAB鍵即可自動補全已部分輸入的程序名，文件名，變量名等等。

使用'function'關鍵字時，Bash的函數聲明與Bourne/Korn/POSIX腳本不兼容（Korn shell 有同樣的問題）。不過Bash也接受Bourne/Korn/POSIX的函數聲明語法。因為許多不同，Bash腳本很少能在Bourne或Korn解釋器中運行，除非編寫腳本時刻意保持兼容性。然而，隨著Linux的普及，這種方式正變得越來越少。不過在POSIX模式下，Bash更加符合POSIX。

bash的語法針對Bourne shell的不足做了很多擴展。其中的一些列舉在這裡。

### 花括號擴展

花括號擴展是一個從C shell借鑑而來的特性，它產生一系列指定的字串（按照原先從左到右的順序）。這些字串不需要是已經存在的文件。

```bash
$ echo a{p,c,d,b}e
ape ace ade abe
$ echo {a,b,c}{d,e,f}
ad ae af bd be bf cd ce cf
```

花括號擴展不應該被用在可移植的shell腳本中，因為Bourne shell產生的結果不同。

```shell
#! /bin/sh

# 传统的shell并不产生相同结果
echo a{p,c,d,b}e # a{p,c,d,b}e
```

當花括號擴展和通配符一起使用時，花括號擴展首先被解析，然後正常解析通配符。因此，可以用這種方法獲得當前目錄的一系列JPEG和PNG文件。

```bash
ls *.{jpg,jpeg,png}    # 首先扩展为*.jpg *.jpeg *.png，然后解析通配符
echo *.{png,jp{e,}g}   # echo显示扩展结果；花括号扩展可以嵌套。
```

除了列舉備選項，還可以用「..」在花括號擴展中指定字符或數字範圍。較新的Bash版本接受一個整數作為第三個參數，指定增量。

```bash
$ echo {1..10}
1 2 3 4 5 6 7 8 9 10
$ echo file{1..4}.txt
file1.txt file2.txt file3.txt file4.txt
$ echo {a..e}
a b c d e
$ echo {1..10..3}
1 4 7 10
$ echo {a..j..3}
a d g j
```

當花括號擴展和變量擴展一起使用時，變量擴展解析於花括號擴展之後。有時有必要使用內置的`eval`函數

```bash
$ start=1; end=10
$ echo {$start..$end}  # 由于解析顺序，无法得到想要的结果
{1..10}
$ eval echo {$start..$end} # 首先进行变量扩展的解析
1 2 3 4 5 6 7 8 9 10
```

### 使用整數

與Bourne shell不同的是bash不用另外生成進程即能進行整數運算。bash使用((...))命令和$...變量語法來達到這個目的：

```bash
 VAR=55             # 将整数55赋值给变量VAR
 ((VAR = VAR + 1))  # 变量VAR加1。注意这里没有'$'
 ((++VAR))          # 另一种方法给VAR加1。使用C语言风格的前缀自增
 ((VAR++))          # 另一种方法给VAR加1。使用C语言风格的后缀自增
 echo $((VAR * 22)) # VAR乘以22并将结果送入命令
 echo $[VAR * 22]   # 同上，但为过时用法
```

((...))命令可以用於條件語句，因為它的退出狀態是0或者非0（大多數情況下是1），可以用於是與非的條件判斷：

```bash
 if((VAR == Y * 3 + X * 2))
 then
         echo Yes
 fi

 ((Z > 23)) && echo Yes
```

((...))命令支持下列比較操作符：'==', '!=', '`>`', '`<`', '`>=`'，和'`<=`'。

bash不能在自身進程內進行浮點數運算。當前有這個能力的unix shell只有KornShell和Z shell。

### 輸入輸出重定向

bash擁有傳統Bourne shell缺乏的I/O重定向語法。bash可以同時重定向標準輸出和標準錯誤，這需要使用下面的語法：

```bash
 command &> file
```

這比等價的Bourne shell語法"command > file 2>&1"來的簡單。2.05b版本以後，bash可以用下列語法重定向標準輸入至字串（稱為here string）：

```bash
 command <<< "string to be read as standard input"
```

如果字串包括空格就需要用引號包裹字串。

例子:

重定向標準輸出至文件，寫數據，關閉文件，重置標準輸出。

```bash
 # 生成标准输出（文件描述符1）的拷贝文件描述符6
 exec 6>&1
 # 打开文件"test.data"以供写入
 exec 1>test.data
 # 产生一些内容
 echo "data:data:data"
 # 关闭文件"test.data"
 exec 1>&-
 # 使标准输出指向FD 6（重置标准输出）
 exec 1>&6
 # 关闭FD6
 exec 6>&-
```

打開及關閉文件

```bash
 # 打开文件test.data以供读取
 exec 6<test.data
 # 读文件直到文件尾
 while read -u 6 dta
 do
   echo "$dta"
 done
 # 关闭文件test.data
 exec 6<&-
```

抓取外部命令的輸出

```bash
  # 运行'find'并且将结果存于VAR
  # 搜索以"h"结尾的文件名
  VAR=$(find . -name "*h")
```

### 進程內的正則表達式

bash 3.0支持進程內的正則表達式，使用下面的語法：

```bash
 [[ string =~ regex ]]
```

正則表達式語法同regex(7) man page所描述的一致。正則表達式匹配字串時上述命令的退出狀態為0，不匹配為1。正則表達式中用圓括號括起的子表達式可以訪問shell變量BASH_REMATCH，如下：

```bash
 if [[ abcfoobarbletch =~ 'foo(bar)bl(.*)' ]]
 then
         echo The regex matches!
         echo $BASH_REMATCH      -- outputs: foobarbletch
         echo ${BASH_REMATCH[1]} -- outputs: bar
         echo ${BASH_REMATCH[2]} -- outputs: etch
 fi
```

使用這個語法的性能要比生成一個新的進程來運行grep命令優越，因為正則表達式匹配在bash進程內完成。如果正則表達式或者字串包括空格或者shell 關鍵字，（諸如'*'或者'?'），就需要用引號包裹。Bash 4 開始的版本已經不需要這麼做了。

### 轉義字符

$'string' 形式的字串會被特殊處理。字串會被展開成string，並像C語言那樣將反斜槓及緊跟的字符進行替換。反斜槓轉義序列的轉換方式如下：

轉義字符｜擴展成...

\a｜響鈴符

\b｜退格符

\e｜ANSI轉義符，等價於\033

\f｜饋頁符

\n｜換行符

\r｜回車符

\t｜水平制表符

\v｜垂直制表符

\\｜反斜槓

\'｜單引號

\nnn｜十進位值為nnn的8-bit字符（1－3位）

\xHH｜十六進位值為HH的8-bit字符（1或2位）

\cx｜control-X字符

擴展後的結果將被單引號包裹，就好像美元符號一直就不存在一樣。

雙引號包裹的字串前若有一個美元符號（$"..."）將會使得字串被翻譯成符合當前locale的語言。如果當前locale是C或者POSIX，美元符號會被忽略。如果字串被翻譯並替換，替換後的字串仍被雙引號包裹。

### 關聯數組

Bash 4.0 開始支持關聯數組，通過類似AWK的方式，對於多維陣列提供了偽支持。

```bash
$ declare -A a         # 声明一个名为a的伪二位数组
$ i=1; j=2
$ a[$i,$j]=5           # 将键 "$i,$j" 与值 5 对应
$ echo ${a[$i,$j]}
```

### 移植性

調用Bash時指定 `--posix` 或者在腳本中聲明 `set -o posix`，可以使得Bash幾乎遵循 POSIX 1003.2 標準。若要保證一個Bash腳本的移植性，至少需要考慮到 Bourne shell，即Bash取代的shell。Bash有一些傳統的 Bourne shell 所沒有的特性，包括以下這些：

- 某些擴展的調用選項
- 命令替換（即`$()`）（儘管這是 POSIX 1003.2 標準的一部分）
- 花括號擴展
- 某些陣列操作、關聯數組
- 擴展的雙層方括號判斷語句
- 某些字串生成操作
- 進程替換
- 正則表達式匹配符
- Bash特有的內置工具
- 協進程

### 鍵盤快捷鍵

Bash默認使用Emacs的快捷鍵，可以通過 `set -o vi` 讓它使用Vi的快捷鍵

### 進程管理

Bash有兩種執行命令的模式：批處理模式、併發模式。

#### 批處理模式

要以 批處理模式 執行命令（即按照順序），必須用`;`分隔，例子如下：

```bash
command1 ; command2
```

在這個例子中，當`command1`執行完畢，即執行`command2`。

#### 併發模式

使`command1`後台運行，則單獨在結尾處使用`&`，例子如下：

```bash
command1 &
```

要並發執行兩個命令，它們必須用`&`分隔，例子如下：

```bash
command1 & command2
```

在這種情況下，`command1` 在後台執行（通過`&`），從而立即將控制返回到shell，以執行`command2`。

通過 Control 可以將當前進程掛起（放置後台並暫停運行），可通過 `fg` 命令恢復至前台，也通過`bg`將掛起的進程後台運行。

#### 查看進程狀態

所有進程的情況，我們可以通過`jobs`命令查看，包含正在後台運行和停止了的。

```bash
$ jobs
[1]-  Running                  command1 &
[2]+  Stopped                  command2
```

上例的輸出結果中，中括號包圍的數字（如： "1" 和 "2"）為job的ID號。加號(+)用於指定`fg`和`bg`命令的默認對象job。減號( - )用於指定，若當前的默認對象job退出後，下一個默認對象job是誰。"Running" 和 "Stopped" 表示進程狀態。最後一個欄位為命令。

總結：

- 一般命令在前台執行（`fg`），執行完畢後，控制返回給使用者。
- 在命令後面加上`&`，它會在後台執行（`bg`），並將特殊的環境變量`$!`設置為該任務的進程ID。這時shell可以並發執行其他命令。
- 後台程序試圖寫入數據到終端設備時（與寫入標準輸出不同）可能被阻塞。
- shell可以等待一個後台任務執行完成，只需使用`wait`命令，加上進程ID或者任務序號；也可以等待所有的後台任務，只需使用不加參數的`wait`

## 啟動腳本

bash啟動的時候會運行各種不同的腳本。

當bash作為一個登錄的交互shell被調用，或者作為非交互shell但帶有--login參數被調用時，它首先讀入並執行文件/etc/profile。然後它會依次尋找~/.bash_profile，~/.bash_login，和~/.profile，讀入並執行第一個存在且可讀的文件。--noprofile參數可以阻止bash啟動時的這種行為。

當一個登錄shell退出時，bash讀取並執行~/.bash_logout文件，如果此文件存在。

當一個交互的非登錄shell啟動後，bash讀取並執行~/.bashrc文件。這個行為可以用--norc參數阻止。--rcfile file參數強制bash讀取並執行指定的file而不是默認的~/.bashrc。

如果用sh來調用bash，bash在啟動後進入posix模式，它會儘可能模仿sh歷史版本的啟動行為，以便遵守POSIX標準。用sh名字調用的非交互shell不會去讀取其他啟動腳本，--rcfile參數無效。

當bash以POSIX模式啟動時（例如帶有--posix參數）它使用POSIX標準來讀取啟動文件。在此模式下，交互shells擴展變量ENV，從以此為文件名的文件中讀取命令並執行。

bash會探測自己是不是被遠程shell守護程序運行（通常是rshd）。如果是，它會讀取並執行~/.bashrc中的命令。但是rshd一般不會用rc相關參數調用shell，也不會允許指定這些參數。

### Bash與Bourne shell和csh啟動腳本的比較

Bash的特性是從Bourne shell和csh發展而來，因此一定程度上允許同Bourne shell的啟動文件共享，並提供一些csh使用者熟悉的啟動特性。

#### 設置可繼承的環境變量

Bourne shell登陸時使用 `~/.profile` 來設置環境變量，這些環境變量可以被子進程繼承。Bash可以以兼容的方式使用`~/.profile`，只需在Bash自有的腳本中顯式執行下面這行代碼。通過在`~/.profile` 中避免使用Bash特有的語法，就可以和Bourne shell保持兼容性。

```bash
. ~/.profile
```

#### 別名和函數

更通用的函數以及借鑑自csh的「別名(alias)」很大程度上取代了Bourne shell的別名(alias)和函數。然而這兩個特性一般不能從登錄式shell中繼承，在該登錄式shell的子shell中，它們必須被重新定義。儘管有個環境變量`ENV`可以被用於這個問題，不過 csh 和 Bash 都可以用子shell的啟動腳本直接處理。在Bash當中，`~/.bashrc` 是交互式子shell啟動時執行的腳本。如果想要在登錄式shell中使用 `~/.bashrc` 定義的函數，可以在 `~/.bash_login` 的環境變量後面加上這樣一行：

```bash
. ~/.bashrc
```

#### 登錄與登出時執行的命令

最初登錄時，csh 執行 `~/.login`，可以執行一些只在登錄時執行的操作，例如顯示系統負載、硬碟狀態、是否收到新郵件、在日誌中記錄登錄時間，等等。Bourne shell 可以在 `~/.profile` 文件中模擬這種行為，但並沒有預先定義文件名。可以在 `~/.bash_profile` 文件的環境變量設置和函數定義的後面添加這樣一行：

```bash
. ~/.bash_login
```

與之類似，csh還有一個文件 `~/.logout`，這個文件只在登錄式shell退出時執行。Bash與之對應的文件是 `~/.bash_logout`，並且不需要專門的設置。在 Bourne shell 中，`trap` 這個內置工具可以實現類似的效果。

#### 兼容舊環境的Bash啟動腳本示例

下面這個 `~/.bash_profile` 的框架與 Bourne shell 兼容，並且為 `~/.bashrc` 和 `~/.bash_login` 提供類似於 csh 的語義。`[ -r ''文件名'']` 測試指定文件是否存在，如果不存在，跳過 `&&` 後面的部分

```bash
[ -r ~/.profile ] && . ~/.profile             # 只使用Bourne shell的语法设置环境变量
if [ -n "$PS1" ] ; then                       # 判断是否是交互式shell
   [ -r ~/.bashrc     ] && . ~/.bashrc        # 加载~/.bashrc（tty、prompt、函数设置等）
   [ -r ~/.bash_login ] && . ~/.bash_login    # 执行登录式shell登录时的任务
fi                                            # if块的结束标志
```

### Bash 啟動腳本與作業系統相關的問題

一些 Unix 和 Linux 版本常在 `/etc` 放置 Bash 的系統級啟動腳本。Bash在其標準的初始化過程中執行它們，不過其他啟動腳本可以按照不同於Bash啟動序列文檔所述的順序來讀取這些文件。root 使用者的文件默認內容，以及新使用者被創建時系統提供的默認文件可能有問題。啟動 X Window系統 的啟動腳本可能使用使用者的 Bash 啟動腳本嘗試在 窗口管理器 啟動之前設置使用者的環境變量。這些問題常常可以通過使用 `~/.xsession` 或者 `~/.xprofile` 來讀取 `~/.profile` 而解決。

## 外部連結

- GNU Bash主頁
- GNU Bash在GNU的主頁
- bashdb，帶有調試器的Bash
- Bash入門指南
- Bash同標準unix shell的不同
- Bash by example Bash基礎編程

## 基本資料

- 作者：布萊恩·福克斯
- 開發者：Chet Ramey
- 發行日期：1989年6月8日
- 程式語言：C語言
- 作業系統：Unix-like,、macOS（系統包括較舊的GPLv2版本；當前版本可通過第三方獲得）、Windows（透過WSL提供，為較新的GPLv3+版本）
- 平台：IA-32，x86-64，ppc64el，ARM64，RISC-V 等
- 語言：多語言（gettext）
- 類型：Unix shell、命令語言
- 許可協議：GPLv3+