シェルのコロンは何もしません。それでも使いましょう

シェルのコロンは何もしません。それでも使いましょう

はじめに

シェルのコロン (:) は、引数を評価してその結果を破棄する組み込みの null-command です。単体では何も行いませんが、パラメータ展開の前に置くことで、シェルが結果の文字列をコマンドとして実行しようとすることなく、展開を実行させることができます。

必須引数のバリデーション

${VAR:?diagnostic} 展開に : を使用すると、変数が設定されており、かつ空でないことを確認するワンライナーが実現できます。

: "${1:?missing argument, aborting.}"
echo "Hello $1!"

もし $1 が未設定または空の場合、シェルは診断メッセージを stderr に出力して終了ステータス非ゼロで終了します。そうでなければ、展開は $1 の値を返し、スクリプトは続行されます。同じテクニックは名前付き変数でも機能します。例えば : "${GREET_NAME:?missing argument, aborting.}" は、エラーメッセージに変数名を含めることができます。

:= によるデフォルト値の設定

${VAR:=default} 展開は、VAR が未設定または空の場合にデフォルト値を割り当てます。 : を接頭辞として付けると、割り当てを実行しつつ、その値を破棄します。

: "${DATA_DIR:=/var/data}"
: "${RETRIES:=3}"

これらの行の後、DATA_DIRRETRIES は、まだ設定されていない場合にデフォルト値を保持します。

ファイルの切り詰めとテスト

: の後のリダイレクトは、null-command が出力を生成しないため、ファイルをゼロの長さに切り詰めます。

: > error.log # error.log を切り詰める
: > error.log > access.log # 両方のファイルを切り詰める

同様に、リダイレクトと : だけを含むサブシェルを使用して、読み取り可能か、あるいは書き込み可能かをテストできます。

( : < dataset.json ) && echo YES # dataset.json は読み取り可能か?
( : >> result.json ) && echo YES # result.json は書き込み可能か?

コロンが必要なのは、サブシェルにコマンドが必要だからです。リダイレクト自体が実際の作業を行っています。

trap と条件文でのコロンの使用

trap 組み込み関数はコマンドを期待します。: は何もせずにその要件を満たします。

trap : INT
sleep 60 # sleep は割り込み可能なまま維持される

then ブランチにコマンドを含める必要がある if-else 文では、: がプレースホルダーとして機能できます。

if some-command; then : # コマンドが必要 else echo "command failed" fi

よくある質問(記事より)

なぜ null-command が必要なのですか? コロンがなくても展開は行われるのでは?

パラメータ展開はそれだけで発生しますが、null-command または同様の構成がない場合、シェルは結果の文字列を実行すべきコマンドとして扱います。例えば:

% ${HELLO:=123} zsh: command not found: 123

: を接頭辞として付けると、展開を実行しつつ結果を破棄します:

% : ${HELLO:=123} % echo $HELLO 123

${VAR=${VAR:-default-value}} と書けるのに、なぜ null-command を使うのですか?

どちらも同じ効果を得られますが、コロン形式は変数を一度しか記述しないため、タイポの可能性を減らすことができます:

: "${DATA_DIR:=/var/data}" # DATA_DIR は一度だけ登場 DATA_DIR="${DATA_DRI:-/var/data}" # タイポの可能性あり (DATA_DRI)

コミュニティの反応 (HN コメント選抜)

  • @gpvos が Larry Wall の言語再設計の法則を引用:「誰もがコロンを欲しがる。Larry はコロンを手に入れた。」

  • @amiga386 が if some-command; then : else echo "command failed" fi というイディオムに注目し、同等の if! some-command; then echo "command failed" fi も POSIX 標準であると指摘。

  • @rgrau は、:? テストをコマンドの引数に直接インライン化すること(例: echo "${1:? first param required}")を提案し、ループ本体にコロンを使用した while ループのパターンについても説明。

  • @fphilipe は、エディタを開かずに auto-squash を伴うインタラクティブな rebase を行うために、Git の EDITOR としてコロンを使用している。

  • @archargelod は、読みやすい if 文を : "${1:?...}" に置き換えることは可読性を損なうと主張し、代替案として [ -z "$1" ] && { echo "missing argument, aborting." 1>&2; exit 1 } を提示。

  • @kevincox は、:? 形式は必須の環境変数を確認するのに便利だと感じたが、他の多くの例については専用のコマンドを使ったほうが明確であると考えた。

  • @kazinator は、サブシェルテスト ( : < file ) && echo YES は不要であると批判。リダイレクトだけで読み取り/書き込みのテストは可能であり、: >> file でゼロ長ファイルを作成することは望ましくない場合があると指摘。

  • @olexsmir は、個人のユースケースとして、dotfiles のブートストラップスクリプトで : "${DOTFILES_PATH:=$HOME/.dotfiles}" を使用して設定環境変数のデフォルト値を設定していることを共有。

  • @luciana1u は、コロンは何もしないため、「LLM がオーバーエンジニアリングできない唯一の bash コマンド」であると述べた。

  • @m2f2 は、getopts スタイルのループでコロンを使用していることを説明:while : ; do case "$1" in... esac done

  • @jeffrallen は著者に感謝しつつも、自分は読み手に対してコード自体が説明してくれるようなスタイルを好むため、このイディオムは使わないと述べた。

  • @lucideer は、これらの例は単に読みやすい複数行のコードをワンライナーに変えているだけであり、スクリプトには不要なものだと感じた。

  • @normie3000 は、if! x; then something; fi よりも if x then :; else something; fi を好んだ。

  • @PunchyHamster は、このようなワンライナーを導入するプルリクエストは拒否すると言い、より長いスクリプトは Python や Perl で書き直すべきだと主張。

  • @garethrowlands は、このようなトリックが必要な理由は、シェル構文の古くからの文字列置換の性質にあるとした。

  • @ocd は、忘れてしまったユーティリティを素早く grep するために、: alias f3probe のような「何もしないエイリアス」を作成するためにコロンを使用している。

  • @dominiwe は、シェルスクリプトを同時に有効な YAML ファイルにするためにコロンを必要とした。

  • @del は、コロンが初期の Unix シェルにおける数少ない特殊なコードパスの一つであったのではないかと疑問を呈した。

  • @greatgib は、この構文を最初に見たのは Claude が生成したコードの中であり、その後にこの記事で見つけたと言った。

  • @search_facility は、クロスプラットフォームの安定性のために bash から純粋な Python スクリプトに切り替え、「bash zoo」を放棄した。

Sources