15
13

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

POSIX準拠のシェルスクリプトは何もしない時に何もしない「 : 」 コマンドを省略してはいけない

Last updated at Posted at 2021-08-24

はじめに

: コマンドは何もしないコマンドです。何もしないコマンドですが役目があります。例えば POSIX 準拠のシェルスクリプトでは、条件分岐や関数などで何もしない時でもコマンドを省略することはできません。: コマンドはこのようなときに使用します。この記事では : コマンドを省略できないという話と、おまけで他にどういう時に : コマンドを使うのか?という話をまとめてみました。

POSIX での : の説明

まず POSIX では : についてどのように書いてあるかを引用します。何もしないコマンドなので説明も少ないです。

NAME

colon - null utility

SYNOPSIS
: [argument...]

DESCRIPTION

This utility shall only expand command arguments. It is used when a command is needed, as in the then condition of an if command, but nothing is to be done by the command.

訳 このユーティリティはコマンド引数の展開のみを行います。このコマンドは if コマンドの then のようにコマンドが必要だが何もしないという場合に使われます。

then のようにコマンドが必要」

他の言語を使っている人がやりがちですが以下のようなコードは「POSIX 準拠のシェルスクリプトでは」エラーになります。理由は then の中にコマンドがないからです。コマンドは実行可能なものなので、コメントはコマンドに含まれません。POSIX では then 中に何かしらのコマンドを書くことが要求されています。

if [ -e "$file" ]; then # ファイルが有るか?
  # ファイルがある時は何もしない
else
  echo "ファイルがありません" >&2
  exit 1
fi

何もしないのであれば以下のように : コマンドを入れれば問題なく動きます。: 以外のコマンドでも構わないので、例えば変数代入や関数定義でも構いません。

if [ -e "$file" ]; then # ファイルが有るか?
  : # ファイルがある時は何もしない
else
  echo "ファイルがありません" >&2
  exit 1
fi

何もしない関数などでも : は必要

以下のコードでも POSIX 準拠シェルではエラーになります。

foo() {
  # 何もしない関数
}

同じように何もしないのであれば : を書く必要があります。

foo() {
  :
}

その他 { }( ) でも何もしない時に : が必要です(こんなコードを書くことはまず無いと思いますが)。

{
  :
}

{ :; } # 補足 { } は予約語扱いなので : の後に ; が必要
( : )

: が 省略できるシェル できないシェル

上記で「POSIX 準拠のシェルスクリプトでは」とわざわざ太字にしたのは POSIX に(完全に)準拠してないシェルでは、エラーにならない(省略できる)場合があるからです。この事に気づいていないと手元のマシンでは動いたのにサーバーでは動かないなどということが発生したりします。

: を書かなくてもエラーにならないシェルは次のシェルです。

  • zsh
  • yash (POSIX 準拠モードにしてない場合)
  • FreeBSD sh (ash 系)
  • NetBSD sh (ash 系)

なぜエラーにならないシェルがあるのか?というと最終的にはシェル開発者がそうすべきだと判断したということになるのでしょうが歴史的経緯もあるでしょう。

dash, bash, ksh, mksh は省略できない

POSIXの規定では : を省略することはできません。: を省略できないシェルは、dash、bash、ksh88、ksh93、mksh、OpenBSD sh、BusyBox ash 等です。POSIX シェルではありませんが古い Bourne シェルでも省略することはできません。

zsh は csh ユーザーのための仕様

zsh は当初は csh ユーザーにとって使いやすい POSIX シェルを目指して開発されました。そして csh では then の中に何も含める必要はありません。

if (-e "$file") then
  # 何もしない
else
  echo "ファイルがありません"
  exit 1
endif

つまり zsh では csh ユーザーのために意図的に設計された仕様であると言えるでしょう。

yash は POSIX モードでは準拠

yash は個人的印象では zsh に近い設計思想を持っていると考えています。そのため zsh と同じように then の中に何も含めなくて良くしたのではないかと推測しています。ただし POSIX 準拠を強く意識しているシェルでもあるため POSIX モードではちゃんとエラーになります。つまり yash の実装もバグではなく意図的な仕様です。

FreeBSD sh と NetBSD sh はバグ

FreeBSD sh と NetBSD sh はおそらく(オリジナルの ash の)バグでしょう。これらの元になったシェルの ash は、もともと Bourne シェルのクローンとして開発されたシェルです。しかし Bourne シェルでも何もしない時にコマンドを省略することはできないわけですから、正しく実装されてなかったと言えます。FreeBSD と NetBSD ではおそらく昔の ash の不具合をそのまま引き継いでおり、互換性を保つためにあえてそのままにしているのだと思われます。

ちなみにこれらのシェルと同じ動作をするのが Debian 3.0 版の ash です。これは dash (Debian ash) に名前が変わる前のシェルで dpk -l の説明には 「NetBSD /bin/sh」と書かれています。もちろん現在の dash では POSIX に準拠したとおりの動きに修正されています。ash 時代に修正されたのか dash 時代に修正されたのかはわかりませんが ash の歴史あたりを詳しく調べればなにかわかるかもしれません。

「コマンド引数の展開のみを行います」

さてこの記事では余談に近い話となりますが、: コマンドのもう一つの使い方は POSIX でも説明されている引数の(変数)展開を利用することです。これも例を見たほうが早いですね。

# DEFAULT 変数が空もしくは未定義なら、DEFAULT 変数に abc を代入する
: "${DEFAULT:=abc}"

# FILE 変数が空もしくは未定義なら「ファイルが指定されていません」とエラーを出して終了する
: "${FILE:?ファイルが指定されていません}"

: は何もしないコマンドですが、変数展開と組み合わせることで変数の初期設定や未設定チェックをする時に使うことができます。

おまけ

: で空ファイルを作る

これは POSIX の : コマンドの EXAMPLES に書いてあるのですが、空ファイルを作る時に使うことができます。touch コマンドと違ってファイルの中身を空にするので注意してください。

: > /tmp/dummy.txt

既存のファイルの中身を空にしたくない場合には、<> を使うことができます。ただし touch コマンドと違って更新日時を変更することはありません。

: <> /tmp/dummy.txt

この : を省略して > /tmp/dummy.txt だけでも空ファイルが作れると書いてある記事を見かけますが zsh では想定通りに動作しないので気をつけてください。zsh では : コマンドを省略した場合に環境変数 NULLCMD で指定されたコマンド(デフォルトでは cat)コマンドが実行されます。そのため(入力が与えられてない場合に)空ファイルが作られるのではなく、入力を待ち続けることになります。そして入力があればその内容がファイルに出力されます。他のシェルと同じ動作にするには NULLCMD: を設定するか、setopt SH_NULLCMD を実行します。ちなみにコマンドを省略したときの動きですが、おそらく POSIX では規定されてないと思います(というか zsh で動きが違うために標準化できない?)。

: で終了ステータスを取り扱う

:set -e の状態で終了ステータスを取り扱う時に時々必要になります。

set -e

# set -e でスクリプトを中断させることなく終了ステータスを判断したい場合
somecmd && : 
case $? in
  0) ... ;;
  1) ... ;;
  2) ... ;;
esac

# こんな方法でもできますが・・・
ret=0
somecmd || ret=$?
case $ret in
  ...
esac

# foo 関数の中の somecmd がエラーを返しても無視したい場合
foo() {
  somecmd || : 
}
foo

: は本当に何もしないコマンド?

雑談めいた話なので興味がない人は読まなくて良いです。ほとんどのシェルでは : は何もしないコマンドです。何もしないので最も高速に実行できるコマンドです。しかし zsh では意外と遅いコマンドです。これは推測ですが zsh では : に対して特別な処理を行っておらずシェルにビルトインされた : というコマンドを本当に呼び出しているのではないかと思っています。その理由は : コマンドは zsh でシェル関数で上書きできるからです。

:() {
  echo colon
}
: # => colon

これができるようにするためは : コマンドを特別扱いせずに他のコマンドと同じように処理するのが最も簡単な実装でしょう。よって zsh では : コマンドのパフォーマンスが落ちていると考えています。

ということで、何もしない処理をもっとも高速かつ他のシェルでも動くようにするには、実は変数への代入や関数定義の方が良かったりします。

if [ -e "$file" ]; then
  DUMMY=''
else
  dummy() { :; }
fi

関数定義をする方法であれば set -x で実行ログを出力した時に、何もしない時に本当に何も出力されないというメリットがあったりするのですが、実際こんな話を元に : の代わりに関数定義しても意図が伝わらないので普通に : を使うべきです。

さいごに

macOS のデフォルトのインタラクティブシェルが zsh に変わって初めて使うシェルが zsh というシェルスクリプト初心者が増えてきたと思うので、勘違いを減らすために念を押して言っておきますが POSIX 準拠では then の中を省略できません。ようするに : を省略した時にエラーになるからと言って bash や dash 側の不具合ではないということです。エラーになる方が POSIX に準拠した動きです。これは POSIX の仕様(Shell & Utilities 以下)をちゃんと読んでいればすぐに分かる事です。完全に POSIX に準拠しているシェルは存在しないので、シェルによって動作に違いがある場合はどれが POSIX に準拠した動きであるのかを(英語ですが)ちゃんと原典で調べる癖をつけましょう。

と、記事を書きましたが : の使い道については「何もしない組み込みコマンド ":" (コロン)の使い道」の方が詳しくて他の用途も書かれていますね。この記事の主題は POSIX 準拠のシェルスクリプトの話ということで。

15
13
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
15
13

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?