ysapでbash入門⑦

·
#CLI#Terminal#ysap#bash

bash学習記録:基礎編(Chapter 11 , Section 02まで)

Chapter 7 Recap:連想配列(Associative Array)

/etc/passwd のデータ構造を理解したことで、スクリプトの挙動が一気に読めるようになった。

root:x:0:0:root:/root:/bin/bash
 ↓    ↓  ↓  ↓   ↓     ↓       ↓
name pass uid gid gecos home shell

IFS=: で区切り文字をコロンに設定し、read で各フィールドを変数に割り当てる。while ループの中で shells[$name]=$shell と書き込んでいく、これが連想配列の基本的な使い方。

declare -A shells   # 連想配列を定義(-A がポイント)

while IFS=: read -r name pass uid gid gecos home shell; do
    if [[ $name == '#'* ]]; then
        continue
    fi
    shells[$name]=$shell   # ユーザー名をキーにシェルを格納
done < /etc/passwd

echo "nobody's shell: ${shells[nobody]}"

read で定義された変数はループ内でなんでも使える。$uid$home も当然アクセス可能で、今回は $name$shell だけ使って他は無視しているだけ。


Chapter 8:Parameter Expansion と Array Expansion

Parameter Expansion とは

変数を単に $var で取り出すだけでなく、取り出しながら加工や条件分岐ができる仕組み。パイプや外部コマンドを使わずに済むのでシンプルに書ける。

name="Dave"
echo ${name}        # Dave(基本形、{}で囲む)
echo ${#name}       # 4(文字列の長さ)

デフォルト値の指定

echo ${var:-default}    # varが空なら "default" を表示(varは変えない)
echo ${var:=default}    # varが空なら "default" をvarに代入して表示

スクリプトで「引数がなければデフォルト値を使う」という処理によく登場する。

文字列の削除

filename="photo-2022-10-31.jpg"

echo ${filename#*.}     # jpg(最短一致で前から削除)
echo ${filename##*.}    # jpg(最長一致で前から削除)
echo ${filename%.*}     # photo-2022-10-31(最短一致で後ろから削除)
echo ${filename%%.*}    # photo-2022-10-31(最長一致で後ろから削除)

# が前から、% が後ろから削除。##%% で最長一致になる。感覚としては # は先頭を削る、% は末尾を削ると覚えると少し楽。

文字列の置換

echo ${filename/jpg/png}    # photo-2022-10-31.png(最初の1箇所)
echo ${filename//o/0}       # ph0t0-2022-10-31.jpg(全部置換)

Array Expansion とは

配列の要素を取り出したり、全体を操作するための展開。

arr=("apple" "banana" "cherry")

echo ${arr[0]}      # apple(インデックスで1つ取り出す)
echo ${arr[@]}      # apple banana cherry(全要素)
echo ${#arr[@]}     # 3(要素数)
echo ${arr[@]:1:2}  # banana cherry(1番目から2個)

[@] が「全要素」を意味するキーで、${arr[@]} の形はループとセットでよく使う。

for item in "${arr[@]}"; do
    echo "$item"
done

"${arr[@]}" とダブルクォートで囲むのが重要で、スペースを含む要素が1つの要素として正しく扱われる。


Chapter 9:shopt と set

shopt とは

shopt"shell option" の略。bashの動作をカスタマイズするための設定コマンド。

shopt -s オプション名   # 有効化(set)
shopt -u オプション名   # 無効化(unset)
shopt                   # 現在の設定一覧を確認

set との違い

コマンド対象
shoptbash固有のオプション
setPOSIXシェル共通のオプション(他のシェルにも存在)

set- で有効化、+ で無効化と、shopt-s/-u と逆なので注意。

extglob:拡張glob

shopt -s extglob

通常の *? に加えて、より複雑なパターンマッチが使えるようになる。

パターン意味
?(パターン)0回か1回マッチ
*(パターン)0回以上マッチ
+(パターン)1回以上マッチ
@(パターン)ちょうど1回マッチ
!(パターン)マッチしないもの

特に !() が便利で、「〇〇以外」を指定できる。

shopt -s extglob
ls -1 files/!(*.txt)    # .txt以外のファイルを表示
rm !(*.jpg)             # .jpg以外を全部削除

ls -la でドットファイルは「見える」が、dotglob は「まとめて操作」するときに力を発揮する。

shopt -s dotglob
cp files/* backup/    # .で始まるファイルも一緒にコピー

例えばリポジトリをクローンして .git ディレクトリを消したいときはこれが使える。

shopt -s dotglob
rm -rf .git*    # .gitから始まるものを全部削除

ライフタイム

shopt -s の設定はそのシェルセッションが終わると消える。永続化したい場合は ~/.bashrc に書いておく。

# ~/.bashrc に追記
shopt -s extglob
shopt -s dotglob

Chapter 10:Brace Expansion

arr=(/etc/{foo,bar}/{1,2,3}.txt)

これは展開すると6ファイルが一気に対象になる。

/etc/foo/1.txt
/etc/foo/2.txt
/etc/foo/3.txt
/etc/bar/1.txt
/etc/bar/2.txt
/etc/bar/3.txt

数値・アルファベットの連番展開

echo {1..10}        # 1 2 3 4 5 6 7 8 9 10
echo {01..10}       # 01 02 03 04 05 06 07 08 09 10(ゼロパディング)
echo {a..z}         # a b c d ... z
echo {1..10..2}     # 1 3 5 7 9(2ステップ)

ゼロパディングはファイル名の連番作成などで便利。

touch file-{01..05}.txt    # file-01.txt〜file-05.txt を一気に作成
mkdir {2020..2026}         # 年ごとのディレクトリを一気に作成

強力な分、ミスったときのダメージも大きい。まず echo で確認してから実行するのが鉄則。

echo rm /etc/{foo,bar}/{1,2,3}.txt   # 展開結果を確認
rm /etc/{foo,bar}/{1,2,3}.txt        # 問題なければ実行

これを学んでいてふと気づいたこと:初期のコンピュータウィルスがシェルスクリプト数行で壊滅的なダメージを与えられた理由がよくわかる。権限管理の大切さはUnixの初期(1970年代)から存在していたが、当時は物理的にアクセスできる人が限られていたので問題にならなかった。インターネットの普及で「信頼前提」の設計が一気に弱点になった。


Chapter 11-01:date と printf の時刻フォーマット

Epoch Time(エポック時間)

Unixは 1970年1月1日 00:00:00 UTC を時間の原点(0)として、そこからの秒数で時刻を管理している。

printf '%(%Y/%m/%d %H:%M:%S)T\n' -1          # -1 は「今この瞬間」
printf '%(%Y/%m/%d %H:%M:%S)T\n' 0            # 1970年1月1日
printf '%(%Y/%m/%d %H:%M:%S)T\n' 100000000    # 1973年3月3日

%(...)T は printf の時刻フォーマット専用の書き方で、秒数を渡すと日時に変換して表示してくれる。

2038年問題

32bitシステムだと秒数を保存できる上限が 2147483647、つまり 2038年1月19日 に達する。そこを超えると0に戻ってしまうのがいわゆる2038年問題。64bitなら西暦292277026596年まで大丈夫なので実質永遠。ただ工場の機械や医療機器など、古い32bitシステムを使い続けている現場では今後問題になりうる。

DockerコンテナのUTCについて

Daveのbashlab環境はDockerコンテナで動いており、デフォルトでUTCになっている。バンクーバー(PST = UTC-8)から使っているため時刻がずれて見えるが、学習上は無視してOK。Dockerfileで ENV TZ=America/Vancouver と書くか、起動時に -e TZ=America/Vancouver を渡せばタイムゾーンを合わせられる。


Chapter 11-02:printf と正規表現

printf の変数直接展開は危険

# 危険
printf $name

# 安全
printf "%s" "$name"
printf 'everything have to be string (%s)\n' "$name"

printf の最初の引数はフォーマット文字列として解釈される。変数を直接渡すと、その中身が %s%n のようなフォーマット文字として解釈されてしまう。SQLインジェクションと同じ概念で、「外から来たデータをそのまま命令として使うな」 という原則。

フォーマット文字列は自分で書いた信頼できる固定文字列にして、データは引数として渡す。

正規表現:02-good の解説

01-fine はパイプを繋げて切り出していたが、02-good では正規表現で一発。

regex='^.*\/(.*) - ([0-9]{4}-[0-9]{2}-[0-9]{2})\..*$'
for f in ./images/*; do
    if ! [[ $f =~ $regex ]]; then
        echo "$f didn't match pattern"
        continue
    fi

    name=${BASH_REMATCH[1]}
    date=${BASH_REMATCH[2]}
    echo "$date: $name"
done

対象ファイル名:./images/fun family party - 2022-10-31.jpg

正規表現の分解:

部分意味マッチする部分
^行の先頭
.*何でもいい./images
\// リテラル/
(.*)何でもいい(キャプチャfun family party
-そのまま--
([0-9]{4}-[0-9]{2}-[0-9]{2})日付(キャプチャ2022-10-31
\.. リテラル.
.*何でもいいjpg
$行の末尾

() で囲んだ部分がキャプチャグループで、マッチした内容は自動的に BASH_REMATCH 配列に入る。

BASH_REMATCH[0]   # マッチ全体
BASH_REMATCH[1]   # 1つ目の () → fun family party
BASH_REMATCH[2]   # 2つ目の () → 2022-10-31

学習リソース: