ysapでbash入門②

·
#CLI#Terminal#ysap#bash

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

Dave Eddy(ysap.sh)のBashコースで学んだことをまとめます。今回は02-03 vim Crash Courseから03-05 Input/Outputまでの基礎編です。(vimの設定が時間掛かった…

開発環境のセットアップ

Vimの設定:LazyVimとの共存

Daveの設定を使いたいが、普段使っているLazyVimも残したい。調査の結果、Neovimは ~/.config/nvim/ を、Vimは ~/.vimrc を参照するため、競合しないことが判明。

# Daveのdotfilesをクローン
git clone https://github.com/bahamas10/dotfiles.git ~/dotfiles

# vimrc だけをシンボリックリンク
ln -sf ~/dotfiles/vimrc ~/.vimrc

# vimディレクトリもリンク(カラースキームなど)
ln -sf ~/dotfiles/vim ~/.vim

# vim-plugをインストール
curl -fLo ~/.vim/autoload/plug.vim --create-dirs \
    https://raw.githubusercontent.com/junegunn/vim-plug/master/plug.vim

# プラグインをインストール
vim +PlugInstall +qall

結果:vim = Daveの設定、nvim = LazyVimで完全に共存

Daveのvimrc の特徴

  • vim-plug ベースのシンプルな構成
  • プラグインは最小限(airline、ale、rust.vim など)
  • set nonumber で行番号なし(ミニマル志向)
  • set textwidth=80 で80カラム制限
  • 明るい背景が好み(set background=light

カスタマイズ:jkでEsc

ホームポジションから手を動かさずにノーマルモードに戻りたかったので、vimrcに追加:

" Custom key mappings
inoremap jk <Esc>

Escキーは遠いので、この設定は快適。シェル職人?の多くが使っている定番マッピング。僕自身lazyvimを使いたての時からEscがhhkbで若干遠いために使っていたマッピングで、Claude、gptから教えてもらいました。

Dave Eddyのスタイルから学んだこと

ターミナル中心の哲学

Daveの設定を見て気づいたこと:

gitconfig がミニマル

[user]
        name = Dave Eddy
        email = dave@daveeddy.com
[color]
        ui = auto
[push]
        default = matching
[pull]
        rebase = false

エイリアスなし。完全に素のgitコマンドを使っている。理由:

  • コマンドを体で覚える
  • どのマシンでも同じ操作(環境依存を避ける)
  • スクリプト内でエイリアスは使えないから

シバンの深掘り:なぜ #!/usr/bin/env bash なのか

シバンとは何か

#! は "shebang"(シバン)または "hashbang"(ハッシュバン)と呼ばれる。

#!/usr/bin/env bash
# ↑ この1行目がシバン

役割:

  • カーネルに「このスクリプトをどのインタープリタで実行するか」を伝える
  • 必ず1行目に書く(2行目以降は無効)
  • スクリプトに実行権限(chmod +x)があれば、./script で直接実行できる

3つの書き方とその違い

1. /bin/bash 方式(固定パス)

#!/bin/bash
  • bashが /bin/bash にあると仮定
  • Linuxサーバーでは標準的
  • PATH検索しないので、若干速い
  • 問題:環境依存が強い

2. /usr/bin/env bash 方式(PATH検索)- Dave推奨

#!/usr/bin/env bash
  • env コマンドがPATHからbashを探す
  • どこにbashがあっても動く
  • 異なる環境でも移植性が高い
  • モダンなベストプラクティス

3. /bin/sh 方式(POSIX互換)

#!/bin/sh
  • POSIX互換シェル(bashではない)
  • 最大の移植性
  • Bash固有の機能([[, 配列など)は使えない

実際の環境での違い

macOSの例:

# システムデフォルトのbash(古い)
/bin/bash --version
# GNU bash, version 3.2.57 (2007年!)

# Homebrewでインストール
brew install bash

# 新しいbashの場所
which bash
# /opt/homebrew/bin/bash

/opt/homebrew/bin/bash --version
# GNU bash, version 5.2.26 (2024年)

この状況で:

  • #!/bin/bash → 古いバージョン(3.2)が使われる
  • #!/usr/bin/env bash → 新しいバージョン(5.2)が使われる(PATHの順序による)

なぜDaveは env を使うのか

1. 環境の違いに対応

# Linux
/bin/bash

# macOS (Homebrew)
/opt/homebrew/bin/bash

# FreeBSD
/usr/local/bin/bash

# ユーザー独自インストール
/home/user/.local/bin/bash

env を使えば、どこにbashがあっても動く

2. 新しいバージョンを優先

システムに古いbashがあっても、PATHで新しいものを優先できる。

3. Daveの哲学(多分?:「どこでも動くコードを書け」

環境依存を減らすことで:

  • 自分のマシンで動く
  • チームメンバーのマシンで動く
  • CIサーバーで動く
  • 将来別のOSに移行しても動く

セキュリティの考慮

ただし、固定パスの方が安全な場面もある:

# root権限で実行するスクリプト
#!/bin/bash

# PATHが改ざんされていても、固定パスなら安全

サーバー環境やセキュリティが重要な場面では、固定パスも選択肢。しかし一般的な用途では env の柔軟性が勝る

実験:シバンの有無での違い

# シバンなしのスクリプト
echo 'echo "hello"' > test
chmod +x test
./test
# → 現在のシェルで実行される(zshならzsh)

# シバンありのスクリプト
echo '#!/usr/bin/env bash' > test
echo 'echo "hello"' >> test
chmod +x test
./test
# → bashで実行される

シバンがないと、現在のシェルで実行される。これは予期しない動作の原因になる。

拡張子をつけない:Unixの流儀

Daveはスクリプトに .sh 拡張子をつけない。これもUnix哲学の一部。

なぜ拡張子をつけないのか

1. Unixの伝統

# 標準コマンドを見てみる
ls /usr/bin/ | head
# grep, awk, sed, cat, ls, cp, mv...
# どれも拡張子がない

Unix/Linuxの世界では、実行可能ファイルに拡張子は不要。シバンがあればインタープリタが分かる。

2. 実装の隠蔽

# 今日はBashで書いた
mycommand

# 明日Pythonに書き換えても、呼び出し方は同じ
mycommand

拡張子がないと、中身の実装を変えてもユーザーに影響しない

例:

# backup というコマンド
./backup

# 最初はシェルスクリプト
#!/usr/bin/env bash

# 後でPythonに書き換え
#!/usr/bin/env python3

# ユーザーは気にしない、./backup で呼ぶだけ

3. コマンドとして使う

# これは不自然
./backup.sh
git-commit.sh

# これが自然
./backup
git-commit

PATHに入れて使う時、拡張子は邪魔。

# ~/bin に配置
~/bin/deploy.sh   # ✗ 呼び出し: deploy.sh
~/bin/deploy      # ✓ 呼び出し: deploy

4. タブ補完が楽

# 拡張子あり
./back<Tab>up.sh<Tab>  # 2回必要

# 拡張子なし
./back<Tab>  # 1回で完了

いつ拡張子をつけるか

つけた方がいい場合:

  1. ソースコードとして管理
project/
  ├── src/
  │   ├── helper.sh      # ライブラリ(sourceで読み込む)
  │   ├── utils.sh
  │   └── config.sh
  └── bin/
      ├── deploy         # 実行ファイル(拡張子なし)
      └── backup
  1. 複数言語が混在するプロジェクト
scripts/
  ├── build.sh
  ├── test.py
  ├── analyze.rb
  └── deploy.js

ファイルの種類を一目で判別したい時。

  1. Windows互換性が必要

Windowsでは拡張子が重要。クロスプラットフォームを意識するなら .sh をつける。

つけない方がいい場合(Dave式):

  1. コマンドとして使う
~/bin/mycommand     # PATH に入れる
./deploy           # プロジェクトのスクリプト
/usr/local/bin/backup
  1. Unix的に使いたい

標準コマンドのように扱いたい時。

Daveのリポジトリでの使い分け

実際にDaveのdotfilesを見ると:

# 実行ファイル(拡張子なし)
~/bin/mycommand
~/bin/deploy

# ライブラリ(拡張子あり)
~/lib/helper.sh
~/lib/utils.sh

原則:

  • 実行するもの = 拡張子なし
  • 読み込むもの(source) = .sh をつける

シバンと拡張子の関係

#!/usr/bin/env bash
# ↑ シバンがあれば、中身がbashだと分かる

# だから拡張子は不要
# ファイル名だけで、その役割を表現する
backup
deploy
git-cleanup

これがUnix哲学:実装の詳細を隠し、インターフェースをシンプルに。

必須ツールのインストール

# bat(catの強化版)
brew install bat

# シンタックスハイライト付きでファイル表示
bat script.sh

Bashの基礎文法

クォートの違い

name='dave'

# シングルクォート = 完全にリテラル(展開なし)
echo 'hello $name'  # → hello $name

# ダブルクォート = 変数展開される
echo "hello $name"  # → hello dave

覚え方:

  • シングル = 静的(そのまま)
  • ダブル = 動的(展開される)

条件式:[[ vs [

Daveは [[ を積極的に使う。

# Bash推奨
if [[ -n $1 ]]; then
    echo "引数あり"
fi

# 古い書き方(POSIX互換)
if [ -n "$1" ]; then
    echo "引数あり"
fi

[[ の利点:

  • クォート不要で安全
  • 正規表現が使える
  • &&|| が使える

よく使う条件テスト:

[[ -n $var ]]      # 文字列が空でない(not empty)
[[ -z $var ]]      # 文字列が空(zero length)
[[ -f $file ]]     # ファイルが存在
[[ -d $dir ]]      # ディレクトリが存在
[[ $a == $b ]]     # 文字列が等しい

for文:2種類のスタイル

シェル伝統的な書き方(リスト反復):

for name in "$@"; do
    echo "$name"
done

for file in *.txt; do
    echo "$file"
done

C言語風の書き方(カウンター):

for (( i = 0; i < max; i++ )); do
    echo "$i"
done

Daveのポイント:C-styleの方が速い

理由:

  • 外部コマンド(seq)を呼ばない
  • Bash内部で完結
  • メモリ効率が良い
# 遅い(seqコマンドを起動)
for i in $(seq 1 1000000); do
    :
done

# 速い(Bash内部で処理)
for (( i = 1; i <= 1000000; i++ )); do
    :
done

関数

#!/usr/bin/env bash

greet() {
    local name=$1  # ローカル変数
    echo "Hello $name"
}

# 関数呼び出し
greet "dave"

# コマンド置換で出力をキャプチャ
var=$(greet "dave")
echo "var is $var"  # → var is Hello dave

重要:

  • echo = 標準出力(キャプチャ可能)
  • echo >> file = ファイルに書き込み(キャプチャ不可)

入力処理:read

#!/usr/bin/env bash

# 標準入力から1行ずつ読む
while read -r line; do
    echo "we read line: $line"
done

-r オプション必須:

read line      # バックスラッシュをエスケープとして解釈
read -r line   # rawモード(そのまま読む)

使い方:

# ファイルから読む
./read-input < file.txt

# パイプで読む
cat file.txt | ./read-input

# コマンド出力を処理
ls -1 | while read -r filename; do
    echo "Found: $filename"
done

基礎的な発見

隠しファイルの罠

# 間違い:ドットで始まると隠しファイルになる
cp ../hello .hello

# 正解:カレントディレクトリにコピー
cp ../hello .

# 隠しファイルを表示
ls -la

. で始まるファイルは ls で表示されない。基礎中の基礎だが、実際にハマった。

改行の扱い

echo "hello"    # 改行が追加される
echo -n "hello" # 改行なし

xxd でバイナリを確認すると:

echo "hello" | xxd
# 68 65 6c 6c 6f 0a  ← 最後の 0a が改行

echo -n "hello" | xxd
# 68 65 6c 6c 6f     ← 改行なし

変数にコマンド出力を代入する時、改行が含まれていると問題になる場合がある。

Vimの組み合わせ文法

Daveのコースを見ながら気づいた:Vimは「動詞 + 範囲」の組み合わせ。

動詞(操作):

  • d = delete(削除)
  • c = change(変更 = 削除して入力モード)
  • y = yank(コピー)

範囲(対象):

  • iw = inner word(単語の中身)
  • aw = a word(単語全体、スペース含む)
  • i" = 「"」の中身
  • ip = inner paragraph(段落)

組み合わせ例:

ciw    # 単語を削除して入力モード
di"    # "の中身を削除
dap    # 段落を削除
ci(    # ()の中身を変更

これを知ってから、Vimの操作が格段に速くなった。

学びの本質

このセクションを学んで気づいたこと:

プログラミング言語よりも根本的なことを学んでいる。

  • . が「現在のディレクトリ」
  • 隠しファイルの仕組み
  • 標準入力/出力の概念
  • パイプとリダイレクト
  • ファイルシステムの基礎

多くの開発者は「なんとなく」使っているこれらの概念を、本質的に理解できている実感がある。

Unix/Linuxの哲学とシェルは、全てのプログラミングの土台。この基礎を丁寧に学ぶことが、長期的に大きな力になる。

次のステップ

Chapter 4以降で学ぶ内容:

  • Case文
  • 配列(indexed / associative)
  • コマンド置換
  • プロセス置換
  • テキスト処理(sed, awk, grep)

引き続き、Dave の「実用主義」と「ミニマリズム」から学んでいこうと思う。というか、めちゃめちゃ楽しいぞこれ。


参考リンク: