何もしない組み込みコマンド ":" (コロン)の使い道

  • 551
    いいね
  • 16
    コメント

Bash でシェルスクリプトを勉強していくと出会うのが : (コロン)という名前の組み込みコマンド。このコマンドは何もしないコマンドです。

こんなコマンドの存在は不思議だなと思う反面、C言語にも void という型があったり(関数のような形で存在するのは JavaScript とかですね)、LaTeX にも \relax があったり、何もしない命令というものは機械語の NOP からある普通のものです。

この Bash の : の使い道についてまとめてみました。

何か書かなければならないところに仮置きする

例えば「ここに制御構造を置くんだけど、この節に入るものは後で書くんだけどな〜」といった場合、制御構造の節の中に何も書かないと Bash は構文エラーとなります。

#!/bin/bash

arg="$1"

if [ -z "$arg" ] ; then
    echo "デフォルトモード開始"
else
    # カスタムモード:あとで書く
fi

これは else 節に何もないことでエラーとなってしまいます。# はコメントであって Bash の文を構成する要素ではないので else 節には何もないということになってしまいます。

$ ./sample.sh 
./aaa.sh: line 9: syntax error near unexpected token `fi'
./aaa.sh: line 9: `fi'

そういったとき、else 節は入れておきたいんだけど中身では今は何もしたくない場合に : を入れておくと良かったりします。

#!/bin/bash

arg="$1"

if [ -z "$arg" ] ; then
    echo "デフォルトモード開始"
else
    # カスタムモード:あとで書く
    :
fi

ファイルを truncate する

(本記事のコメントで教えていただいたものです)

既に内容を持っているファイルを truncate する(0バイトに切り詰める)場合に何も出力しないコマンド : とファイルリダイレクト > を組み合わせた :> が使われます。

$ :> debug.log

これは

$ echo -n "" > debug.log

と大体同じ意味ですが、:> という記号が一つの命令のように見えることなどが好まれるようです。また、rmtouch の2ステップと比較しても、処理がアトミックに書けるという特徴があります。

  • 2016/02/15 追記:当初 rm して touch するのとほぼ同じことと書いていましたが、ファイルがさす inode が変わる影響を考えると必ずしも適切では無いので echo -n "" > debug.log に変えてあります。
  • 2016/02/17 追記:ファイルリダイレクトに対する出力コマンドが完全に無い状態 > debug.log でも :> debug.logecho -n "" > debug.log と同様の効果があるとコメントで教えていただきました。
  • 2016/05/27 追記:通常の Linux などには最初からインストールされている coreutils には truncate というそのままの名前のコマンドがあります。

ファイルが存在し続けていることを前提としているファイル監視プログラムがある場合に truncate の手法が必須となってくる場合もあります。

変数参照の副作用を利用する

Bash の変数は $value もしくは ${value} というオーソドックスな参照方法以外にもいくつかの書き方があります。

書き方 定義 Perlで書くと
${parameter:-word} $parameter が未定義か空の場合、word を展開 $parameter or "word"
${parameter:=word} $parameter が未定義か空の場合、word を代入してそれを展開 $parameter or ($parameter = "word")
${parameter:?word} $parameter が未定義か空の場合、word を標準エラー出力に出力して終了 $parameter or die "word";
${parameter:+word} $parameter が未定義か空の場合以外、word を展開 $parameter and "word"
  • Markdown のテーブル表記に || が書けなかったので or にしてありますが、演算子の優先順序の都合によって読み替えて下さい
  • Perl の場合、未定義 (undef) か空以外にも 0"0" が偽として扱われますが、上記ではその部分を考慮していないことをご注意下さい

上記のうち、${parameter:=word}${parameter:?word} は、参照以外に副作用があり、この副作用だけを使いたい場合に : が使われることがあります。

# こう書くよりも
if [ -z "$parameter" ] ; then
    parameter=word
fi

# こう書くよりも
parameter=${parameter:-word}

# こう書いたほうが簡潔
: ${parameter:=word}

他者が書いた既存の Bash スクリプトを拡張する際など、未定義変数の参照をエラーにしようと set -u もしくは set -o nounset が入っていないからといって、それを追加することで大きな改修が必要になる場合があります。例えば引数がいくつ与えられるか分からない場面で $1 などと書いている場合、引数がない場合に $1 を参照した時点で set -o nounset 下ではエラーになってしまいます。

逐一、変数が未定義かどうか確認するのは骨が折れますが、上記 ${parameter:?word} で多少楽をすることができます。

# これでもいいけれど
test -z "$parameter" && exit 1

# こっちのほうがさらに簡潔(見た目わかりづらいけれど)
: ${parameter:?}

必ず正常終了するコマンドとして

: コマンドは何もしないですが どんなときも正しく終了する つまり、必ず終了コード 0 で終了するという特徴があります。

$ : ; echo $?
0

この特徴を利用するケースがあります。

終了コードが 0 である必要がある場合

スクリプト中に set -e もしくは set -o errexit がある場合、終了コードが 1 以上の番号のコマンドがあるとそこで終了してしまいます。

多くの場合は 1 以上の番号の終了コードはエラーコードであり、すなわちエラーですが、与えられた 2 つのファイルに相違がある場合の diff コマンドの終了ステータス 1 であるとか、エラーとしては扱いたくない場合があります。

set -o errexit

# 差分を見せる
echo "show diff from config.orig to config"
diff -u config.orig config || :

演算子 || で結ばれた 2 つのコマンドは、左側のコマンドの終了コードが 1 以上の場合、右側が実行されてその終了コードが全体の終了コードとなります(左側のコマンドの終了コードが 0 の場合は右側のコマンドは実行されません)。

この場合 diff -u config.orig config に差分があって終了コード 1 で標準出力に差分を表示しても || で右側のコマンド : が実行されて、その終了コードは必ず 0 なので、set -o errexit でも以降のコマンド実行へ移ることができます。

制御構造で無限ループを作るための条件として

C系の言語で無限ループを作るために良く知られたイディオムがあります。

while(1) {
   // 無限ループ
}

Bash でもこれにならった書き方として : が使われることがあります。

while : ; do
    # 無限ループ
done

もっとも、このような真値を想定させるような使い方の場合には true というコマンドが用意されていて、そちらを使うほうが可読性が高いです。かくいう私も以前からこのような場合は true を使っていました。

while true ; do
    # 無限ループ
done

最近の Bash であれば true は組み込みコマンドですが、coreutils も外部コマンドとして true コマンドを用意しています(/bin 以下にあるか /usr/bin 以下にあるかなどは OS やディストリビューションによって差異があります)。

$ type true
true is a shell builtin
$ ls -l /usr/bin/true
-rwxr-xr-x  1 root  wheel  13728  9 10  2014 /usr/bin/true*

中間的なコメントとして

: はどんな引数を与えることもできて、かつどんな引数を与えられたとしても何もしません。

というわけで # とは違うコメント的なトークンとして使うことができます。

echo "backup start"
# いまは何もしない
: rsync -avzC $DATA_DIR/ $BACKUP_USER@$BACKUP_HOST:$BACKUP_DIR/

この用途では普通に # を使っても同じことです。

また : は通常のコマンドなので、# とは違い完全なコメント行を作ることはできません。つまり、コマンドをつなぐ同一行にある |> といったトークンを「コメント化」することはできません。

# 以下は `:` コマンドの出力を bonunced.log にリダイレクトする。つまり空の bounced.log ができるだけ。
: ssh $LOG_HOST "grep status=bounced /var/log/maillog" > bounced.log

上記の結果を見ると「あえて # を使わず : でコメントを取る理由なんて無いんじゃない?」とも思えますが、半コメント的なところが意外な場面で有効だったりします。

一行複数コマンド中の特定のコマンドだけのコメントアウト

シェルスクリプトというよりも、ターミナルでセミコロン区切りの長い一行を書いたときに、特定のコマンドだけ今は実行したくないんだけど、全部消すのも面倒…といった場合の範囲コメントとして使うことができます.

$ for dir in samba_* ; do cd $dir ; echo "backup start" ; make backup ; echo "finish at $(date)" >> log ; cd - ; done

ちょっと make backup だけは今は実行させたくないという場合、

$ for dir in samba_* ; do cd $dir ; echo "backup start" ; : make backup ; echo "finish at $(date)" >> log ; cd - ; done

と書くとよいです。もっとも make なら -n オプションがあるというツッコミもありそうですが。

ログに書くところはやめておきたいという場合は

$ for dir in samba_* ; do cd $dir ; echo "backup start" ; make backup ; : echo "finish at $(date)" >> log ; cd - ; done

となります。前述で > みたいなコマンドを区切るものは…という注意もありましたが、: は一切の出力をしないので、この場合は何も追記 >> しないのです。たまたま想定通りに行く事例です。

デバッグモード (xtrace モード) での出力メモとして

Bash にはデバッグ用に xtrace モードが用意されています。xtrace モードを有効にするには以下のようにします。

  • bash -x スクリプト として起動する
  • スクリプト中で set -x もしくは set -o xtrace を実行する

xtrace モードを有効にした場合、どんなコマンドが実行されたかがすべて出力されます。どこの条件分岐を通ってどんな値を変数に入れたかなどを簡単に調べることができます。

しかし、まだ条件分岐中の処理を実行したくない場合など、どの条件分岐に入ったかまでを見たい場合に xtrace モードで出力されるメモとして : を使うと嬉しいことがあります。

#!/bin/bash

arg="$1"

if [ -z "$arg" ] ; then
    : empty
    # まだ転送はしない
    # rsync -avzC $BACKUP_DIR/ $REMOTE_HOST:$REMOTE_PATH/$
else
    : given
    # rsync -avzC $BACKUP_DIR/ $REMOTE_HOST:$REMOTE_PATH/$arg/
fi
$ bash -x ./aaa.sh foo
+ arg=foo
+ '[' -z foo ']'
+ : given

# は命令や文といった類ではないので xtrace では出力してくれませんが、: は通常の意味でのコマンドなので出力してくれるのです。

上記のサンプルは簡単すぎてありがたみも感じられないかもしれませんし、echo でも大して変わりないかもしれませんが、ある程度以上大きな Bash スクリプトで xtrace への表示目的に : を使うと便利な場面もあります。

自分なりのデバッグ出力の切り替えとして

Bash の変数は、コマンドラインに置かれた後は変数展開をした後でコマンド文字列として解釈されるので、以下のように実行コマンド名を変数に入れておくこともできます。

#!/bin/bash

DEBUG="echo DEBUG:"

$DEBUG "start process"

とはいえ、デバッグモードではない時に、上記コマンドでデバッグ出力を抑制したい場合、DEBUG 変数を # にしてもうまくいきません。

#!/bin/bash

DEBUG="#"

$DEBUG "start process"
$ ./foo.sh 
./foo.sh: line 5: #: command not found

これは、Bash のコマンドライン解釈が、# コメント処理 → 変数展開 → コマンドライン実行 の流れを経ているからだと私は理解しています。

foobar.sh
#!/bin/bash

COMMAND="echo foo # bar"

$COMMAND
echo foo # bar
$ foobar.sh
foo # bar
foo

上記 $DEBUG が何もしないコマンドに置き換われば所望の動作をするわけで、ここでも : が活用できます。

#!/bin/bash

DEBUG=":"

$DEBUG "start process"

コマンド履歴にメモをする

例えばコマンドラインで長いコマンドを打って Enter を押す寸前に、別の作業をしなければならなくなった場合、どうしますか?

Bash のキーボード操作を少し知っている方であれば、Ctrl+u で現在の行(カーソルの左側)をキル(カット)して、別の作業が終わった後で Ctrl+y をしてヤンク(ペースト)をするかもしれません。ただ、この場合は別の作業が長引いた場合には Ctrl+u をもう一度押してしまって結果を押し出してしまう可能性もあります。カーソルを動かして OS のクリップボードに入れる、GNU Screen や tmux などのスクリーンマルチプレクサのコピー機能を使うなどありますが、どれも若干の面倒さがあります。

キルやヤンクにまつわる部分など、Bash の入力ライブラリである readline の知識と小粋な設定があれば巧なことができるとは思いますが、手軽にやるとすればここでも : が役に立ちます。要するに :を使って Bash のコマンド履歴にメモを残す のです。

例えば、以下のように Git にコミットをするぞというところで Enter を押そうとした時に、README.md に変更内容を記録するのを忘れたとしましょう。

$ git commit -m "implement file sync mode" bin/archive.sh

カーソルが末端にあるとして Ctrl+u を押してカットするのもいいですが、ここでは Ctrl+a でカーソルを先頭に持って行ったあとで : (コロンとスペース) を入力して Enter を押します。こうすることで、以前の git コマンド全体が : コマンドの引数となります。

$ : git commit -m "implement file sync mode" bin/archive.sh

この状態で Enter を押しましょう。これで何が起こるかというと、今打ったコマンドがコマンド履歴の中に入るのです。

$ history | tail -n 2
    954  : git commit -m "implement file sync mode" bin/archive.sh
    955  history | tail -n 2

この状態で別の作業をします。

$ vim README.md

そして先ほどの作業に戻るときは、Bash のコマンド履歴をたどるインクリメンタルサーチ Ctrl+r で git などと打つことによって先ほどの : git ... をたぐり寄せることができます。

(reverse-i-search)`git': : git commit -m "implement file sync mode" bin/archive.sh

先ほど打った : git ... のコマンドがインクリメンタルサーチで引っかかったら、そのまま Ctrl+a を押してカーソルを先頭に移動させつつインクリメンタルサーチを終わらせます。カーソルが冒頭に来ているので、先ほど打った : (コロン・スペース)分の2文字を削除して(Ctrl+d を2回打つと良いです)そのまま Enter を押すことで、先ほど保留したコマンドを実行することができます。

これの応用で、よく history コマンドでコマンド履歴を見る習慣のある人は、これから実行する作業の前に : ウェブサーバのバックアップ作業 などと打つことによって、純粋なメモをコマンド履歴に「書く」ことができます。

メモのためには echo などの別のコマンドなどを使うことができますが、短くて素早く打つことができることや他意がないことなどで : (コロン・スペース)の2文字が優れているといえます。なお # によるコメントはコマンドラインでも打つことができて希望通り何も起こらないですが、# はコマンドではないのでコマンド履歴には入りません(入る環境もあるようですが、私の手元では詳しく追えていません)。

ちなみに、コマンド履歴に純粋にメモを残すには組み込みコマンド history のオプション -s が使えるそうです。

-s     Store the args in the history list  as  a  single  entry.
       The  last  command  in the history list is removed before
       the args are added.

man bashhistory コマンドの解説部分より)

https://twitter.com/hirose31/status/699098413917286400

目立たないことを利用・悪用する

: という文字・フォントは多くの場合は目立たないということを利用もしくは悪用した技もあります。

Fork爆弾として有名な13文字もその一つでしょう(これは危険なコマンドなので絶対に実行してはいけません)。

とはいえ上記Fork爆弾の13文字から学べることもあって、組み込みコマンド : は定義を上書きできる ということです。実際、cd を上書きして pushd popd のようなヒストリ機能をつけたりといったことはよく行われており、cd と同様に組み込みコマンドである : も上書き定義できるというわけです。

これを利用すれば「ほぼすべての場合において何もしないコメント的に活用できるコマンド」をプロジェクト固有の使い方でスクリプト中に書いておいて、ある特殊な場合において関数定義を流し込んで有効化させるということもできます。

function : {
    logger -t colonlog "$[@]"
}

関数定義を削除したい場合は unset -f、関数定義を無視して本来の組み込みコマンドの機能を利用する場合には builtin を頭につけて呼び出すとよいです。

ヒアドキュメントと組み合わせた擬似的な複数行コメントとして

Bash には複数行コメントはありませんが、何もしないコマンド : とヒアドキュメントを組み合わせることで擬似的な複数行コメントを実現することができます。

: <<'COMMENT'
# ここはまだまだ未完成!
DUMMY_LOCK_FILE_TARGET=/tmp/dummy

trap "rm -f $DUMMY_LOCK_FILE_TARGET" EXIT

if ln -s $DUMMY_LOCK_FILE_TARGET $LOCK_FILE ; then
    curl -u "foo:bar" http://example.jp/api/v1/login

else
    echo "ロックされています。あとで試して下さい。"
fi
COMMENT

: はどんな引数を与えても何しないのと同様に、どんな標準入力を与えても何もしないので、こういう応用ができます。

: が組み込みコマンドでコストがほとんどかからないとはいえ、見た目コストがかかりそうな印象があることや、それほど一般的な技でもないので、多くの人が管理するスクリプトでは通常の # によるコメントに置き換えたりする方がいいでしょう。

もっとも、必ず立ちはだかる exit の向こうにある : は実行されないわけで、これを利用して埋め込みドキュメントを作るという手法もあります。

#!/bin/bash

arg="$1"

if [ $# = 0 ] || [ "__$arg" = "__-h" ] || [ "__$arg" = "__--help" ] ; then
    perldoc $0
    exit
fi

# いろいろな処理

exit

# ここから先は実行されない

: <<'POD'

=pod

=head1 NAME

anywhere.sh - go to anywhere

=head1 SYNOPSIS

  anywhere.sh PLACE

=cut

POD

上記の例では Perl のドキュメント形式である POD を、実行されることがない : の標準入力となるヒアドキュメントに書いておき、スクリプトの引数に -h などが与えられたら自分自身を perldoc コマンドで処理して整形されてた POD を見せるものです。perldoc コマンドは POD をまさに man コマンドのように見せてくれるので、手軽なマークアップ形式の一つです。

このヒアドキュメント形式のコメント化には注意点があります。

通常の <<END 形式のヒアドキュメント内では $(command) 展開が働くので、コメントアウトしようとしているコード群の中に $(command) があるとそれが実行されてしまいます。その command の実行が副作用を伴う場合には、目立たないバグの温床となる可能性もあるでしょう(副作用が無くともそれなりに実行コストがかかります)。そのため、上記では $(command) の実行と展開を抑止するため <<'END' 形式のヒアドキュメントを使用しています。

おまけ: true: の違いについて

上記で常時終了コードが 0 である true コマンドを挙げましてたが、true コマンドも終了コードが 0 ということ以外の動作が無いことから、実はシェル組み込み関数版の true: と同等なのではないかという疑問も浮かびます。

この記事のコメントに返信しながらそんなことを考えていたのですが、実際の実装は全く同等であることを調査してくださった方がいらっしゃいました。

2016年2月現在の HEAD での "colon.def" の実装部分 を見ると、確かにそうでした。

$BUILTIN :
$DOCNAME colon
$FUNCTION colon_builtin
$SHORT_DOC :
Null command.

No effect; the command does nothing.

Exit Status:
Always succeeds.
$END

$BUILTIN true
$FUNCTION colon_builtin
$SHORT_DOC true
Return a successful result.

Exit Status:
Always succeeds.
$END

実際の colon_builtin 関数はこんな感じでした。

int
colon_builtin (ignore)
     char *ignore;
{
  return (0);
}

少なくとも終了コードがいつも 0 であることを利用したい場合は : よりも true を使うべきでしょう。

while true ; do
    date
    sleep 1
    clear
done

Wikipedia 日本語版にある true の記事 も参考になります。

付記:いたずらに難読化させてはいけない

もともとこの記事は、業務コードで xtrace などを使うときに自分自信が無意識で : を書いていることに気づいて、そんなコードを読んで分からない方への助けになればいいなと思って書いたものでした。おかげさまで、当初全く予想していなかった反響をいただきましたが、はてブのコメントでは可読性の欠如であったり行き過ぎた「シェル芸」への否定的な意見をいただきました。

私自身も チームでプログラミングをしている際は、いたずらに難読なコードを書くべきではない と思っています。この記事のタイトルが「使い道」であって「活用」ではないのも、 積極的に使っていくべきだと喚起しているわけではない ことのあらわれだと思っていただけると幸いです。

: の定義を上書きすることについても否定的な意見をいくつかいただきました。外部から上書きされていることを考えて builtin : と書かないといけないのかという意見もありましたが、組み込み関数が上書き可能であることは事実ですし、それを上書きして不利益を被るのは一般的にはそのシェルの使用者本人のみです(他所から : を上書きされたことに対して不利益を被ったのであれば、それはそもそもただのマルウェアです)。記事中にもありますが、同じく組み込みコマンドである cd を上書きして拡張する手法はそこそこ行われており、組み込みコマンドを上書きすることは一般的には忌避されるものですが、完全なタブーというわけでもないでしょう。

どんなプログラミング言語にも一見馬鹿げたとしか思えない手法があるものですが、ときに頭の良い人がそれを利用してブレイクスルーを起こしたりすることもあり、完全な悪とみなすのも早計かもしれません。ただ、多くの人にとって馬鹿げたことができてしまう手法があるということを頭の片隅に置いておくことは、そんな面倒な事柄から避けるための知識にもなるのではないでしょうか。

その他にもこんな使い方あるよというアイデアをお持ちの方がいらっしゃいましたら、コメントなどでフォローお待ちしています。