Bourne Shell(古いsh)とPOSIXシェル(現在のshやbash)の違い

## Bourne Shell はとっくにレガシーです

Bourne シェルとは昔の UNIX で使われていた古いシェルの名前です。現在のほとんどの sh は Bourne シェルではありません。Bourne シェルは最終バージョン（SVR4.2 版）のリリースが 1992 年なので 30 年も前のシェルです。現在は Bourne シェルの後継シェルが広く使われています。Linux、macOS、BSD 系 Unix（初期の BSD は除く）には Bourne シェルが搭載されたことがないため、古い UNIX を使ったことがある人以外は Bourne シェルを使ったことは無いはずです。Bourne シェルは近く終焉を迎えます。詳細は以下の記事を参照してください。

- [Bourneシェルの終焉まで残り1年（→3年） ～ 新しいシェルへの移行は完了していますか？](https://qiita.com/ko1nksm/items/82f1b64fc656a512aca4)

Bourne シェルは Steve Bourne が開発したシェルの名前です。しばしば勘違いされていますが POSIX で標準化されているシェルは Bourne シェルではありません。`sh` というコマンド名は同じですが、**POSIX の `sh` は Bourne シェルのことでなく POSIX シェルの標準規格にも準拠していません**。現在使われている Bourne シェルの後継シェル（複数あります）は [POSIX の仕様](https://pubs.opengroup.org/onlinepubs/9699919799/utilities/V3_chap02.html)に（ほぼ）準拠しており、それらは総称として POSIX シェルと呼ばれています。具体的には dash、bash、ksh、mksh、yash、zsh 等のことです（**注意** fish は Bourne シェルとも POSIX シェルとも互換性はありません）。POSIX で標準化された機能のみを持っているシェルは存在せず、**すべての POSIX 準拠シェルも拡張機能を持っています**。

たまに 「sh は Bourne シェルです」と言われていたりしますが、**現在では複数のシェルが sh を名乗っている**ため sh という名前ではどのシェルのことなのかわからなくなりました。FreeBSD sh や NetBSD sh は Bourne シェルのクローンとして開発された ash をベースとしたシェルですが、FreeBSD sh や NetBSD sh は Bourne シェルでも ash でもありません。たとえ似ていたとしてもそれぞれは別のシェルで違った機能を持っています。厳密に区別する場合には dash、bash、ksh、FreeBSD sh、NetBSD sh など具体的なシェルの名前を言うようにしてください。

すべての POSIX シェルは Bourne シェルと（完全ではないが）互換性を持った後継シェルなので「Bourne シェル系」と呼ばれることがありますが「Bourne シェル」そのものではありません。また Bourne シェルは C シェル (csh) に対応して B シェルと呼ばれることもありますが曖昧な意味で使われており、いくつかのサイトでは Bourne シェル（系）と言いながら Bourne シェルでは動かないコードを書いていたり、紛らわしいことに Bourne シェル系の意味で B シェルという用語を使いながら Bourne シェルの略と説明しているサイトがあります。Bourne シェルという用語は正しく使われていないことが多々あるため注意してください。

POSIX でシェルが標準化されたのは 1992 年なので、それ以前は POSIX シェルと言う呼び名はありませんでした。従って、当時は一般的に Bourne シェル（系）と呼ばれており、その中に Bourne シェルと後継シェルが含まれていました。しかし現在は Bourne シェルは使わていません。話がややこしくなるので Bourne シェルという呼び方は Bourne シェルそのものを指す時だけに使い、それ以外は POSIX シェルと呼ぶことをおすすめします。

## この記事の内容

Bourne シェルと POSIX シェルはよく勘違いされており私も昔混乱していたので、異なるシェルであるという根拠として具体的に何が違うのかについてまとめます。ただし詳しくまとめるつもりはありません。Bourne シェルの範囲で書けば POSIX シェルでも動くかもしれませんが、これだけ違う所があるんだから 古い Bourne シェルでも動くようにするとかいいかげんやめようぜ、Bourne シェルと呼ぶのをやめようぜ、と伝えるのが目的です。Python 2 系がサポート終了したように Bourne シェルも終了したシェルです。（実際 Bourne シェルがシステムシェルになっているサポート中の OS ってあるのでしょうか？もしどなたか知っている方がいたらコメントで教えて下さい。）

この記事で動作確認した環境と Bourne シェルは Solaris 10 の `/bin/sh` です。Bourne シェルには複数のバージョンがありますが、Solaris 10 の Bourne シェルのバージョンは SVR4.0 相当のようです。正確には Bourne シェルにはバージョン番号がなく、UNIX のバージョンで呼ばれていることが多いのでこの記事でもそれを使用しています。（例 SVR4 - System V Release 4）

## Bourne シェルバージョン覧

- Version 7 (1979)
- System III (1981)
- SVR1 (1983)
- SVR2 (1984)
- SVR3 (1986)
- SVR4.0 (1989) 例 Solaris 10 の `/bin/sh`
- SVR4.2 (1992)

## Bourne シェルは・・・

注意 ここに書いているものは POSIX に準拠したシェルならば必ず実装しなければいけないものばかりです（各シェル固有の拡張は含めていません）。つまり Bourne シェルが POSIX に準拠してない点を述べています。

### 算術式展開に対応していない

```console
$ echo "$((1 + 2))"
$((1 + 2))

$ # expr を使わなければいけない
$ expr 1 + 2
3
```

しばしば `$((...))` は bash 拡張構文だと勘違いされますが、元々は ksh88 で導入された機能で、その後 POSIX で標準化されたため、現在の POSIX に準拠シェルはどのシェルでも使えます。

### 新しいコマンド置換に対応していない

```console
$ echo "$(expr 1 + 2)"
syntax error: `(' unexpected

$ # 古いバッククォートのみ対応
$ echo `expr 1 + 2`
3
```

バッククォートを使った古いコマンド置換は、現在でも使用可能で POSIX でも標準化されていますが、ネストするときにエスケープしなければならず書き方が面倒です。

### パラメーター展開の一部に対応していない

```console
$ var=abc
$ echo "${var#?}"
bad substitution

$ echo "${#var}"
bad substitution

$ # これには対応している
$ echo "${var:-}"
abc
```

これらは ksh88 で導入され POSIX で標準化されました。

### `$PPID` に対応していない

```console
$ echo "$PPID"
（何も出力されない）
```

### `read -r` に対応していない

```console
$ read -r line
-r: is not an identifie
```

SVR4.2 で対応したらしいので、一応 Bourne シェルは対応していることになりますが、そもそも SVR4.2 版の Bourne シェルがほとんど使われていません。

### `while ... do ... done < file` がサブシェルになってしまう

```console
$ echo test > /tmp/dummy
$ var=1
$ while read line; do
>   echo "$line"
>   var=2
> done < /tmp/dummy
test
$ echo "$var" # 2 が表示されなければいけない
1
```

正確にはリダイレクトを伴う複合コマンドがサブシェルになるという仕様です。したがって `for` や `case` などでもリダイレクトと組み合わせれば同じようなことが発生します。

### `export` / `readonly` で値を指定できない

```console
$ export A=123
A=123: is not an identifier

$ # 別々に分ける必要がある
$ A=123
$ export A
```

### `case` のパターンの前に `(` をつけられない

```console
$ case 123 in (*)
syntax error: `(' unexpected
```

`case` のパターンに `(` がつけられるようになったのは、元々は新しいコマンド置換 `$(...)` と組み合わせたときのバグの回避策でした。コマンド置換にバッククォートを使う Bourne シェルにはこの仕様は必要ありませんでした。

### `set --` で位置パラメータを空にできない

```console
$ set -- a b c
$ set --

$ echo "$@"
a b c

$ # shift で代用できる
$ shift $#
$ echo "$@"

```

### `set -u` の状態で位置パラメータがない時に `$@` または `$*` を参照できない

```console
$ set -u
$ shift $#

$ echo "$@"
@: parameter not set

$ echo "$*"
*: parameter not set
```

`${1+"$@"}` という書き方が、上記の問題の回避策として利用されていました。POSIX に準拠したシェルではエラーにならないことが規定されていますが、NetBSD 10 の NetBSD ksh (pdksh) や posh には現在もこのバグがあります。

### `test` (`[` `]`) でハイフンで始まる文字列を演算子と誤認識してしまう

```console
$ var="-z"
$ [ "$var" ]
test: argument expected
```

この問題の回避策として `[ x"$var" ]` のように、頭に任意の文字をつけるという書き方が使われていましたが、現在の POSIX シェルでは不要です。

### `cd --` に対応していない

```console
$ cd -- /
--: does not exist
```

初期のコマンドは `--` に対応していないものがいくつかありました。`--` の意味は以下を参照してください。

[POSIX cd](https://pubs.opengroup.org/onlinepubs/9699919799/utilities/cd.html) 

> OPTIONS
> The cd utility shall conform to XBD Utility Syntax Guidelines .

[POSIX 12.2 Utility Syntax Guidelines](https://pubs.opengroup.org/onlinepubs/9699919799/) 

> Guideline 4:
> All options should be preceded by the '-' delimiter character.

### `cd -` （前のディレクトリに戻る）に対応していない

```
$ cd -
-: does not exist
```

### `$PWD` / `$OLDPWD` に対応していない

```console
$ cd /tmp && cd /
$ echo "$PWD"
（何も出力されない）
$ echo "$OLDPWD"
（何も出力されない）
```

`OLDPWD` は `cd -` で前のディレクトリに戻るために必要となったものです。

### `~` が展開されない

```console
$ cd ~
~: does not exist
```

### `set -C` に対応していない

```console
$ set -C
-C: bad option(s)
```

### `set -o` を使った長いシェルオプション名に対応していない

```console
$ set -o errexit
-o: bad option(s)
```

`-o` を使ったシェルオプションの指定は POSIX で標準化されています。

### `IFS` がコマンド名を単語分割する

```sh
IFS=l
echolocation # echo "ocation" が実行される
```

IFS (Internal Field Separator) という名前の由来は Bourne シェルでの動作から来ているのでしょう。現在の POSIX シェルの IFS の動作は Internal Field Separator という名前にあっていないと思います。

### `IFS` を `unset` できない

```console
$ unset IFS
IFS: cannot unset
```

POSIX には `IFS` が `unset` の場合にデフォルト値と同じように振る舞うという規定があります。（[2.6.5 Field Splitting](https://pubs.opengroup.org/onlinepubs/9699919799/utilities/V3_chap02.html#tag_18_06_05)参照）

### `IFS` で結合できない

```console
$ set -- a b c
$ IFS=:
$ echo "$*"
a b c
```

### `IFS` が `for` のリストを単語分割してしまう

```console
$ IFS=/
$ for i in a/b/c; do echo "$i"; done
a
b
c
```

POSIX に準拠している場合は単語分割を行いません。

```console
$ IFS=/
$ for i in a/b/c; do echo "$i"; done
a/b/c
```

### `!` が使えない

```console
$ if ! false; then
>   echo ok
> fi
!: not found
```

### `command` コマンドがシェルビルトインではない

```console
$ PATH=""
$ command echo "ok"
$ command: not found
```

### シェル関数とシェル変数の名前空間が同じ

```sh
$ var() { echo "test"; }
var #=> test

$ export var
cannot export functions

$ var=123
$ var
var: not found
```

bash の `unset` が変数定義が存在しないときに関数を削除する（POSIX 的に許容された動作）のは、シェル関数とシェル変数の名前空間が同じだったときの名残です。

```sh:bashでの動作
$ foo() { echo "test"; }
$ unset foo
$ foo
foo: command not found
```

### 位置パラメータが `$1` 〜 `$9` までしか使えない

```console
$ echo "${10}"
bad substitution
```

`shift` で引数をずらしていけば参照することはできます。

### `.` で読み込んだスクリプトを `return` で中断することができない

```sh:lib.sh
echo "loaded"
return
```

```console
$ . ./lib.sh
loaded
cannot return when not in function
```

### `umask` がシンボリックモードに対応していない

```console
$ umask u=rwx,g=rx,o=rx
$ umask
0000
$ umask -S

```

### `alias` が使えない

```console
$ alias ECHO=echo
$ ECHO ok
ECHO: not found
```

Solaris 10 には外部コマンド版の `alias` (`/usr/bin/alias`) があるのでそれと勘違いしないように注意してください。

### 変数代入が後ろから評価される

```console
$ value=`echo a>&2` value=`echo b>&2` value=`echo c>&2`
c
b
a
```

### パイプ記号として `^` が使えてしまう

```console
$ echo "abc" ^ sed s/a/A/
Abc
```

Bourne シェルの前の Thompson シェルでは、最初の頃パイプ記号として `^` を使用していました。Thompson シェル用のシェルスクリプトとの互換性を保つための**文書化されていない**機能です。

## Bourne シェルを使いたい場合

もし移植性のテストや古シェルスクリプトを動かしたいなどの理由で古い Bourne シェルを使いたい場合、以下の選択肢があります。

### [Heirloom Project](http://heirloom.sourceforge.net/)

Heirloom Project の中の一つとして [Heirloom Bourne Shell](http://heirloom.sourceforge.net/sh.html) が提供されています。このプロジェクトは OpenSolaris で公開されたコードを元に基本的なコマンドを新しい OS で動くように移植しているプロジェクトです。シェルだけではなく各種コマンドの歴史的版（POSIX 以前）やその POSIX 版など当時のシェルスクリプトを動かすのに必要になる多数のコマンドが移植されています。また UTF-8 へ対応も行われているようです。積極的に開発をするたぐいのプロジェクトではありませんが 2007 年頃でメンテナンスは停止しているようです。ちなみに私は使ったことがありません。

### [Schily-Tools project](http://schilytools.sourceforge.net/)

Schily-Tools project の中の一つとして [Schily Bourne Shell](http://schilytools.sourceforge.net/bosh.html) が提供されています。Jörg Schilling さん（POSIX - Austin Group のメーリングリストでもよく見かける方です）によってメンテナンスされているプロジェクトでシェル以外にもいくつかのコマンドを提供しています。Schily Bourne Shell は OpenSolaris で公開された Bourne シェルをフォークして開発されており「The non-POSIX SVr4/OpenSolaris variant (obosh)」「The minimal POSIX compliant variant (pbosh)」「The POSIX compliant extended variant (bosh)」の 3 つの亜種が含まれています。この中で obosh が Bourne シェル相当のシェルです。プロジェクトは活発にメンテナンスされています。ちなみに私は POSIX シェルである bosh/pbosh を使うためにビルドしていますが obosh 自体はほとんど使っていません。

余談ですが Schily Bourne Shell の亜種の一つである pbosh は 最小限の POSIX 準拠シェルで POSIX で規定されていない機能を全く含んでいません。例えば dash は （おそらく実用上の理由で） POSIX では規定されていない `local` コマンドを例外的に実装していますが pbosh ではそれすらも実装していません。ただ Schily Bourne Shell はビルドが必要でユーザーも少ないため POSIX 準拠のシェルスクリプトの開発には　dash をおすすめしています（yash の POSIX モードもおすすめです）。

## さいごに

純粋な POSIX シェル用のシェルスクリプトを書きたい人は dash がおすすめです。インタラクティブシェルには向きませんが POSIX で規定された機能のみを実装する方針で一部例外を除き方言がほとんどなく採用事例が多いシェルです。Ubuntu / Debian のシステムシェル `/bin/sh` である他、macOS でもデフォルトでインストールされています。

他、なにか気がついたりしたら追記していくかもしれません。

参考文献 （この記事に書いてない違いがたくさん書かれています）

- [The Traditional Bourne Shell Family
](https://www.in-ulm.de/~mascheck/bourne/)
- [Major Differences From The Bourne Shell](https://www.gnu.org/software/bash/manual/html_node/Major-Differences-From-The-Bourne-Shell.html) （正確には bash との違い）