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
と大体同じ意味ですが、:>
という記号が一つの命令のように見えることなどが好まれるようです。また、rm
と touch
の2ステップと比較しても、処理がアトミックに書けるという特徴があります。
- 2016/02/15 追記:当初
rm
してtouch
するのとほぼ同じことと書いていましたが、ファイルがさす inode が変わる影響を考えると必ずしも適切では無いのでecho -n "" > debug.log
に変えてあります。 - 2016/02/17 追記:ファイルリダイレクトに対する出力コマンドが完全に無い状態
> debug.log
でも:> debug.log
やecho -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 のコマンドライン解釈が、#
コメント処理 → 変数展開 → コマンドライン実行 の流れを経ているからだと私は理解しています。
#!/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 bash
の history
コマンドの解説部分より)
timeout
コマンドでプロセスグループIDを変更するイディオムとして
こちらの記事のコメント欄で勉強した内容です。
最近の coreutils には、コマンド実行時間を制限する timeout
コマンドが同梱されています。
$ timeout 59 every-minutes-cronjob.sh
使い方は多岐に及びますが、5つの *
で表される「毎分cron」等で実行時間を59秒に制限しておくと、多重実行を未然に防ぐ事ができるといった効用があります(もちろん、処理途中で強制終了することや終了にまつわるシグナルを投げられることは要考察)。
例えば、5分間で流れたログの行数を表示するために、以下のような timeout
コマンドを書いてみたとしましょう。
$ timeout 300 tail -f /var/log/messages | wc -l
しかし、上記コマンドは300秒経ったら行数を表示すること無く、Terminated と表示されて終了します。
上記記事によると
-
timeout
コマンドがプロセスグループのリーダーとなり、そのPIDがPGID(プロセスグループID)として採用される -
timeout
コマンドの右側にパイプでつながれたコマンド(単数または複数)もtimeout
コマンドと同じPGIDとなる -
timeout
コマンドが制限時間を超過して終了する際にtimeout
コマンドと同じPGIDを持つコマンドも同様に終了させられる
という処理の流れとなるため、上記コマンド例の場合は300秒経過した途端、 tail
からの入力がクローズされフラッシュされる前に wc
コマンドも終了させられてしまい、結果的に行数を表示することができないということになります。
これを回避する方法として timeout
コマンドをプロセスグループのリーダーにしない方法があります。そのためには timeout
コマンドをパイプでつながれたコマンド群の一番左側に置かれないようにします。
$ : | timeout 300 tail -f /var/log/messages | wc -l
timeout
に代わる別のプロセスをプロセスグループのリーダーに立てるのですが、そのコマンドは何でも良いわけで、何もしないコマンド :
が選ばれるというわけです。
tail -f FILE
形式の tail
コマンドは標準入力が開かれても動作を変えないので、何も入力されることがないにしても左側にパイプがあっても問題ないようです。
$ timeout 10 tail -f /var/log/messages | sleep 10 | sleep 10 | sh -c "pstree -apgnh $$"
bash,20527,20527
├─timeout,20745,20745 10 tail -f /var/log/messages
│ └─tail,20749,20745 -f /var/log/messages
├─sleep,20746,20745 10
├─sleep,20747,20745 10
└─sh,20748,20745 -c pstree -apgnh 20527
└─pstree,20750,20745 -apgnh 20527
上記では 20745 がプロセスグループのリーダーである timeout
の PID かつ PGID ですが、パイプの一番左側を :
にすると
$ : | timeout 10 tail -f /var/log/messages | sleep 10 | sleep 10 | sh -c "pstree -apgnh $$"
bash,20527,20527
├─timeout,20756,20756 10 tail -f /var/log/messages
│ └─tail,20761,20756 -f /var/log/messages
├─sleep,20757,20755 10
├─sleep,20758,20755 10
└─sh,20759,20755 -c pstree -apgnh 20527
└─pstree,20760,20755 -apgnh 20527
となります。 :
は即座に終了するはずなので pstree
コマンドが実行されるときには存在しないですが、トップの bash,20527,20527
の直下に 20755 の PID/PGID で一瞬存在していたのでしょう。
上記のように :
がプロセスグループのリーダーになった時は、パイプのつなぎ方からそのプロセスグループの一員となるはずの timeout
ですが、PGID がそれとは違うもの(timeout
の PID と同じもの)になっているのは、timeout
特有の挙動なのでしょうか。不勉強な自分も timeout.c
を読んでみます。
目立たないことを利用・悪用する
:
という文字・フォントは多くの場合は目立たないということを利用もしくは悪用した技もあります。
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
を上書きして拡張する手法はそこそこ行われており、組み込みコマンドを上書きすることは一般的には忌避されるものですが、完全なタブーというわけでもないでしょう。
どんなプログラミング言語にも一見馬鹿げたとしか思えない手法があるものですが、ときに頭の良い人がそれを利用してブレイクスルーを起こしたりすることもあり、完全な悪とみなすのも早計かもしれません。ただ、多くの人にとって馬鹿げたことができてしまう手法があるということを頭の片隅に置いておくことは、そんな面倒な事柄から避けるための知識にもなるのではないでしょうか。
*
その他にもこんな使い方あるよというアイデアをお持ちの方がいらっしゃいましたら、コメントなどでフォローお待ちしています。