ysapでbash入門⑥

·
#CLI#Terminal#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>&22はstderrのファイルディスクリプタ番号で、ファイルディスクリプタは以下の3つが基本だ:

番号名前役割
0stdin標準入力
1stdout標準出力
2stderr標準エラー出力

&をつけることで「ファイルではなくファイルディスクリプタの番号として解釈しろ」という意味になる:

  • 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から進めていく。


学習リソース: