いまだに自分も初心者ですが、 初心者から見て初めに知っておきたかったこと、思いがけず詰まったことをメモしておきたいと思います。
誰か(来年のぼく)の参考になればと思います。
[目次]
環境
- 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 = huga
echo "$a" "$b" "$c"
[ はコマンドです!
[
は例えば次のように使います。
$ cat test.sh
a=2022
if [ $a -gt 2000 ]; then
echo "2000年代です。"
fi
この時、[
の前後に変なスペースを入れてはいけません。
これは [
がコマンドの一部であるためであるためです。
コマンドの後は引数やオプションが来ると思うのですが、その時にはスペースを開けると思います、その感覚です。
コマンドであること・使い方は、実際に man
で確認できます。
$ man [
NAME
test - check file types and compare values
SYNOPSIS
test EXPRESSION
test
[ EXPRESSION ]
[ ]
[ OPTION
...
エラーが起きてもスクリプトは終わらない
$ cat test.sh
a="hoge"
c = huga
echo "$a" "$b" "$c"
上記スクリプトは c = huga
の部分でエラーになるので、そこで通常はプログラムの実行が止まるのですが(少なくともぼくにとっては)、実はエラーが起きても止まりません!。
最後まで強制的に実行を続けます(実はこれは bash の起動時のオプションに依存)
$ bash test.sh
test.sh: line 5: c: command not found
hoge
エラー時にスクリプトを止めるオプション
errexit
のオプションを on
にすることで、エラー時に終了させることが可能です。
$ set -o | grep errexit
errexit off
修正したスクリプト
$ cat test.sh
set -e
a="hoge"
c = huga
echo "$a" "$b" "$c"
実行
$ bash test.sh
test.sh: line 6: c: command not found
未定義変数の取り扱いに注意する
通常では、未定義変数を参照した時もスクリプトは止まりません!
このことは、時に恐ろしい事態を招いてしまいます。
data_dir="/home/ubuntu/Documents/work/pien/data"
rm -r "${data_dir}/${path_to_file}"
未定義変数使用時にスクリプトを終了するオプション
エラー時同様、bash 起動時のオプションをつけてあげることで回避可能です。
$ set -o | grep nounset
nounset off
修正したスクリプト
$ cat test.sh
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
exit 128 | exit 64 | exit 0
echo $?
$ bash test.sh
0
これは困ります。
パイプ全体の終了ステータスは、PIPESTATUS
で取得可能です。
$ cat test.sh
exit 128 | exit 64 | exit 0
echo "${PIPESTATUS[@]}"
$ bash test.sh
128 64 0
パイプ失敗時にエラーを吐かせる
こちらも、pipefail
のオプションで指定可能です。
$ cat test.sh
set -o pipefail
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
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 hoge | echo
$ echo hoge | xargs echo
hoge
xargs は外部コマンドのみを対象とする!
ビルドインコマンドと外部コマンドについても意識する必要があります。
挙動が異なるものも多く、ここでは馴染み深い 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
echo "script start"
sleep 15
echo "test"
bash ./echo_script.sh
echo_script.sh
echo "Echo from another script file"
sleep 15
の間に ①, ② をそれぞれ変えてどうなるかを確認してみます。
- ①: 元の文言が出力される
- ②: 新しい文言が出力される!
どうやらサブファイルの読み込みなどにおいては、実行時に初めて読み込まれるようです。
これは python 等他のスクリプト言語とは異なるため、時間のかかるスクリプトで複数ファイルに分割している際は注意が必要でしょう。
リダイレクトはファイルを初期化する
$ cat hoge
some text
$ cat hoge > hoge
$ cat hoge
コマンドによっては終了ステータスが 0 じゃない
終了ステータスが 0 より大きいものは異常(エラー?)と習うかと思います。
しかし、コマンドによっては(ぼくにとって)予想外のタイミングで 1 を返すことがあります。
errexit
が有効になっている bash 環境などでは、こちらが変に効いてきてしまうので注意が必要です。
find 系が多いのかなーと思うのですが、した 2 つは実際に困ったことのあるコマンドです。
diff
diff は差分がなかった時が 0、差分があった時が 1、ファイルがなかった時などのエラーが 2 で終了します。
$ diff <(echo hoge) <(echo pien)
...
$ echo $?
1
grep も diff と同様です。
(差分がなかった時が 0、差分があった時が 1、ファイルがなかった時などのエラーが 2)
$ grep bine test.sh
$ echo $?
1
$ grep bine test.she
grep: test.she: No such file or directory
$ echo $?
2
終わりに
随時更新していきます。