Bash setを知る

なにげなくシェルの先頭に

set -eu

とおまじないのように付けてはいませんか?
setのことを知ると、よりシェルを便利に使えると思ったので、ちょっとsetを掘り下げていこうかなと思います。

setとは

シェルの属性の設定/解除を行うBashの組み込みコマンドです。
例えば、以下のようなsetをシェル冒頭に記述すると、コマンド実行時エラーが検出したら即座にシェルがexitします。

$ vim test.sh
------------------------
#!/bin/bash

set -e

ls -l hoge # 存在しないファイル
echo "test"
------------------------

# 上記シェル実行
$ ./test.sh

# set -eがない場合
#ls: cannot access hoge: No such file or directory
#test

# set -eがある場合(echo "test"が出力されない)
#ls: cannot access hoge: No such file or directory

このsetにはどんなオプションがあるのだろう?
それを調べてみます。

オプション

それぞれのオプションの詳しい動作を見ていきます。
ちなみにオプションがない場合は、設定されているシェル変数が全て表示されます。

また、移行のオプションは「-*」とハイフンで記述していきますが、「+*」とプラスで記述すると、その属性の設定が解除されます。

-a

export test=hoge としなくても、自動で変数などが環境にエクスポートするオプションです。

$ vim test.sh
------------------------
#!/bin/bash

set -a

a=hoge
b=$(bash test2)
echo ${b}
------------------------

$ vim test2
------------------------
echo ${a}
------------------------

$ ./test.sh

# set -aがない場合
#(なにも表示されない)

# set -aがある場合
#hoge

-b

バックグラウンドのジョブのステータス報告を即座に行ってくれるオプションです。

# まずはオプションを設定しないパターン
$ sleep 5s

# 5秒後にEnterキー
#[1]+  Done                    sleep 5s

# 次にオプションを設定するパターン
$ set -b
$ sleep 5s
# 5秒後に勝手に表示される
# [1]+  Done                    sleep 5s

-e(-E/-T)

上述した通り、シェル内でエラーが発生した場合、即座に終了するオプションです。

$ vim test.sh
------------------------
#!/bin/bash

set -e

ls -l hoge # 存在しないファイル
echo "test"
------------------------

# 上記シェル実行
$ ./test.sh

# set -eがない場合
#ls: cannot access hoge: No such file or directory
#test

# set -eがある場合(echo "test"が出力されない)
#ls: cannot access hoge: No such file or directory

さて、このオプションと組み合わせると便利なのが trap コマンドです。
指定されたシグナルを検知したら、指定のコマンドを実行してくれるコマンドです。
今回の場合はERRシグナルを対象とします。

$ vim test.sh
------------------------
#!/bin/bash

set -e

trap 'echo error!!' ERR

ls hoge # 存在しないファイル
echo test
------------------------

$ ./test.sh
#ls: cannot access hoge: No such file or directory
#error!!

とても便利なのですが、シェル関数やサブシェルでのエラーはERRシグナルとして検知してくれません。

$ vim test.sh
------------------------
set -e

trap 'echo error!!' ERR

function test_function() {
    ls hoge # 存在しないファイル
}

test_function

echo test
------------------------

$ ./test.sh
# trapのコマンドが実行されていない
#ls: cannot access hoge: No such file or directory

シェル関数やサブシェルでのエラーも検出したい場合は、
「-E」オプションを指定すると検出してくれるようになります。

$ vim test.sh
------------------------
set -eE

trap 'echo error!!' ERR

function test_function() {
    ls hoge # 存在しないファイル
}

test_function

echo test
------------------------

$ ./test.sh
#ls: cannot access hoge: No such file or directory
#error!!

今回の「-E」はERR疑似シグナルに対してですが
「-T」はDEBUGとRETURNに対して同様の働きをします。

-f

パス名展開を無効にするオプションです。
この字面だと意味がわかりませんが、ワイルドカード指定などができなくなります。

$ ls test*
#test  test2

$ set -f
$ ls test*
#ls: cannot access test*: No such file or directory

-k

引数に指定された代入文が、そのコマンドの環境変数となります。

$ vim test.sh
------------------------
#!/bin/bash
set -k
bash test2 TEST=hoge
------------------------

$ vim test2
------------------------
echo ${TEST}
------------------------

$ ./test1.sh
# set -kがない場合
#(なにも表示されない)

# set -kがある場合
#hoge

-m

ジョブ制御を有効にします。
システム上の対話的シェルではデフォルトで有効です。
これが有効だと、fg/bgコマンドがシェル内で使用できるようになります。

以下の例では、一時中断したものを再度実行しています。

$ sleep 60
# ここでCtrl+z
#[1]+  Stopped                 sleep 60

$ jobs
#[1]+  Stopped                 sleep 60

$ fg %1
#sleep 60

無効にすると当然ですが動きません

$ set +m
$ sleep 60
# ここでCtrl+zしても無反応

恥ずかしながらあまりジョブ制御について知らなかったので、調べてみたところ大変便利なものと気付きました。
ジョブ制御については別の機会に記事に出来ればと思います。

-n

コマンドを読み込みはしますが、実行はせず、構文エラーのみチェックします。

$ vim test.sh
------------------------
#!/bin/bash

set -e

touch hoge
function hoge() {
------------------------

$ ./test.sh
# ./test.sh: line 7: syntax error: unexpected end of file

-t

コマンドを1つ読み込み、実行後終了します。 いまいち使い所がわからない。。。

$ vim test.sh
------------------------
#!/bin/bash

# 「;」で区切らず、改行すると「set」コマンド実行後終了する…
set -t ; echo test
echo test2
------------------------

$ ./test.sh
# test

-u

設定されてない変数を展開しようとするとエラーが発生するようになります。

$ vim test.sh
------------------------
#!/bin/bash

set -u
echo ${test}
------------------------

$ ./test.sh
# set -u を指定しない場合
# (何も表示されない)
# set -u を指定している場合
# ./test.sh: line 4: test: unbound variable

-v/-x

入力されたコマンドを出力します。デバッグなどに便利です。
以下は「-v」の例です。

$ vim test.sh
------------------------
#!/bin/bash

set -v

for i in $(seq 1 3)
do
  echo ${i}
done
------------------------

$ ./test.sh
#for i in $(seq 1 3)
#do
#  echo ${i}
#done
#seq 1 3
#1
#2
#3

「-x」も同様にコマンドを出力するオプションなのですが、少し違います。

$ vim test.sh
------------------------
#!/bin/bash

set -x

for i in $(seq 1 3)
do
  echo ${i}
done
------------------------

$ ./test.sh
#++ seq 1 3
#+ for i in '$(seq 1 3)'
#+ echo 1
#1
#+ for i in '$(seq 1 3)'
#+ echo 2
#2
#+ for i in '$(seq 1 3)'
#+ echo 3
#3

はい、「-x」の場合はforが展開されています。
「-x」のほうがよりデバッグ用途に使いやすいかなと思います。

-B

ブレース展開を有効にします。デフォルトで有効です。
ブレース展開とは?というのは、下記の例を見てもらえればわかると思います。

$ vim test.sh
------------------------
#!/bin/bash

set -B
touch hoge{.1,.2}
------------------------

$ ./test.sh
$ ls -l hoge*
#hoge.1  hoge.2

という感じで展開されます。以下のようなこともできます。

$ vim test.sh
------------------------
#!/bin/bash

set -B
mkdir -p {test1,test2}/hoge/{fuga,piyo}
------------------------

$ ./test.sh
$ tree
#.
#├── test1
#│   └── hoge
#│       ├── fuga
#│       └── piyo
#├── test2
#│   └── hoge
#│       ├── fuga
#│       └── piyo
#└── test.sh

-C

リダイレクト演算子( > , >& , <> )で既存ファイルへの書き込みができなくなります。
ただし、「>|」と書くと上書きできます。

$ vim test.sh
------------------------
#!/bin/bash

set -C
touch hoge
# この書き方だと上書きできない
echo "test" > hoge
cat hoge

# この書き方ならできる
echo "test2" >| hoge
cat hoge
------------------------

$ ./test.sh
./test.sh: line 4: hoge: cannot overwrite existing file
test2

-P

シンボリックリンクに対してcdする際に、シンボリックリンクを辿らず、物理的ディレクトリ構造が使われるようになります。

$ mkdir test1
$ ln -s test1 test2
$ ls -l
# test1 test2
$ cd test2 ; pwd
# ~/test2
$ cd .. ; set -P ; cd test2 ; pwd
# ~/test1

最後に

一通りオプションを見てきましたが、
有用そうなのもあれば、何に使うかわからないものもチラホラありました。

結局は自分がどのようなものを作りたいかにより利用するオプションは当然変わってくると思いますが、
とりあえず

set -axueE

あたりを付けておけば便利になるかなと思いました。