シェルスクリプト・ベタープラクティス

自分用のメモ。

引用部分の凡例

  • 文脈上不要なリンクは削除した。
  • マークを施した箇所がある。

文字符号化スキーム

シェルスクリプトは UTF-8(BOM 無し)で保存する。シェルはファイルの先頭の EF BB BF を BOM として解釈できず、そのまゝトークン化してしまふ。例へば、

() {
	echo OK
}

 # こゝに EF BB BF がある。
okbom.sh

を UTF-8(BOM 附き)で保存して Bash で[1]実行する[2]と、OK と印字される。

改行コード

シェルの改行コードは LF (U+000A) である。CR はほゞ本来の意味で使はれる[3]

字下げ

TAB ( ; U+0009) と空白 ( ; U+0020) を混在させない。既存のファイルをへんしふするときは元の方法に従ふ。その他の場合は TAB を使ふ

字下げに TAB を使ふと、リダイレクト演算子を <<- にするだけで、出力の行頭に TAB がある場合を除いて、ほとんどの場面でヒアドキュメントがインデントできる[4]:

bold=$(tput bold)
normal=$(tput sgr0)

man_name() {
	aliases=$1 && shift
	description=$1 && shift
	
	cat <<-EOD
	${bold}NAME${normal}
	     ${bold}$aliases${normal} - $description
	EOD
}

空白を使ふ場合、ヒアドキュメントのインデントを取り除かなければならない:

man_name() {
  aliases=$1 && shift
  description=$1 && shift

  cat <<EOD
${bold}NAME${normal}
     ${bold}$aliases${normal} - $description
EOD
}

ヒアドキュメントが忌避されて、代はりに無駄な一時ファイルや echo が使はれるおそれもある:

man_name() {
  aliases=$1 && shift
  description=$1 && shift

  echo ${bold}NAME${normal}
  echo "     ${bold}$aliases${normal} - $description"
}

終了ステータス

終了ステータスは慎重に設計する。失敗を全て1にするのはコードスメルになる。01しか使ってゐない場合、失敗の種類を区別するために不要なパラメーターや、一時ファイル、標準入出力などを経由するはめになる。

使用できる値

通常、終了ステータスに使用できる値は0から127までゞある。126はコマンド名は見付かったが、実行可能なユーティリティでなかったときに使ひ、127はコマンドが見付からなかったときに使ふ[5]128は使はない[6][7]128より大きな値は、シグナルを受け取って終了したことを表す[8]ので、他の用途では使はない。

設計方法

設計方法としては、まづ、POSIX のビルトインや標準ユーティリティに倣ふのが良い。例へば、

  • shnice のやうなユーティリティを実行するユーティリティは、実行するユーティリティが126127を返すやうな場合、その値をそのまゝ返す。
  • testgrep のやうな真偽値を返すユーティリティは、真を0、偽を1で表す。
  • expr12tty1のやうにエラーではない失敗が規定されてゐる場合は、1から順に消費する。

BSD 系の sysexits.h[10] に従ふのも良い習慣だと思ふ。


因みに、ほとんどの実装で、exitreturn に範囲外の値を渡すと下位8ビットがそのまゝ返されるが、この動作は未規定unspecifiedである[9:1]

Shebang

シェルスクリプトは Shebang で書きはじめるShebang は、UNIX で、実行するファイルのインタープリターを指定する方法である。

POSIX シェルを使ふ場合、Shebang は、

#!/bin/sh -eu

とする[11]。FreeBSD、Linux、macOS、Cygwin を含むほとんどの OS は、引数が1つだけなら同様に解釈する[12]

env を使って $PATHsh を使はうとするのはやめた方が良いenv の場所が変はることもあるし、別のプログラムによって $PATH が書き換へられてゐることもある[13]

POSIX でないシェル言語を使ひたいとき[14]はさうしてもよい。この記事では主に POSIX シェルについて書き、時々 Bash に触れる。Bash を使ふときは

#!/bin/bash -eu

とする。

シェルオプション

set ビルトインを使ふと、シェルオプションが設定できる。errexit[15]nounset[16] は最初に有効にする:

set -eu

Bash では pipefail[17] を有効にしてもよい:

set -eu -o pipefail

たゞし、これを立てたせゐで || true みたいなバッドイディオムが横行するやうなら、使はない方が良い。

一時的な変更

オプションを一時的に変更したいときは、

reset_cmd=$(set +o)
set +e
false
eval "$reset_cmd"

のやうにする。Bash ではうまくいかないので、一時ファイルを使ふ:

reset=/tmp/reset_$(date +%s)
set +o >"$reset"
set +e
false
. "$reset"

一時ファイルや eval . は普通は使はない。この節は例外。必要なら /dev/urandommktemp などを使ってもよい。

ファイルモード作成マスク

ファイルやディレクトリを作成するときは、事前にファイルモード作成マスクを設定する

umask go-w

とするか、機密ファイルを扱ふ場合は、

umask go-rw

とする。ファイルモード作成マスクが 022 未満の環境でファイルを作成すると、作成してからファイルモードを変更するまでの間にファイルが改竄されるおそれがある。

一時的な変更

ファイルモード作成マスクを一時的に変更したいときは、

# 読者が誤って credentials といふ名前のファイルを破壊しないやうに、credentials が存在しないことを確認してゐる。実用上は不要。
if [ -e credentials ]; then
	exit 64
fi

current_umask=$(umask -S)
umask go-rw
echo "anon:bad password" >credentials
umask "$current_umask"

のやうにする。

trap

trap 特殊ビルトインを使ふと、シグナルをトラップすることができる。例へば、一時ファイルを掃除するには、

cleanup() {
	rm "$tmpfile"
}

trap cleanup EXIT

tmpfile=$(mktemp)

のやうにする[18]

トラップアクションはシグナルごとに1つしか設定できない[19]。例へば、

trap 'echo exit' EXIT SIGQUIT
trap 'echo quit' SIGQUIT

としてから SIGQUIT を受信すると、quit とだけ印字される。

複数のシグナルをトラップした場合のアクションの実行順序は規定されてゐないunspecified[20]

SIGKILL や SIGSTOP はトラップできないことがある[21]。よって、実行しなければならないコマンドをトラップアクションにしてはならない

一時的な変更

trap を一時的に変更したいときは、

reset_trap=$(trap)
trap - EXIT
eval "$reset_trap"

のやうにする。eval は普通は使はない。この節は例外。

一時ファイル

一時ファイルはなるべく使はない。已むを得ないときは、以下のことに注意して使ふ。

ファイルを作成するよりも早く、ファイルモード作成マスクを 022 以上に設定する。ファイルモード作成マスクも参照。

作成したファイルはシェルが終了するときに削除する[22]。SIGKILL や SIGSTOP はトラップされないことがある。trap も参照。

eval

eval 特殊ビルトインはなるべく使はない。已むを得ないときは、以下のことに注意して使ふ。

ユーザーの入力を eval に渡してはならない。他の多くのプログラミング言語の場合におけると同様に、ユーザーの入力を無害化せずに eval に渡すコマンドには、注入攻撃に対する脆弱性がある。例へば、

read greeting
eval "echo \"$greeting\""

は、"; ls # と入力されると、作業ディレクトリの内容を印字してしまふ。

シェルオプションの一時的な変更trap の一時的な変更のやうな場合は、決まった方法で使ふ。

コマンド

シェルで実行可能な文をコマンドと言ふ。POSIX のコマンドは

  • シンプルコマンド
  • パイプライン
  • リスト
  • 複合コマンド
  • 関数定義コマンド

5種類に分類されてゐる。

シンプルコマンド

0個以上の変数代入、0個以上の入出力リダイレクト、省略可能なコマンド名、そして、コマンド名がある場合、0個以上の引数を適当に組み合はせたものであって、空でないものゝことをシンプルコマンドと言ふ。変数代入はコマンド名の左側にあり、引数はコマンド名の右側にある[23]。よって、次のやうなコード片もシンプルコマンドである:

foo=1 bar=2
>>a
<<-EOD >cwd cat
	$PWD
EOD

コマンド名も単語展開されることに注意。つまり、

cmd="ls /"
$cmd

とすると、ls / が実行される[24]ユーザーの入力をコマンド名にしない

パイプライン

1つ以上のコマンドを | で繋ぎ、左端に省略可能な ! を附け加へたものを、文法上、パイプラインと言ふ[25]

while 文や for 文にパイプしない。パイプラインの各コマンドはサブシェルで実行される[26]ので、パイプラインで実行される変数代入やビルトインコマンドは、現在の環境には影響しない[27]。そのため、ファイルリストにあり、名前が . で始まるファイルの個数を数へるプログラム[28]は、

count=0
cat /path/to/filelist | while IFS= read file; do
	case $file in
		(.*) (( ++count )) ;;
	esac
done
echo "$count"

と書くべきではない[29]。代はりに、

count=0
while IFS= read file; do
	case $file in
		(.*) (( ++count )) ;;
	esac
done </path/to/filelist
echo "$count"

とするのが主流である。

ファイルの内容ではなく標準出力をループに渡したいときは、名前付きパイプを使ふ。例へば、作業ディレクトリにあり、名前が . で始まるファイルの個数を数へるプログラムは、

umask go-w
trap "rm /tmp/filelist" EXIT
mkfifo /tmp/filelist
ls -Apq | grep -v /$ >/tmp/filelist &
count=0
while IFS= read file; do
	case $file in
		(.*) (( ++count )) ;;
	esac
done </tmp/filelist
rm /tmp/filelist
echo "$count"

のやうに書く[30]

Bash ならプロセス置換を使って、

count=0
while IFS= read file; do
	case $file in
		(.*) (( ++count )) ;;
	esac
done < <(ls -Apq | grep -v /$)
echo "$count"

としてもよい。

Bash 4.2 以上では、lastpipe オプションを使ふと、パイプラインの最後のコマンドを現在のシェルで実行することができる[31]

通常のパイプラインの終了ステータスについては 2.9.2 Pipelines

パイプラインがやく! で始まらないなら、終了ステータスは、パイプラインで指定された最後のコマンドの終了ステータスとなる。さうでなければ、終了ステータスは、最後のコマンドの終了ステータスの論理 NOT となる。つまり、最後のコマンドが0を返せば終了ステータスは1となり、最後のコマンドが0より大きな値を返せば、終了ステータスは0となる。

 原文

If the pipeline does not begin with the ! reserved word, the exit status shall be the exit status of the last command specified in the pipeline. Otherwise, the exit status shall be the logical NOT of the exit status of the last command. That is, if the last command returns zero, the exit status shall be 1; if the last command returns greater than zero, the exit status shall be zero.

とある。わざわざin the pipeとかspecified in the pipelineとか書かれてゐるのは、恐らく、時間的に最後に返された値を使ふのではなく、字句的に最後に書かれてゐるコマンドによって返された値を使ふといふことを明示してゐるのだらう。

関数定義コマンド

先頭に function を附けない。POSIX の関数定義コマンドの構文は、

fname ( ) compound-command [io-redirect ...]

である。引用元は 2.9.5 Function Definition Command

本文にはブレースグループ[32]以外の複合コマンドを使ふこともできる。例へば、サブシェルを使った

oldpwd() (cd -)

のやうな関数[33]や、while 文を使った

yes() while :; do
	echo "${1-y}"
done

のやうな関数が書ける。

変数代入

シェルの変数代入はシンプルコマンドの一部で、コマンド名の左にあって、変数名の右に = と変数の値が続くものである。シンプルコマンドは0個以上の変数代入を持つ。

コマンド名と引数は省略できるので、

foo=1 bar=2

のやうに、1つのコマンドで複数の変数代入を行ひ、その変数代入を現在の環境に影響させることもできる。シンプルコマンドも参照。

シンプルコマンドは、変数代入でもリダイレクトでもない部分を単語展開し、リダイレクトを実行し、変数代入を単語展開し、それから変数代入を行ふといふ順で実行される[34]。コマンドは、コマンド名があれば、その後に実行される[35]。よって、例へば、

foo=1 bar=$foo
echo "$bar"

は空行を出力するが、

foo=1 echo hello >"$foo"

は、foo=1 が行はれるよりも前に >"$foo" が実行されるため、sh: : No such file or directory のやうなメッセージを印字する。この振る舞ひは見落としやすいので、注意して使ふ[36]

コマンド名が存在する場合、そのコマンド名が標準ユーティリティなら変数代入はそのコマンドの環境にしか影響せず、特殊ビルトインや関数なら現在の環境に影響する[37]。例へば、

foo=1
foo=2 true
echo "$foo"

1 を印字し、

foo=1
foo=2 :
echo "$foo"

2 を印字する。

この振る舞ひはかなりやゝこしいので、特殊ビルトインや関数を実行するときは、そのコマンドで変数代入を行はないやうにする。POSIX の特殊ビルトインは . : break continue eval exec exit export readonly return set shift times trap unset15個。なほ、exportreadonly の右にあるものは引数であって、変数代入ではない。

単語展開

単語展開は、まづ、チルダ展開、パラメーター展開、コマンド置換、算術展開が同時に起こり、続いて、フィールド分割、パス名展開、引用符削除がこの順で起こる[38]。引用符削除が最後に起こるため、単語[39]を引用符で囲むと、他の種類の単語展開ができる。引用符には "' が使へる。" で囲まれた部分では、` $ \ が特別扱ひされる[40]ので、パラメーター展開、コマンド置換、算術展開は起こる。つまり、チルダ展開、フィールド分割、パス名展開だけが沮止される。よって、(チルダ展開、)フィールド分割($IFS が null でない場合。)、またはパス名展開(noglob シェルオプションが無効な場合。)が起こる部分で、チルダ展開、フィールド分割、およびパス名展開のいづれも使はずに、パラメーター展開、コマンド置換、または算術展開を使ふ場合、その部分を " で囲むべきである。これに相当する部分には、

  • コマンド名
  • 引数
  • 変数代入の左辺
  • 変数代入の右辺
  • ヒアドキュメントの区切り文字を除く、リダイレクトの右辺
  • for 文の in の右の部分
  • case 文の case の右の部分

などがある。

たゞし、変数代入の右辺や case 文の case の右の部分は、フィールド分割とパス名展開が起こらないため、" で囲んでも、チルダ展開が起こらなくなるだけである。これらの部分で ~ を使ふときは大抵、チルダ展開を期待してゐる[41]ので、" で囲む理由は少ない。また、パス名展開は変数代入の左辺でも起こらない。

ヒアドキュメントの区切り文字では引用符削除しか起こらないので、区切り文字の単語展開を沮止するために引用符で囲む必要性は無い。' で囲むと、ヒアドキュメントの単語展開が沮止できる。


${TMPDIR:-/tmp} のやうな、$foo ${foo} 以外の形式のパラメーター展開は、パフォーマンスに貢献することが多い。

コマンド置換には $(command) を使ひ`command` は使はない。" で囲まれた部分では `` も展開されることに注意。例へば、echo "`(cd ..; pwd)`" は作業ディレクトリの親ディレクトリのパスを印字する。

関数

関数は以下のことに注意して使ふ。

引数

引数は位置パラメーターとして渡される。位置パラメーターとは、数字で識別され、$0 $1 といった形式で参照されるパラメーターである。10以上のものは ${10} のやうに数字を波括弧で囲まなければならない[42]。引数に使はれるのは1以上のものであり、$0 はシェルかシェルスクリプトの名前を持つ。

読みづらいときは適当な名前のパラメーターに代入すると良い。位置が決まってゐるならそのまゝ代入し、さうでない場合は getopts を使ふ。シェルスクリプトでは getopts より複雑なことはしない方が良い。位置パラメーターのまゝ使ふ場合、ドキュメントで役割を説明する。

加工して返すべき値は標準入力として受け取る

返り値

返り値は終了ステータスとして返される。終了ステータスは単なる8ビット非負整数なので、関数やプログラムの結果を表すには心許ない。そのため、シェルでは、真偽値を返す場合を除いて、結果は標準出力に送る

条件

シェルの条件のしゃうたいは、真なら0を、偽なら0でない値を返すコマンドである。if 文の if elif の右の部分と while 文の while の右の部分は複合コマンドである[43]

複合条件

複合条件は、AND-OR リストを使って

if [ -n "$foo" ] && [ -e "$file_path" ]; then
	echo no good
fi

のやうに書く。test-a -o プライマリは使はない。test は引数の個数が4より大きい場合については規定されてゐないunspecified[44]。例へば、

[ -n "=" -a -e "/path/to/file" ]

はうまく動かないことがある[45]が、

[ -n "=" ] && [ -e "/path/to/file" ]

はうまく動く。

否定条件

否定条件は、パイプラインの ! を使って

if ! command -v openssl >/dev/null; then
	echo "No command 'openssl' found." >&2

	return 69 # EX_UNAVAILABLE
fi

のやうに書く。test! プライマリは使はない。

条件のグループ

条件をグループするときはブレースグループ[32:1]を使ふ。サブシェルは使はない。例へば、複合条件の否定は、

if ! { [ -n "$foo" ] && [ -e "$file_path" ]; } then
	echo good
fi

といふゝうに書く。ブレースグループの終はりには ;[46] が無ければならない。サブシェルを用ゐて

! ([ -n "$foo" ] && [ -e "$file_path" ])

のやうにする方法は、一見すると良さゝうだが、かうすると、サブシェルで実行されるコマンドがシェルを終了させるやうな場合に、現在のシェルが終了しなくなってしまふ[47]。なほ、この条件を

! [ -n "$foo" ] && [ -e "$file_path" ]

と書くことはできない。! はパイプラインの一部であり、AND-OR リストの && || はパイプラインを被演算子に取るので、かうすると、! [ -n "$foo" ][ -e "$file_path" ] の論理積として解釈されてしまふ。

その他の一般的なパターン

エントリーポイント

スクリプトのほゞぜんたいmain といふ名前の関数にする。例へば、

#!/bin/sh
set -eu

main() {
	echo OK
}

main "$@"

といふゝうにする。かうすると、2つ良いことがある。1つはエントリーポイントが明確になり、関数定義が main のロジックから自然に分離されることである。

もう1つは関数定義はキャッシュされるといふことである。例へば、

#!/bin/sh
set -eu

main() {
	foo=1
	sleep 2
	# unset foo
	echo "foo: $foo"
}

main "$@"
poc1.sh

は、

./poc1.sh &
sleep 1
sed "s/# //" <poc1.sh >/tmp/poc1.sh
cat /tmp/poc1.sh >poc1.sh

のやうに実行されても、2秒後に foo: 1 を印字する。main が無ければ、echo "foo: $foo" でパラメーター展開に失敗して終了する。

無駄な〇〇を避ける

無駄な cat を避ける。例へば、cat | といふコード片は、cat は標準入力を標準出力に移し、パイプラインの | は左辺のコマンドの標準出力を右辺のコマンドの標準入力に繋げるだけなので、いつでも省略できる。

Useless Use of Cat Award も参照。

無駄な echo を避ける。例へば、複数行の文字列を印字するときは、

	echo "line 1" >&2
	echo "line 2" >&2

ではなく

	cat <<-EOF >&2
	line 1
	line 2
	EOF

と書く。


  1. POSIX シェルでは、関数名は 3.235 Name に従ふので、BOM (EF BB BF) は関数名にならない。 ↩︎

  2. bash okbom.sh を実行するか、実行可能にしてから Bash シェル上で ./okbom.sh を実行する。 ↩︎

  3. 3.86 Carriage-Return Character (<carriage-return>) には

    出力ストリームで、キャリッジリターンが現れたのと同じ物理行の先頭から印字が始まるといふことを示す文字。C 言語では '\r' で示される文字である。この文字が行の先頭への移動を達成するためにシステムが出力デバイスに送信する正確なシーケンスであるかどうかは規定されてゐないunspecified

     原文

    A character that in the output stream indicates that printing should start at the beginning of the same physical line in which the carriage-return occurred. It is the character designated by '\r' in the C language. It is unspecified whether this character is the exact sequence transmitted to an output device by the system to accomplish the movement to the beginning of the line.

    とある。 ↩︎

  4. 2.7.4 Here-Document には

    リダイレクト演算子が "<<-" なら、入力行と、末尾の区切り文字がある行から、全ての行頭の <tab> 文字が取り除かれる。

     原文

    If the redirection operator is "<<-", all leading <tab> characters shall be stripped from input lines and the line containing the trailing delimiter.

    とある。 ↩︎

  5. 2.8.2 Exit Status for Commands には

    コマンドが見付からなかった場合、終了ステータスは127となる。コマンド名は見付かったが、実行可能なユーティリティではなかった場合、終了ステータスは126となる。シェルを使はずにユーティリティを呼び出すアプリケーションは、同様のエラーを報告するためにこれらの終了ステータスを使ふべきである。

     原文

    If a command is not found, the exit status shall be 127. If the command name is found, but it is not an executable utility, the exit status shall be 126. Applications that invoke utilities without using the shell should use these exit status values to report similar errors.

    とある。 ↩︎

  6. 128は、シグナル番号が0のシグナルを受け取って終了したときに返されるkill には

    sig0(null シグナル)の場合、エラーチェックは実行されるが、シグナルは実際には送信されない。null シグナルは pid の妥当性を調べるために使はれることがある。

     原文

    If sig is 0 (the null signal), error checking is performed but no signal is actually sent. The null signal can be used to check the validity of pid.

    とあり、実際には使はれない。 ↩︎

  7. Appendix E. Exit Codes With Special Meanings には

    Exit Code Number Meaning Example Comments
    128 Invalid argument to exit exit 3.14159 exit takes only integer args in the range 0 - 255 (see first footnote)
    255* Exit status out of range exit -1 exit takes only integer args in the range 0 - 255

    とある(行を抜粋した。)が、実際は exit 3.14159 を実行すると255が返る。また、exit -1 を実行すると255が返るのは、-1 の下位8ビットが返されてゐる(この振る舞ひは未規定unspecified[9]。)だけである。 ↩︎

  8. 2.8.2 Exit Status for Commands には

    シグナルを受け取ったせゐで終了したコマンドの終了ステータスは128より大きいものとして報告される。

     原文

    The exit status of a command that terminated because it received a signal shall be reported as greater than 128.

    とある。 ↩︎

  9. returnexit にはそれぞれ

    If n is not an unsigned decimal integer, or is greater than 255, the results are unspecified.

    If n is specified, but its value is not between 0 and 255 inclusively, the exit status is undefined.

    とある。 ↩︎ ↩︎

  10. sysexits(3) ↩︎

  11. 以前の版では #!/bin/sh を推してゐたが、そのやうな OS には専用のスクリプトが書かれるべきだと思ひ直したので、撤回した。 ↩︎

  12. The #! magic, details about the shebang/hash-bang mechanism on various Unix flavours を参照。 ↩︎

  13. sh が差し替へられると、Shebangenv を使ってゐるプログラムは動作しなくなることがある。次のスクリプトを実行すると sh is concealed. と印字される:

    #!/bin/sh -eu
    
    umask go-w
    
    tmpdir=$(mktemp -d)
    
    cat <<-EOD >"$tmpdir/demo.sh"
    #!/usr/bin/env sh
    
    echo demo.sh executes successfully.
    EOD
    
    echo echo sh is concealed. >"$tmpdir/sh"
    
    chmod +x "$tmpdir/demo.sh"
    chmod +x "$tmpdir/sh"
    
    export PATH=$tmpdir:$PATH
    
    "$tmpdir/demo.sh"
    
    env-on-path.sh

    脆弱性のある sh がインストールされるとさらに悪いことが起こる。Shebang#!/bin/sh なら、/bin/sh たいが書き換へられない限り、このやうな攻撃に遭ふことはない。なほ、念のために附すと、$PATH が書き換へられてゐる時点で、サーバーにログインされたり、不審なプログラムを自ら実行したりしてゐるはずなので、env 自體が脆弱といふわけではない。 ↩︎

  14. 元ひ、POSIX に準拠したくないとき。POSIX 準拠のシェルスクリプトを書くのは意外に難しいので、この記事では深く立ち入らないやうにしてゐる。 ↩︎

  15. set には

    このオプションが有効な場合、(シェルエラーの結果Consequences of Shell Errorsに列挙されてゐる理由のいづれかで、あるいは0より大きな終了ステータスの返却によって、)コマンドが失敗すると、次の例外を除いて、あたかも exit 特殊ビルトインユーティリティを引数無しで実行したかのやうに、シェルが即座に終了する:

     原文

    When this option is on, when any command fails (for any of the reasons listed in Consequences of Shell Errors or by returning an exit status greater than zero), the shell immediately shall exit, as if by executing the exit special built-in utility with no arguments, with the following exceptions:

    とある。 ↩︎

  16. set には

    シェルが未定義unsetのパラメーター('@' 特殊パラメーターと '*' 特殊パラメーターは除く。)を展開しようとすると、標準エラーにメッセージが書き込まれ、シェルエラーの結果Consequences of Shell Errorsにある結果で展開が失敗する。

     原文

    When the shell tries to expand an unset parameter other than the '@' and '*' special parameters, it shall write a message to standard error and the expansion shall fail with the consequences specified in Consequences of Shell Errors.

    とある。 ↩︎

  17. Chapter 33. Options には

    パイプラインに、0以外の返り値を返したパイプ内の最後のコマンドの終了ステータスを返させる。

     原文

    Causes a pipeline to return the exit status of the last command in the pipe that returned a non-zero return value.

    とある。 ↩︎

  18. mktemp は POSIX には含まれてゐない。 ↩︎

  19. trap には

    trap のアクションは前のアクション(デフォルトのアクションと明示的に設定されたアクションのどちらか)をオーバーライドする。

     原文

    The action of trap shall override a previous action (either default action or one explicitly set).

    とある。 ↩︎

  20. 2.11. Signals and Error Handling には

    関聯するトラップアクションのあるシェルに対して複数のシグナルが保留されてゐる場合、トラップアクションの実行の順序は規定されないunspecified

     原文

    If multiple signals are pending for the shell for which there are associated trap actions, the order of execution of trap actions is unspecified.

    とある。 ↩︎

  21. trap には

    SIGKILL や SIGSTOP へのトラップの設定は未定義の結果を生ずる。

     原文

    Setting a trap for SIGKILL or SIGSTOP produces undefined results.

    とある。 ↩︎

  22. /tmp や /var/tmp に作成したものはそのまゝにしてもよいと思ふ。 ↩︎

  23. 2.9.1 Simple Commands も参照。 ↩︎

  24. 例ではフィールド分割も利用してゐる。 ↩︎

  25. 2.9.2 Pipelines を参照。 ↩︎

  26. 2.12. Shell Execution Environment には

    コマンド置換、丸括弧でグループされたコマンド、および非同期リストは、サブシェル環境で実行される。加へて、複数コマンドのパイプラインの各コマンドも、サブシェル環境にある。たゞし、拡張機能として、パイプラインの一部または全部のコマンドが現在の環境で実行されてもよい。他のコマンドは全て現在のシェル環境で実行される。

     原文

    Command substitution, commands that are grouped with parentheses, and asynchronous lists shall be executed in a subshell environment. Additionally, each command of a multi-command pipeline is in a subshell environment; as an extension, however, any or all commands in a pipeline may be executed in the current environment. All other commands shall be executed in the current shell environment.

    とある。 ↩︎

  27. サブシェルの挙動については、2.9.4 Compound Commands2.12. Shell Execution Environment

    ( compound-list )
    compound-list をサブシェル環境で実行する。シェル実行環境を見よ。変数代入と環境に影響をあたへるビルトインコマンドは、リストが終了した後も有効なまゝであってはならない。
     原文
    ( compound-list )
    Execute compound-list in a subshell environment; see Shell Execution Environment. Variable assignments and built-in commands that affect the environment shall not remain in effect after the list finishes.

    サブシェル環境は、無視されないシグナルトラップはデフォルトのアクションに設定されるといふことを除いて、シェル環境の複製として作られる。サブシェル環境に加へられた変更はシェル環境に影響しない。

     原文

    A subshell environment shall be created as a duplicate of the shell environment, except that signal traps that are not being ignored shall be set to the default action. Changes made to the subshell environment shall not affect the shell environment.

    とある。 ↩︎

  28. 別の実装:

    while IFS= read file; do
    	case $file in
    		(.*) echo ;;
    	esac
    done </path/to/file | wc -l
    

    この方法なら while 文にパイプしてもうまく動く。たゞし、ファイルリストがばうだいになるにつれて、本文の方法よりも早くパフォーマンスが悪化する。 ↩︎

  29. 2.12. Shell Execution Environment には

    たゞし、拡張機能として、パイプラインの一部または全部のコマンドが現在の環境で実行されてもよい。

     原文

    as an extension, however, any or all commands in a pipeline may be executed in the current environment.

    とあるため、シェルの実装によってはうまく動くこともある。 ↩︎

  30. このコードは while 文へのパイプについて説明するためのものであり、その他の部分には無駄がある。より良い別の実装:

    find .* ! -path . -prune -type f -exec printf %c {} + | wc -c
    
    ↩︎
  31. 37.3. Bash, version 4 には

    lastpipe シェルオプションが設定されてゐる場合、パイプの最後のコマンドはサブシェルで実行されない

     原文

    When the lastpipe shell option is set, the last command in a pipe doesn't run in a subshell.

    とある。 ↩︎

  32. 複合リストを波括弧で囲んだもの。複合リストについては 2.9.3 Lists

    「複合リスト」といふ用語は、シェル文法にある文法に由来するもので、<newline> 文字で区切られたリストlistsのシーケンスであり、任意個数の <newline> 文字が先行し、または後続することができるものである。

     原文

    The term "compound-list" is derived from the grammar in Shell Grammar; it is equivalent to a sequence of lists, separated by <newline> characters, that can be preceded or followed by an arbitrary number of <newline> characters.

    とある。 ↩︎ ↩︎

  33. 別の実装:

    oldpwd() {
    	echo ~-
    }
    
    ↩︎
  34. 2.9.1 Simple Commands には

    与へられたシンプルコマンドが実行される必要がある場合(すなはち、AND-OR リストや case 文などの条件構造がそのシンプルコマンドをバイパスしてゐない場合)、コマンドテキストの最初から最後にかけて、次の展開、代入、およびリダイレクトが全て実行される:

    1. シェル文法規則に従って、変数代入またはリダイレクトとして認識される単語が、ステップ3とステップ4の処理のために保存される。
    2. 変数代入でもリダイレクトでもない単語が展開される。展開後にフィールドが残ってゐる場合、最初のフィールドはコマンド名と見做され、残りのフィールドはそのコマンドの引数と見做される。
    3. リダイレクトに記述されてゐるやうに、リダイレクトが実行される。
    4. それぞれの変数代入は、値を代入する前に、チルダ展開、パラメーター展開、コマンド置換、算術展開、そして引用符削除される。

    先のリストにおいて、ステップ2からコマンド名が生じない場合、またはコマンド名が特殊ビルトインユーティリティ(特殊ビルトインユーティリティを見よ。)の名前に一致する場合、ステップ3とステップ4の順序は入れ替はってもよい。

     原文

    When a given simple command is required to be executed (that is, when any conditional construct such as an AND-OR list or a case statement has not bypassed the simple command), the following expansions, assignments, and redirections shall all be performed from the beginning of the command text to the end:

    1. The words that are recognized as variable assignments or redirections according to Shell Grammar Rules are saved for processing in steps 3 and 4.
    2. The words that are not variable assignments or redirections shall be expanded. If any fields remain following their expansion, the first field shall be considered the command name and remaining fields are the arguments for the command.
    3. Redirections shall be performed as described in Redirection.
    4. Each variable assignment shall be expanded for tilde expansion, parameter expansion, command substitution, arithmetic expansion, and quote removal prior to assigning the value.

    In the preceding list, the order of steps 3 and 4 may be reversed if no command name results from step 2 or if the command name matches the name of a special built-in utility; see Special Built-In Utilities.

    とある。 ↩︎

  35. 2.9.1 Simple Commands には

    コマンド名がある場合、コマンドの検索と実行に記述されてゐるやうに実行が続行する。コマンド名は無いが、コマンドはコマンド置換を含んでゐるやうな場合、コマンドは、最後に実行されたコマンド置換の終了ステータスで完了する。その他の場合、コマンドは終了ステータス0で完了する。

     原文

    If there is a command name, execution shall continue as described in Command Search and Execution . If there is no command name, but the command contained a command substitution, the command shall complete with the exit status of the last command substitution performed. Otherwise, the command shall complete with a zero exit status.

    とある。 ↩︎

  36. しばらく考へたけれど、実用性を犠牲にせずに構文でうまく立ち回る方法は思ひ付かなかった。 ↩︎

  37. 2.9.1 Simple Commands には

    変数代入は次のやうに実行される:

    • 結果としてコマンド名が無い場合、変数代入は現在の実行環境に影響する。
    • コマンド名が特殊ビルトインユーティリティでも関数でもない場合、変数代入は、そのコマンドの実行環境にエクスポートされ、ステップ4で実行される展開の副作用を除いて、現在の実行環境には影響しない。この場合、次のことは規定されないunspecified:
      • 代入がステップ4のその後の展開において可視かどうか
      • これらの展開の副作用として実行される変数代入がステップ4のその後の展開、現在のシェル実行環境、またはその両方において可視かどうか
    • コマンド名が関数として実装された標準ユーティリティ(XBD のユーティリティを見よ。)の場合、変数代入の影響は、そのユーティリティが関数として実装されてゐなかったかのやうに現れる。
    • コマンド名が特殊ビルトインユーティリティの場合、変数代入は現在の実行環境に影響するset -a オプションが有効でない場合(set を見よ。)、次のことは規定されないunspecified:
      • 特殊ビルトインユーティリティの実行中に変数が export 属性を得るかどうか
      • 特殊ビルトインユーティリティの完了後に、変数代入の結果として得た export 属性が永続するかどうか
    • コマンド名が関数として実装された標準ユーティリティでない関数の場合、その関数の実行中は、変数代入が現在の実行環境に影響する。次のことは規定されないunspecified:
      • 関数の完了後に変数代入が永続するかどうか
      • 関数の実行中に変数が export 属性を得るかどうか
      • (関数の完了後に変数代入が永続する場合、)関数の完了後に変数代入の結果として得た export 属性が永続するかどうか
     原文

    Variable assignments shall be performed as follows:

    • If no command name results, variable assignments shall affect the current execution environment.
    • If the command name is not a special built-in utility or function, the variable assignments shall be exported for the execution environment of the command and shall not affect the current execution environment except as a side-effect of the expansions performed in step 4. In this case it is unspecified:
      • Whether or not the assignments are visible for subsequent expansions in step 4
      • Whether variable assignments made as side-effects of these expansions are visible for subsequent expansions in step 4, or in the current shell execution environment, or both
    • If the command name is a standard utility implemented as a function (see XBD Utility), the effect of variable assignments shall be as if the utility was not implemented as a function.
    • If the command name is a special built-in utility, variable assignments shall affect the current execution environment. Unless the set -a option is on (see set), it is unspecified:
      • Whether or not the variables gain the export attribute during the execution of the special built-in utility
      • Whether or not export attributes gained as a result of the variable assignments persist after the completion of the special built-in utility
    • If the command name is a function that is not a standard utility implemented as a function, variable assignments shall affect the current execution environment during the execution of the function. It is unspecified:
      • Whether or not the variable assignments persist after the completion of the function
      • Whether or not the variables gain the export attribute during the execution of the function
      • Whether or not export attributes gained as a result of the variable assignments persist after the completion of the function (if variable assignments persist after the completion of the function)

    とある。 ↩︎

  38. 2.6 Word Expansions には

    単語展開の順序は次の通り:

    1. チルダ展開(チルダ展開を見よ。)、パラメーター展開(パラメーター展開を見よ。)、コマンド置換(コマンド置換を見よ。)、そして算術展開(算術展開を見よ。)が全て実行される。トークン認識の第5項目を見よ。
    2. IFS が null でなければ、ステップ1で生成されたフィールドの部分に対して、フィールド分割(フィールド分割を見よ。)が実行される。
    3. set -f が有効でなければ、パス名展開(パス名展開を見よ。)が実行される。
    4. 最後に、引用符削除(引用符削除を見よ。)が常に実行される。
     原文

    The order of word expansion shall be as follows:

    1. Tilde expansion (see Tilde Expansion), parameter expansion (see Parameter Expansion), command substitution (see Command Substitution), and arithmetic expansion (see Arithmetic Expansion) shall be performed, beginning to end. See item 5 in Token Recognition.
    2. Field splitting (see Field Splitting) shall be performed on the portions of the fields generated by step 1, unless IFS is null.
    3. Pathname expansion (see Pathname Expansion) shall be performed, unless set -f is in effect.
    4. Quote removal (see Quote Removal) shall always be performed last.

    とある。 ↩︎

  39. 単語展開されるトークン。 ↩︎

  40. 2.2.3 Double-Quotes には

    二重引用符("")で文字charactersを囲むと、次のやうに、その二重引用符の中の(バッククォート、<dollar-sign>、および <backslash> を除く)全ての文字のリテラル値が保持される:

     原文

    Enclosing characters in double-quotes ( "" ) shall preserve the literal value of all characters within the double-quotes, with the exception of the characters backquote, <dollar-sign>, and <backslash>, as follows:

    とある。 ↩︎

  41. 次のやうなコマンドを想像すると分かりやすいと思ふ:

    tilde="~"
    home=~
    
    case ~ in
    	(/home/* | /Users/*) echo 'I'"'"'m a huperoffspring!'
    esac
    
    ↩︎
  42. 2.6.2 Parameter Expansion には

    パラメーターが波括弧で囲まれてをらず、かつ名前である場合、その名前で表される変数が存在するかどうかにかゝはらず、展開は最長の妥当な名前(XBD の名前を見よ。)を使ふ。その他の場合、パラメーターは1文字のシンボルであり、その文字が数字でも特殊パラメーター(特殊パラメーターを見よ。)の内の一つでもない場合の振る舞ひは未規定unspecifiedである。

     原文

    If the parameter is not enclosed in braces, and is a name, the expansion shall use the longest valid name (see XBD Name), whether or not the variable represented by that name exists. Otherwise, the parameter is a single-character symbol, and behavior is unspecified if that character is neither a digit nor one of the special parameters (see Special Parameters).

    とある。 ↩︎

  43. この性質のおかげで、例へば、

    if case $PWD in ($HOME*) true ;; (*) false; esac; then
    	cat <<-'EOD'
    	I'm in my own $HOME.
    	EOD
    fi
    

    みたいに case 文を条件にして、ある文字列の中に別のある文字列が含まれるかどうかを判定することができる。test にはかういふ機能は無い。 ↩︎

  44. test を参照。 ↩︎

  45. GNU bash 5.1(POSIX モード)で実行すると、標準エラーに

    sh: [: too many arguments
    

    と印字された。 ↩︎

  46. より一般的にはセパレーター。; もしくは & の右に0個以上の改行が続くもの、または1つ以上の改行。 ↩︎

  47. exit には

    現在の実行環境がサブシェル環境の場合、シェルは、指定された終了ステータスでそのサブシェル環境を終了し、そのサブシェル環境が呼び出された環境で続行する

     原文

    If the current execution environment is a subshell environment, the shell shall exit from the subshell environment with the specified exit status and continue in the environment from which that subshell environment was invoked;

    とある。 ↩︎