いまだに自分も初心者ですが、 初心者から見て初めに知っておきたかったこと、思いがけず詰まったことをメモしておきたいと思います。
誰か(来年の自分)の参考になればと思います。
[目次]
- 環境
- 結論
- スタイル編
- エラーが起きてもスクリプトは終わらない
- 未定義変数の取り扱いに注意する
- パイプは最後の終了ステータスしか見ない
- 引数と標準入力を区別する
- シェルスクリプト実行中に適時シェルスクリプトを読み込む
- リダイレクトはファイルを初期化する
- コマンドによっては終了ステータスが 0 じゃない
- 終わりに
環境
- Machine: Raspberry Pi 4 Model B Rev 1.4 - OS: Linux ubuntu 5.4.0-1045-raspi - Bash version: GNU bash 5.0.17(1) aarch64
結論
* ShellCheck を導入しよう * VSCode の拡張もあるよ
スタイル編
この辺は ShellCheck で圧倒的にカバー可能。
変数
変数定義では前後に余計なスペースは絶対入れてはなりません。
a="hoge" b=pien # c: command not found というエラーになる。 c = huga echo "$a" "$b" "$c"
[ はコマンドです!
[
は例えば次のように使います。
$ cat test.sh #!/bin/bash a=2022 if [ $a -gt 2000 ]; then echo "2000年代です。" fi # これはアウト # if [$a -gt 2000]; then
この時、[
の前後に変なスペースを入れてはいけません。
これは [
がコマンドの一部であるためであるためです。
コマンドの後は引数やオプションが来ると思うのですが、その時にはスペースを開けると思います、その感覚です。
コマンドであること・使い方は、実際に man
で確認できます。
$ man [ NAME test - check file types and compare values SYNOPSIS test EXPRESSION test [ EXPRESSION ] [ ] [ OPTION ...
エラーが起きてもスクリプトは終わらない
$ cat test.sh #!/bin/bash a="hoge" # Error!! c = huga echo "$a" "$b" "$c"
上記スクリプトは c = huga
の部分でエラーになるので、そこで通常はプログラムの実行が止まるのですが(少なくとも私にとっては)、実はエラーが起きても止まりません!。
最後まで強制的に実行を続けます(実はこれは bash の起動時のオプションに依存)
# 上記ファイル実行時の出力 $ bash test.sh test.sh: line 5: c: command not found hoge
エラー時にスクリプトを止めるオプション
errexit
のオプションを on
にすることで、エラー時に終了させることが可能です。
# デフォルトでは off $ set -o | grep errexit errexit off
修正したスクリプト
$ cat test.sh #!/bin/bash set -e a="hoge" # Error!! c = huga echo "$a" "$b" "$c"
実行
# c の定義以降の echo が呼ばれてないことが分かる。 $ bash test.sh test.sh: line 6: c: command not found
未定義変数の取り扱いに注意する
通常では、未定義変数を参照した時もスクリプトは止まりません!
このことは、時に恐ろしい事態を招いてしまいます。
#!/bin/bash data_dir="/home/ubuntu/Documents/work/pien/data" # path_to_file が定義されてない時(実装ミスがあった時) # フォルダ全部が削除されてしまう。 rm -r "${data_dir}/${path_to_file}"
未定義変数使用時にスクリプトを終了するオプション
エラー時同様、bash 起動時のオプションをつけてあげることで回避可能です。
# デフォルトでは off $ set -o | grep nounset nounset off
修正したスクリプト
$ cat test.sh #!/bin/bash set -u data_dir="/home/ubuntu/Documents/work/pien/data" rm -r "${data_dir}/${path_to_file}" echo "ここまで来るかな?"
実行
# 未定義変数参照時にスクリプト終了となる。 # 『ここまで来るかな』は来ない! $ bash test.sh test.sh: line 5: result: unbound variable
パイプは最後の終了ステータスしか見ない
bash においては true
, false
もコマンド(see: man true(false)
)なので、それを使って確かめてみます。
$ cat test.sh #!/bin/bash exit 128 | exit 64 | exit 0 echo $? # パイプのうち、最後のコマンドの終了ステータスが反映される。 $ bash test.sh 0
これは困ります。
パイプ全体の終了ステータスは、PIPESTATUS
で取得可能です。
$ cat test.sh #!/bin/bash exit 128 | exit 64 | exit 0 echo "${PIPESTATUS[@]}" $ bash test.sh 128 64 0
パイプ失敗時にエラーを吐かせる
こちらも、pipefail
のオプションで指定可能です。
$ cat test.sh #!/bin/bash set -o pipefail # 3 が全体の終了コード、右から組み立てられる感じ? exit 128 | exit 64 | exit 3 | exit 0 | exit 0 echo $? echo hoge $ bash test.sh 3 hoge
パイプがこけた時点でスクリプトを終了するには、-e
と組み合わせて set -eo pipefail
と指定しておけば問題ないです。
パイプはそれでも実行されます
set -o pipefail
を指定することで、パイプの一部でもこけたらエラーを吐かせるようにすることができました。
ただ、パイプは『繋げて前の出力を待つ』という性質上、組み立てた時点で実行されます!
(パイプ元がパイプ先に接続する時、パイプ先が待機可能になっている必要があるイメージ)
$ cat test.sh #!/bin/bash set -eo pipefail true | false | echo hoge $ bash test.sh hoge
引数と標準入力を区別する
bash に慣れてきてパイプを繋げまくっていた頃、xargs
が何のためにあるのか一瞬迷子になりました。
それは引数と標準入力を意識してなかったことに起因してます。
パイプは標準出力と標準入力を繋げて遊ぶゲームです。
そのため基本的にパイプの駒の候補となるのは、標準出力を受け付けるコマンドです。
(引数と標準入力をどちらも取るコマンドも多く存在します。)
そこで引数を受け付けたい場合に、xargs
を使う、という戦法になります。
$ man xargs
NAME
xargs - build and execute command lines from standard input
...
一番簡単な例を示します。
# echo は引数しか受け付けないため、このワンライナーは何も出力されない。 $ echo hoge | echo # xargs を用いてコマンドを使う。 $ echo hoge | xargs echo hoge
xargs は外部コマンドのみを対象とする!
ビルドインコマンドと外部コマンドについても意識する必要があります。
挙動が異なるものも多く、ここでは馴染み深い echo
を題材に取り上げます。
とりあえず echo
について確認します。
# echo の挙動 $ type echo echo is a shell builtin $ type -a echo echo is a shell builtin echo is /usr/bin/echo echo is /bin/echo ## ビルドインコマンド(通常) $ echo -e '\uFF10' 0 ## 外部コマンド $ /usr/bin/echo -e '\uFF10' \uFF10
xargs
では『外部コマンド』が呼ばれていることを確認します。
$ echo -e 'FF10' | xargs -I@ echo -e '\u@' \uFF10 $ echo -e 'FF10' | xargs -I@ which echo /usr/bin/echo
ではビルドインコマンドを使いたい場合はどうしたらいいのか。
→ シェルを明示的に呼んであげるとよさそうです。
$ echo 'FF10' | xargs -I@ bash -c 'echo -e "\u@"' 0
シェルスクリプト実行中に適時シェルスクリプトを読み込む
シェルスクリプトは呼び出されるまで呼び出されません!
以下の2ファイルを使って簡単に確認してみます。
test.sh
#!/bin/bash echo "script start" sleep 15 echo "test" # ① # サブファイル(echo_script.sh)を実行する bash ./echo_script.sh
echo_script.sh
#!/bin/bash echo "Echo from another script file" # ②
sleep 15
の間に ①, ② をそれぞれ変えてどうなるかを確認してみます。
- ①: 元の文言が出力される
- ②: 新しい文言が出力される!
どうやらサブファイルの読み込みなどにおいては、実行時に初めて読み込まれるようです。
これは python 等他のスクリプト言語とは異なるため、時間のかかるスクリプトで複数ファイルに分割している際は注意が必要でしょう。
リダイレクトはファイルを初期化する
$ cat hoge some text # こういうのは良くない。 # > の時点で対象のファイルが初期化されてしまう。 $ cat hoge > hoge # hoge は空っぽになってしまっている。 $ cat hoge
-C オプションで意図しないファイルの上書きを防ぐ
$ set -C $ echo hoge > memo.md bash: memo.md: cannot overwrite existing file
コマンドによっては終了ステータスが 0 じゃない
終了ステータスが 0 より大きいものは異常(エラー?)と習うかと思います。
しかし、コマンドによっては(私にとっては)予想外のタイミングで 1 を返すことがあります。
errexit
が有効になっている bash 環境などでは、こちらが変に効いてきてしまうので注意が必要です。
find 系が多いのかなーと思うのですが、した 2 つは実際に困ったことのあるコマンドです。
diff
diff は差分がなかった時が 0、差分があった時が 1、ファイルがなかった時などのエラーが 2 で終了します。
$ diff <(echo hoge) <(echo pien) ... $ echo $? 1
grep
grep も diff と同様です。
(差分がなかった時が 0、差分があった時が 1、ファイルがなかった時などのエラーが 2)
# no grep result $ grep bine test.sh $ echo $? 1 # no such file $ grep bine test.she grep: test.she: No such file or directory $ echo $? 2
終わりに
随時更新していきます。