Bashは、対話型コマンド言語であると同時に、スクリプト実行環境でもあります。 信頼性の高いシェルコードでは、単語、終了ステータス、ファイルディスクリプターを 別々のデータフローとして扱います。Bashの機能が必要なスクリプトは、 明示的なインタープリター指定から始めます。
#!/usr/bin/env bashbash -c 'commands' name arg1 は、name を $0 に、arg1 を $1 に
割り当てます。コマンド文字列が位置パラメーターを使用する場合、この仕様が
重要になります。
シェル文法と実行 #
リストは、パイプラインを ;、&、&&、または || で連結します。
&& と || は短絡演算子であり、& は非同期リストを開始します。
パイプラインはコマンドを | で接続します。|& を使用すると、左側の
コマンドの標準エラー出力もパイプへ送られます。
build && test && deploy
generate_report 2>&1 | tee build.log丸括弧はサブシェルを作成するため、その内部での代入、ディレクトリ変更、 オプションは呼び出し元のシェルに影響しません。波括弧は現在のシェルで実行され、 空白と、末尾のセミコロンまたは改行が必要です。
( cd build && make )
{ cd build && make; }パイプライン内のコマンドは、既定では別々の環境で実行されます。したがって、
パイプライン右側の while ループで代入した変数が、その後も設定されているとは
限りません。代わりに、リダイレクトまたはプロセス置換からループへ入力します。
count=0
while IFS= read -r line; do
((count++))
done < <(printf '%s\n' one two)
printf '%s\n' "$count"shopt -s lastpipe は、非対話型かつジョブ制御が無効という特定の条件下で、
パイプライン最後のコマンドについてこの挙動を変更します。コードは、この
オプションの実行モデルに依存すべきではありません。
条件、ループ、関数 #
シェルの条件はコマンドのステータスです。ゼロは成功、ゼロ以外は失敗を表します。
if、while、until が受け取るのはブール式ではなくリストです。
if [[ -d $directory && -r $directory/config ]]; then
printf 'configuration is readable\n'
elif [[ -e $directory ]]; then
printf 'configuration is unavailable\n' >&2
else
printf 'directory does not exist\n' >&2
fiBashの条件判定には [[ ... ]] を使用します。引用符で囲まれていない
オペランドに対する単語分割とパス名展開を回避できるためです。算術には
(( ... ))、globパターンによる分岐には case を使用します。周囲の
条件式では不要な場合でも、コマンド内の変数展開は引用符で囲みます。
for file in "$@"; do
[[ -f $file ]] || continue
process "$file"
done
retry=0
until fetch; do
((++retry >= 3)) && exit 1
done関数は現在のシェルで実行されます。local は変数を現在の関数に限定し、
return は関数のステータスを返します。引数は関数の位置パラメーターになるため、
"$@" で引き渡します。
run_checked() {
local output
output=$("$@") || return
printf '%s\n' "$output"
}展開と引用 #
コマンドを実行する前に、Bashはブレース展開、チルダ展開、パラメーター展開、 算術展開、コマンド置換、プロセス置換、単語分割、パス名展開、引用符削除を 実行します。パラメーター展開、算術展開、コマンド置換、およびプロセス置換は、 左から右へ実行されます。ブレース展開はパラメーター展開より先に行われるため、 変数を使って範囲を作ることはできません。
引用方法によって、この処理は次のように変わります。
- 単一引用符は、次の単一引用符までのすべての文字をそのまま保持します。
- 二重引用符は、パラメーター展開、算術展開、およびコマンド置換を許可しながら、 一つの単語を保持します。
- 引用符で囲まれていない展開には、単語分割とパス名展開が適用されます。
"$@"は位置パラメーターごとに一つの単語へ展開され、"$*"はIFSの 先頭文字で連結された一つの単語へ展開されます。
files=("$directory"/*.log)
printf '%s\n' "${files[@]}"
name=${path##*/} # Remove the longest matching prefix.
extension=${name##*.}
printf '%s\n' "${name%.*}" # Remove the shortest matching suffix.
printf '%s\n' "$((retries + 1))"一致しない引用符なしのglobは、既定ではリテラルのまま残ります。空の結果が
必要な場合に限り、狭いスコープで nullglob を設定してください。failglob は、
一致しないパターンをエラーにします。for file in $(command) で任意のファイル名を
解析してはなりません。単語分割とglob展開によって名前が破損するためです。
配列、算術、正規表現 #
インデックス配列と連想配列は、"${array[@]}" として展開した場合にのみ
要素の境界を保持します。文字列キーを代入する前に、連想配列を宣言します。
declare -a files=(one 'two words')
declare -A status=([build]=pending)
files+=("three words")
for file in "${files[@]}"; do
printf '<%s>\n' "$file"
done算術式ではシェル変数を使用でき、評価値がゼロ以外の場合にステータス0を返します。
入力の先頭にゼロが含まれる可能性がある場合は、10進数の前に 10# を付けます。
そうしないと、Bashが先頭のゼロを8進数として解釈する可能性があります。
number=008
(( value = 10#$number + 1 ))[[ value =~ regex ]] はPOSIX拡張正規表現を使用します。リテラルでないパターンは
変数に格納し、その展開を引用符で囲まないでください。引用するとマッチングの
意味が変わります。マッチに成功すると、BASH_REMATCH[0] にマッチ全体が入り、
後続の要素にキャプチャーグループが入ります。
version='release-1.2.3'
regex='^release-([0-9]+)\.([0-9]+)\.([0-9]+)$'
if [[ $version =~ $regex ]]; then
printf 'major=%s\n' "${BASH_REMATCH[1]}"
fiリダイレクトと入力 #
リダイレクトは、パイプラインの接続が確立された後、左から右へ適用されます。
2>&1 は標準出力の現在の出力先を複製するため、記述位置によって結果が変わります。
command >output.log 2>&1 # Both standard streams go to the file.
command 2>&1 >output.log # Standard error retains its prior destination.
command |& consumer # Equivalent to command 2>&1 | consumer.コマンドが標準入力を使用している間にループが入力を必要とする場合は、 専用のディスクリプターを開きます。
while IFS= read -r line <&3; do
printf '%s\n' "$line"
done 3< input.txtread -r はバックスラッシュの解釈を防ぎます。変数を指定しない場合、read は
REPLY に代入し、入力区切り文字を除去します。mapfile(別名 readarray)は、
行をインデックス配列へ読み込み、-t を使用しない限り区切り文字を保持します。
mapfile -t lines < input.txt
printf '%s\n' "${lines[@]}"引用符で囲まれていないヒアドキュメント区切り文字では、パラメーター展開、
コマンド置換、および算術展開が有効になります。引用符で囲んだ区切り文字では、
本文がリテラルになります。<<- が除去するのは先頭のタブ文字であり、
空白ではありません。
cat <<'EOF'
literal $HOME and $(command)
EOF起動ファイル、プロンプト、引数 #
対話型ログインBashは、/etc/profile を読み、その後
~/.bash_profile、~/.bash_login、~/.profile のうち最初に読み取り可能な
ファイルを読みます。対話型非ログインBashは ~/.bashrc を読みます。
ディストリビューションのファイルによってシステム全体の起動ファイルが追加される
場合があるため、パスを推測せずローカルのドキュメントを参照してください。
非対話型Bashは、BASH_ENV が設定されている場合、その変数で指定された
ファイルを読みます。
対話型エイリアス、補完、およびプロンプト設定は ~/.bashrc に配置します。
対話型ログインシェルで設定を共有する場合、一般にログインプロファイルから
このファイルをsourceします。子プロセスが必要とする環境変数だけを、
ログイン起動ファイルでexportします。
PS1 は主対話プロンプトを制御します。非表示の端末シーケンスは \[ と \] で
囲んでください。そうしないと、readlineが表示上のプロンプト幅を誤って計算します。
PS1='\[\e[32m\]\u@\h \W\[\e[0m\]\$ '$0 はシェルまたはスクリプトの名前、$# は引数の数であり、$1 から
${10} は引数を選択します。"$@" で引数の境界を保持してください。
現在の引数を意図的に消費した後に限り、shift を使用します。
失敗時の意味とオプション #
set -o pipefail が有効でない限り、パイプラインのステータスは最後のコマンドの
ステータスです。有効な場合は、いずれかのコマンドが失敗するとゼロ以外の
ステータスが報告されます。パイプライン直後の PIPESTATUS には、
各コマンドのステータスが格納されています。
set -o pipefail
producer | consumer
printf 'statuses: %s\n' "${PIPESTATUS[*]}"set -e は「すべてのエラーで終了する」ことを意味しません。Bashは、条件テスト、
&& および || リストの最後以外のコマンド、! でステータスを反転したコマンド、
および複数の複合コマンドのコンテキストで、その効果を抑制します。
したがって、重要な箇所にはエラー処理を記述します。
if ! deploy "$environment"; then
printf 'deployment failed\n' >&2
exit 1
fiset -u は未設定変数の展開を検出しますが、任意入力には ${optional:-} のような
明示的な既定値が必要です。set -x は展開後のコマンドを公開して秘密情報を
漏らす可能性があるため、管理された診断でのみ有効にしてください。
各オプションの意味をスクリプトが考慮している場合にのみ set -euo pipefail を
使用します。これは明示的な検証とクリーンアップを補完するもので、置き換える
ものではありません。
コメント
GitHub で閲覧・コメントする(投稿にはログインが必要です)