ysapでbash入門⑥
bash学習記録:基礎編(Chapter 01 ~ Chapter 07, Section 02の復習編)
前回Chapter 07 Section 02まで進んだところで、一度Chapter 01から復習することにした。改めて見返すと新たな疑問や気づきが多く、寄り道しながらもかなり学びのある回になった。と、同時に若干飽きてきた感も出てきました。(笑
[[]]と(())の違い
復習を進めながら改めて整理した。見た目が似ているが用途が全く違う。
[[]]は文字列や条件の比較に使う:
name="dave"
if [[ $name == "dave" ]]; then
echo "hello dave"
fi
if [[ -f ./script.sh ]]; then # ファイルが存在するか
echo "exists"
fi
(())は**算術式(数値の計算・比較)**に使う:
num=5
if (( num > 3 )); then
echo "bigger than 3"
fi
(( num++ )) # インクリメント
(( result = num * 2 )) # 計算
一番わかりやすい使い分けは「数字を扱うなら(())、それ以外は[[]]」だ。混乱しやすいのが数値の比較演算子で:
[[ 10 > 9 ]] # 危険!文字列比較なので"1"と"9"を比べてしまう
(( 10 > 9 )) # 正しい数値比較
数値比較は必ず(())を使うのが安全だ。
bashの構文まとめ
似たような記法が多いので表で整理した。
| 構文 | 名前 | 用途 | 例 |
|---|---|---|---|
$() | コマンド置換 | コマンドの出力を変数に | today=$(date) |
$(()) | 算術式展開 | 数値計算の結果を変数に | result=$((3 + 5)) |
${} | パラメータ展開 | 変数の値を展開・加工 | ${var:-default} |
"" | ダブルクォート | $展開をしつつ文字列化 | "hello $name" |
'' | シングルクォート | 一切展開しない文字列 | 'hello $name' |
[] | test命令 | 条件判定(旧来) | [ -f file.txt ] |
[[]] | 条件式 | 条件判定(bash拡張) | [[ -f file.txt ]] |
(()) | 算術式評価 | 数値の条件判定・計算 | (( num > 3 )) |
() | サブシェル | 現在のシェルを汚染せずに実行 | ( cd /tmp; ls ) |
{} | 現シェルグループ | 現在のシェルでまとめて実行 | { echo hi; echo bye; } |
ダブルクォートは$変数や$()などの展開を有効にしつつ、スペースを含む文字列をひとまとまりとして扱いたい時に使う。シングルクォートは一切展開しないので、$nameと書いてもそのまま文字列として表示される。
Chapter 03-03: Conditionals(&&と||の使い分け)
&&と||の挙動について改めて整理した。
[[ -f file.txt ]] && echo "it exists" # 成功したら実行
[[ -f file.txt ]] || echo "not found" # 失敗したら実行
最初は「&&はANDだから両方、||はORだから片方」と英語的に読んで混乱していた。シェルでは左のコマンドの結果次第で右を実行するかどうか決まるという流れで読む方がしっくりくる。
一行で両方書くこともできる:
[[ -f file.txt ]] && echo "it exists" || echo "its not"
ただしこれには落とし穴があって、&&の部分が失敗した場合も||が実行されてしまうことがある。スクリプトに書く場合はif文の方が安全だ:
if [[ -f file.txt ]]; then
echo "it exists"
else
echo "its not"
fi
zshでの!&&問題
zshで! [[ -f file.txt ]] && echo "not"と書こうとして詰まった:
[[ -f file.txt ]] !&& echo not
zsh: event not found: &&
zshが!をヒストリ展開として解釈してしまうのが原因だ。!を先頭に持ってきて正しく書くか:
! [[ -f file.txt ]] && echo "not found"
あるいは||を使う方がシンプルだ:
[[ -f file.txt ]] || echo "not found"
Chapter 04-01: Indexed Arrays
配列の操作を復習した。shellcheckがEエラーを出したが正しく動くという場面があった:
echo "@: ${array[@]}"
# E: Argument mixes string and array. Use * or separate argument.
"${array[@]}"はクォートすると各要素が別々の引数として展開されるため、文字列と混在させると意図しない動作になる可能性があるとshellcheckが警告する。echoの場合は全引数をスペースでつなげて表示するだけなので結果的に動いてしまうが、正しく書くなら:
echo "*: ${array[*]}" # 全要素を1つの文字列として展開
echo "@:" "${array[@]}" # 別引数として渡す
@と*の使い分けを改めて整理すると:
"${array[@]}"→ 各要素を別々の引数として展開"${array[*]}"→ 全要素を1つの文字列として展開
表示結果は同じように見えても引数の渡し方が違うので、スペースを含む要素がある場合に差が出る。
Useless Use of Cat
cat /usr/share/dict/words | ./read-input # catを使う方法
./read-input < /usr/share/dict/words # リダイレクトを使う方法
結果は同じだが仕組みが違う。catを使う方法はプロセスを2つ起動する(catプロセスとスクリプトのプロセス)。リダイレクトはプロセスを1つだけ起動し、シェルが直接ファイルをstdinに繋ぐだけだ。
これがDaveの動画でも紹介されているUseless Use of Catで、リダイレクトで直接渡せるのにわざわざcatを挟むのは無駄という話だ。catを使う書き方は視覚的にわかりやすいので初心者がやりがちだが、知識がついてきたら自然と<リダイレクトで書くようになる。
ファイルディスクリプタとstderrの流れ
>&2の意味について改めて整理した。
2>/dev/nullや>&2の2はstderrのファイルディスクリプタ番号で、ファイルディスクリプタは以下の3つが基本だ:
| 番号 | 名前 | 役割 |
|---|---|---|
| 0 | stdin | 標準入力 |
| 1 | stdout | 標準出力 |
| 2 | stderr | 標準エラー出力 |
&をつけることで「ファイルではなくファイルディスクリプタの番号として解釈しろ」という意味になる:
2> file→ stderrをファイルにリダイレクト>&2→ stdoutを**ファイルディスクリプタ2(stderr)**にリダイレクト
重要なのはstderrは溜まる場所ではなく出口だという点だ。デフォルトではstdoutもstderrも両方ただ画面に表示されるだけで、どこかに蓄積されているわけではない。stderrに流す意味は分離できる可能性を持たせることで、必要な時に:
./script.sh > output.log 2> error.log
と書けば通常ログとエラーログを別々に保存できる。「どこに流すか」と「流したものをどうするか」は別の話で、スクリプトを書く側は「エラーはstderrに流しておく」という行儀の良い設計をするだけで、実際にどう扱うかは呼び出し側に委ねる考え方だ。
caseの|について
for name in "$@"; do
case "$name" in
d* | b*) hello "$name";;
*) goodbye "$name";;
esac
done
case文の中の|はパイプでも論理ORでもなく、パターンの区切り文字として使われている。d*もしくはb*という意味だ。
||(論理OR)と結果的には「または」という意味は同じだが見ているものが違う。caseの|はあくまで文字列パターンの話で、||はコマンドの成功・失敗の話だ。理屈より先に「caseの中では|がパターンの区切り」と覚えてしまうのが早い。
read -rの-rフラグ
Chapter 03-01のUser Inputで出てくるreadコマンドだが、shellcheckが以下の警告を出す:
I: read without -r will mangle backslashes.
-rなしのreadはバックスラッシュをエスケープ文字として解釈してしまう。例えばユーザーが\nと入力しても、bashが改行として処理してしまい入力値が変わってしまう。
# 悪い例
read -p "input: " val
# 良い例
read -rp "input: " val
-rをつけることでバックスラッシュをただの文字として扱う。readを使う時はほぼ常に-rをつけるのが定石で、shellcheckも必ず警告を出してくれる。
forループとbrace展開の制約
Chapter 03-04のFor Loopsで詰まったポイント。ユーザーから数字を受け取ってその回数ループしようとすると:
read -rp "put anything you like number: " num
for i in {1..$num}; do # W: Bash doesn't support variables in brace range expansions.
echo "$i"
done
shellcheckがWの警告を出す。bashのbrace展開{1..10}はスクリプトの実行前に展開されるため、変数を使った{1..$num}はサポートされていない。$numが展開される前にbrace展開が処理されてしまうからだ。
解決策はCスタイルのforループを使うことだ:
for ((i=1; i<=num; i++)); do
echo "$i"
done
(())の中は算術式なので$なしで変数を参照できる。ループの外では$numと書くのに(())の中ではnumだけでいいという点も覚えておきたい。
seqコマンドを使う方法もある:
for i in $(seq 1 "$num"); do
echo "$i"
done
ただDaveはCスタイルを推奨していたので、bashらしい書き方として(())を使うのが良さそうだ。
まとめ
復習回だったが、改めて見直すことで&&と||の挙動、@と*の配列展開の違い、stderrの「流れ」の概念など、最初に学んだ時より深く理解できた部分が多かった。bashの構文は似たような記法が多く混乱しやすいが、一個一個の仕組みがわかってくると「あ、そういうことか!」の連続で面白い。
次回はChapter 07 Section 03のRecapから進めていく。
学習リソース: