bashとシェル芸
シェル芸を嗜むシェル芸人でなくとも、Unix系OSのユーザであれば日々シェルのお世話になっているはず。
数あるUnix系シェルの中でも、最も普及していると言われるbashについて、あまり使われてなさそうな文法を紹介する。
前提
- 動作確認環境
- Ubuntu 16.04.3 LTS
- Linux kernel 4.4.0-97-generic x86_64
- GNU bash, version 4.3.48(1)-release (x86_64-pc-linux-gnu)
- 日本語版マニュアルは
JM Projectから引用
パイプラインの文法
パイプラインの文法全体の定義は以下の通り
パイプライン (pipeline)は、制御演算子 | または |& で区切った 1 つ以上のコマンドの並びです。
パイプラインのフォーマットを以下に示します:[time [-p]] [ ! ] command [ [|||&] command2 ... ]
command の標準出力は command2 の標準入力にパイプで接続されます。
つまり
$ seq 10 | wc -l
10
のようにすると、seq 10
の標準出力がwc -l
の標準入力に接続されるので、seq 10
の書き出した1〜10の10行がwc -l
に読み込まれ、wc -l
の書き出した10
がターミナルに表示される。
command |& command2
|& を使うと、command の標準エラー出力もパイプを通して command2 の標準入力に接続されます。
これは 2>&1 | の短縮形です。
この標準エラー出力の暗黙のリダイレクションは、 コマンドに指定された全てのリダイレクションの後に実行されます。
パイプラインの区切り文字に|
を使うと、左側のコマンドの標準出力のみが、右側のコマンドの標準入力に接続される。
例えば
$ seq 10 | dd | tail -n 5
0+1 records in
0+1 records out
21 bytes copied, 5.8859e-05 s, 357 kB/s
6
7
8
9
10
のようにすると、seq
は1〜10の10行を標準出力へ書き出し、dd
はその10行を標準入力から読み込んで、そのまま標準出力へ書き出し、tail -n 5
は標準入力から読み込んだ10行のうち、最後の5行を標準出力へ書き出す。
このとき、dd
は読み書きしたデータ量などの統計情報を標準エラー出力に書き出し、それはターミナルへそのまま表示される。
パイプラインの区切り文字に|&
を使うと、左側のコマンドの標準エラー出力も、右側のコマンドの標準入力に接続される。
例えば
$ seq 10 | dd |& tail -n 5
9
10
0+1 records in
0+1 records out
21 bytes copied, 0.000175366 s, 120 kB/s
のようにすると、dd
が標準エラー出力に書き出した統計情報もtail -n 5
の標準入力へ接続され、tail -n 5
は1〜10の10行と統計情報の両方を標準入力から読み込み、その全体のうち最後の5行を標準出力へ書き出す。
これは
$ seq 10 | dd 2>&1 | tail -n 5
9
10
0+1 records in
0+1 records out
21 bytes copied, 0.000734976 s, 28.6 kB/s
と等価である。
! command | command2
pipefail オプションが有効になっている場合を除き、 パイプラインの返却ステータスは最後のコマンドの終了ステータスになります。
パイプラインの前に、予約語である ! がある場合、 そのパイプラインの終了ステータスは上記の終了ステータスを 論理否定したものになります。
Unix系OSのプロセスは、exit(2)システムコール等によって終了ステータスを返却することができる。bashはコマンドを実行した際の終了ステータスを利用して、様々な制御を行うことができる。また、パイプラインや複合コマンドに対しても終了ステータスが定義され、同じように利用できる。
パイプラインの終了ステータスは、シェルオプションpipefailの設定によって変わるが、デフォルト設定(pipefail無効)では、パイプラインの最後(一番右側)のコマンドの終了ステータスが、パイプライン全体の終了ステータスとなる。
例えば
$ seq 5 | grep 7; echo $?
1
のようにすると、seq 5
は1〜5の5行を書き出し、grep 7
はその中から7
を検索するが、マッチする行がないので終了ステータス1
で終了する。パイプライン全体の終了ステータスも1
となる。echo $?
は直前に実行されたコマンドの終了ステータスを表示するため、ターミナルに1
が表示される。
ここで
$ ! seq 5 | grep 7; echo $?
0
とすると、パイプライン全体の終了ステータスが論理否定され、0
になる。
複合コマンドの文法
bashでは、複数のコマンドを組み合わせて実行する仕組みとして、パイプライン、リスト、複合コマンドといった文法が用意されている。
リスト (list) とは、1つ以上のパイプラインを演算子 ;, &, &&, || のいずれかで区切って並べたものです。
ここでいうパイプライン
とは
|で区切られた1つ以上のコマンドの並び
であるから、単純にコマンドを1つ書いたものもパイプラインの一種である。
いまここで
$ seq 5 | grep 3
3
$ echo hoge
hoge
という2つのパイプラインを考えると、これら2つのパイプラインをseq 5 | grep 3
,echo hoge
という順序で並べたリストを4種類のリスト演算子それぞれで作ることができる。
$ seq 5 | grep 3 & echo hoge
[1] 9266
hoge
3
[1]+ Done seq 5 | grep
$ seq 5 | grep 3 ; echo hoge
3
hoge
$ seq 5 | grep 3 && echo hoge
3
hoge
$ seq 5 | grep 3 || echo hoge
3
リスト演算子が**&の場合、seq 5 | grep 3
はバックグラウンドで実行され、その終了を待たずにecho hoge
が実行される。そのため、2つのパイプラインの出力が前後することがある。
リスト演算子が;の場合、まずseq 5 | grep 3
が実行され、その終了を待って、echo hoge
が(必ず)実行される。
リスト演算子が&&の場合、まずseq 5 | grep 3
が実行され、その終了ステータスが0の場合に限り、echo hoge
が実行される。
リスト演算子が||**の場合、まずseq 5 | grep 3
が実行され、その終了ステータスが0以外の場合に限り、echo hoge
が実行される。
{ list; }
{ list; }
list が単に現在のシェル環境で実行されます。
list の最後は改行文字かセミコロンでなければなりません。
これは グループコマンド(group command) と呼ばれます。
返却ステータスは list の終了ステータスです。
メタ文字である ( や ) と違い、 { と } は 予約語 であり、予約語として認識される場所に現われる必要があることに注意してください。
これらは単語分割の対象とならないため、 リスト との間が空白またはシェルのメタ文字で分かれている必要があります。
前述のように
パイプライン
は「|で区切られた1つ以上のコマンドの並び」であり、
リスト
は「1つ以上のパイプラインを演算子 ;, &, &&, || のいずれかで区切って並べたもの」である。
ここで「リスト全体を、1つのコマンドのように扱い、パイプラインの要素として組み込んだり、リダイレクトを使いたい」場合には、2種類の複合コマンドを使うことで実現できる。
$ (seq 5 | grep 3 && echo hoge) | wc -l
2
$ { seq 5 | grep 3 && echo hoge; } | wc -l
2
(list) という形式の複合コマンドは、リストをサブシェル(子プロセス)で実行する。
{ list; } という形式の複合コマンドは、サブシェルを使わず、リストを現在のシェルプロセスでそのまま実行する。(forkのオーバーヘッドがない)
(
と)
はシェルのメタ文字なので、前後にスペースを空けなくとも単語区切りとして解釈される。
{
と}
はシェルの予約語なので、前後の単語と分離できない場合には、スペースを空けないと文法エラーになる。
$ {seq 5 | grep 3 && echo hoge; } | wc -l
bash: syntax error near unexpected token `}'
# { と seq を単語分割できない
$ { seq 5 | grep 3 && echo hoge;}| wc -l
2
# ; と | が単語区切りになるメタ文字なのでスペースを詰めても良い
クォートの文法
|
&
;
(
)
<
>
$
やスペースなどの文字はシェルで特別な意味を持つため、元の文字そのものを表現したい場合には、クォートを使って特別な意味や機能を無効化することができる。
bashのクォートには3種類ある。
- バックスラッシュ
\
をクォートしたい文字の前に付ける - シングルクォートで囲む(全ての文字が特別な意味を失う)
- ダブルクォートで囲む( $ ` \ ! 以外の文字が特別な意味を失う)
$'str'
改行、バックスペース、エスケープなどの制御文字や、ASCII範囲外のUnicode文字を、ASCIIテキストで書かれたシェルスクリプトで表現したい場合には、クォートを使って特別な意味を無効化するだけでは足りず、何らかのASCII表現でそれらの文字を表す必要がある。
bashの組み込みコマンドもしくは外部コマンドであるecho
やprintf
と、bashのコマンド置換機能を用いて
$ touch "$(echo -e '\U1F363')"; ls
🍣
$ touch "$(printf '\U1F363')"; ls
🍣
のようにする方法もあるが、コマンドがサブシェルで実行されるオーバーヘッドがあり、コードも長くなる。
ここで$'str'
というクォートの文法を用いると
$ touch $'\U1F363'; ls
🍣
と短く表現でき、サブシェル実行のオーバーヘッドもない。
さいごに
今回紹介した文法は、bashの文法のほんの一部分に過ぎない。
bashには、他にも便利で面白い文法がまだまだあるので、興味があればぜひman bash
を読んでみて欲しい。