Appendix A — Shell

A.1 Shell とは

Unix 哲学

これから使うシェルという道具は, 前章の Unix の系譜 (Section 1.3.2) で見た OS, Unix とともに Bell 研究所で生まれました. macOS と Linux でコマンドや開発ツールがほぼ共通なのは同じ Unix の流れを汲むためで, これから学ぶシェルの操作もこの系譜の上にあります. その設計の背景にあるのが Unix 哲学 (Unix philosophy) です.

Do One Thing and Do It Well

小さくよく設計された部品を組み合わせ, つなぎ合わせることでより複雑で強力な処理を組み立てる, という思想です. 「ミニマルでモジュラー」とも言われるこの考え方は, シェルの設計そのものに色濃く表れています.

Shell, Terminal, CLI

前章で見たように, アプリケーションは OS が用意した窓口 (システムコール) を通じてその機能を利用するのでした (Section 1.3). シェルもその一つで, OS への指示を人間がキーボードから直接打ち込むためのプログラムです. シェル (shell) やターミナル (terminal) といった言葉は, いずれも コマンドラインインターフェース (CLI, command line interface) を指しています. シェルにはいくつか種類がありますが, ここでは最も標準的な Bash (Bourne again shell) を中心に扱います. 各OSの標準的なシェルは次のとおりです.

  • Linux: Bash
  • macOS: zsh (Bash 互換)
  • Windows: PowerShell, cmd.exe, Git Bash

Windowsの場合はBashと非互換なPowerShellが使われることが多いですが, この授業ではWSLを使うことを推奨しているため, Linuxと同じくBashを使ってもらいます.

なぜシェルを使うのか

GUI (マウス操作) で済むのに, なぜわざわざコマンドを打つのでしょうか. 理由は大きく4つあります.

  • 力 (Power): GUI ではそもそもできないことができます. トラブルの解決もコマンドラインでしかできないことが多いです.
  • 再現性 (Reproducibility): スクリプトに書いた操作は再現できますが, クリック操作は再現できません.
  • サーバーとの対話: リモートのサーバーやスーパーコンピューターを使うとき, シェルが唯一の窓口になることが多いです.
  • AI: AIはシェルを使ってコンピュータを操作します. シェルの仕組みがわかっていれば, AIに指示を出すときもより正確に伝えられます

A.2 シェルの基本

はじめてのシェル

まずはシェルを開いてみましょう. VSCode を使っているなら, Ctrl + ` で統合ターミナルが開きます. macOS のターミナルアプリや, WSL の Ubuntu などでも構いません.

開くと, 次のようなプロンプトが表示されます.

username@hostname:~$

これはシェル流の「あなたは誰で, いまどこにいるのか」という表示です.

  • username は利用者の名前です (1台の計算機に複数いることもあります).
  • @hostname は計算機やサーバーの名前です.
  • :~ はいまいるディレクトリです (~ はホームディレクトリを表します).
  • $ はコマンド入力の開始位置です. 管理者権限を持つ root のときはこれが # に変わります.

キーボードショートカット

シェルを快適に使うために, 最低限これだけは覚えておくとよいでしょう.

  • Tab: 入力の補完
  • / : 過去のコマンドをさかのぼる

コマンドの構文

Bash のコマンドは, すべて同じ基本構文を持っています.

\[ \texttt{command} \quad \texttt{option(s)} \quad \texttt{argument(s)} \]

たとえば次のようになります.

ls -lh ~/Documents/
sort -u myfile.txt

コマンド (command) は何をするかを指定します.

  • オプションや引数は無くても構いませんが, コマンドだけは必ず必要です.
  • ls はディレクトリの中身を一覧表示するコマンド, sort は行を並べ替えるコマンドです.

オプション (option) はフラグ (flag) とも呼ばれ, 動作を細かく指定します.

  • ダッシュ - で始まり, たいてい1文字です.
  • -l は長い形式で表示する, -h は人間に読みやすい単位で表示する, -u は重複行を削除する, といった意味です.
  • 複数のオプションは1つのダッシュにまとめられます.
ls -l -a -h /var/log    # これと
ls -lah /var/log        # これは同じ
  • まれに, 2つのダッシュで始まる長い名前のオプションもあります.
ls --group-directories-first --human-readable /var/log

引数 (argument) は, 何に対して操作するかを指定します.

  • たいていはファイルやパス, あるいはその集合です.

空白

シェルコマンドで大事なのは, 空白と順番によってoptionとargumentが区別されているということです. シェルはまず行を空白で区切ってトークンに分け, その先頭をコマンド名, 残りを引数とみなします. だからこそ空白の有無が意味を大きく変えます.

たとえば環境変数の代入がそうです. 次の2つは, どちらも同じ NAME, =, Alice という語からできていますが, 空白の有無でまったく違う意味になります.

NAME=Alice      # OK: assign "Alice" to the variable NAME
NAME = Alice    # error: run a command called "NAME" with args "=" and "Alice"

NAME=Alice は空白を含まない1つのトークンなので, シェルは「代入」と解釈します. 一方 NAME = AliceNAME / = / Alice という3つのトークンに割れてしまうため, シェルは NAME をコマンド名だと思って実行しようとし, NAME: command not found になります. 代入では = の前後に空白を入れてはいけません.

ヘルプの引き方

コマンドの使い方に困ったら, man (manual pages) が助けになります.

man ls
  • スペースキーで1画面ずつ進みます.
  • /パターン でマニュアル内を検索でき, n で次の一致へ進みます.
  • hman 自身のヘルプ, q で終了します.

man は網羅的ですが冗長なので, 最近ならAIに聞くのもよいでしょう.

A.3 ファイルとディレクトリ

移動する

ディレクトリ間の移動は, 次の2つが基本になります.

  • pwd: いまいるディレクトリ (working directory, current directory) を表示する.
  • cd: ディレクトリを移動する.

絶対パス

絶対パスとは, ルートディレクトリ / から始まるパスのことです. 例えば, この資料を作っているプロジェクトの絶対パスは, /Users/kazuharu/github/workshop-graduate-2026 になります.

相対パス

相対パスとは, カレントディレクトリからの相対的な位置を表すパスです. たとえば, examples サブディレクトリに移動するには, 絶対パスでは /Users/kazuharu/github/workshop-graduate-2026/examples と書く必要がありますが, 相対パスなら単に examples と書くだけで済みます.

コマンド

絶対パスも使えますが, 相対パスと特殊記号を組み合わせる方が扱いやすいです. 特殊記号には, ホーム (~), カレントディレクトリ (.), 親ディレクトリ (..) があります.

cd examples    # examples サブディレクトリへ入る
cd ../..       # 2つ上のディレクトリへ戻る
pwd            # いまどこにいるか確認

ここで一つ注意があり, ディレクトリ名に空白が含まれていると厄介です. たとえば My Documents というフォルダに cd My Documents としても動きません. Bash は空白を区切りとみなし, MyDocuments を別々の引数だと解釈してしまうからです.

  • 対処法1: 引用符で囲む. cd "My Documents"
  • 対処法2: 空白をエスケープする. cd My\ Documents (Tab 補完を使うと自動でこの形になります)
  • 一番よい対処法: そもそもファイル名・フォルダ名に空白を使わない

一覧表示する

ls はディレクトリの中身を一覧表示します. -lh オプション (長い形式, 人間に読みやすい単位) を付けると, 各オブジェクトの属性まで見えます.

ls -lh examples

出力の各行は, 次のような情報を持っています.

drwxr-xr-x 2 kazuharu users 4.0K Jan 12 22:12 ABC

これを左から順に読み解くと, 次のようになります.

  • 先頭1文字はオブジェクトの種類です. d (ディレクトリ), l (リンク), - (ファイル).
  • 続く9文字はパーミッション (権限) です. 3文字ずつ, 所有者, 所有者のグループ, その他のユーザー の順に並びます.
    • 各文字は r (読み取り), w (書き込み), x (実行) の可否を表します. - はその権限が無いことを示します.
  • 次の数字はハードリンクの数ですが, あまり重要ではありません.
  • その後に, オブジェクトの 所有者グループ が続きます. グループの名前は環境によって変わり, この例の users の位置が, macOS では staff, Ubuntu (WSL) ではユーザー名と同名のグループになります.
  • 最後に, サイズ, 作成日時, 名前が並びます.

パーミッションと所有者については, 後の節で詳しく説明します (Section A.8).

作る・消す

シェルで最もよく行う作業の一つが, ファイルやディレクトリの作成です.

  • mkdir: ディレクトリを作る.
  • touch: 空のファイルを作る.
mkdir testing
touch testing/test1.txt testing/test2.txt testing/test3.txt
ls testing

消すときは次を使います.

  • rm: ファイルを消す.
  • rmdir: 空のディレクトリを消す.
rm testing/test1.txt
rmdir testing       # 中身が残っているとエラーになる

rmdir は中身の入ったディレクトリを消せません. そのときは rm に「再帰的」(-r) と「強制」(-f) のオプションを付けます.

rm -rf testing      # 中身ごと消す
Warning

rm -rf はゴミ箱を経由せず, 確認もなく即座に消します. パスを間違えると取り返しがつかないので, 実行前に必ずパスを見直してください.

コピー・移動・名前変更

cp はコピー, mv は移動です.

cp reps.txt copies/reps-copy.txt    # 新しい名前でコピー
cp -r meals/ copies/                # ディレクトリは -r で中身ごとコピー
mv ABC/abc.txt examples             # abc.txt を examples へ移動

同じディレクトリ内で新しい名前を付けて「移動」すると, それは実質的にリネームになります.

mv reps-copy.txt reps2.txt          # 移動 = リネーム

多数のファイルをまとめてリネームしたいときは, rename が便利です. パターンと置換文字列, 対象ファイルを指定します. ワイルドカード (次節) と組み合わせると真価を発揮します.

rename csv TXT meals/monday.csv     # 1つのファイルの拡張子を変える
rename csv TXT meals/*              # meals 内のすべてに適用

ワイルドカード

ワイルドカード は, 他の文字の代わりになる特殊文字です. 重要なのは次の2つです.

  • *: 任意の長さの文字列にマッチします. ファイルをまとめて操作したいときに便利です.
  • ?: 任意の1文字にマッチします. 似た名前のファイルを区別したいときに便利です.
cp *.sh copies          # 拡張子が .sh のファイルをすべてコピー
rm copies/*             # copies の中身をすべて削除
ls meals/??nday.csv     # monday.csv, sunday.csv にマッチ
ls meals/?onday.csv     # monday.csv にマッチ

探す

ナビゲーション関連で最後に紹介するのが find です. 名前やサイズなど, さまざまな条件でファイルやディレクトリを探せます.

find examples -iname "monday.csv"   # 名前で検索 (再帰的, 大文字小文字を無視)
find . -iname "*.txt"               # カレント以下の .txt をすべて
find . -size +100k                  # 100 KB より大きいファイル

A.4 テキストファイルの操作

経済学を含むデータサイエンスでは, スクリプト, Markdown, CSV など, テキストファイルを扱う時間が長いです. シェルはテキスト処理がとても得意なので, ここで代表的な道具を紹介します.

数える

wc (word count) は, 行数・単語数・文字数を数えます.

wc sonnets.txt

なお文字数は手で数えるより多くなります. wc は目に見えない改行文字 \n も1文字として数えるからです.

読む

最も単純にテキストを読むコマンドが cat (concatenate) です. ただし cat は全文を一気に表示するので, 長いファイルでは扱いにくいです.

cat -n sonnets.txt      # -n で行番号を付ける

長いファイルを1画面ずつ読むには moreless を使います. f / b で前後に移動し, q で終了します.

先頭や末尾だけを覗くなら headtail が便利です (既定では10行).

head -n 3 sonnets.txt       # 先頭3行
tail -n 1 sonnets.txt       # 末尾1行
tail -n +3024 sonnets.txt   # 3024行目以降をすべて

探す

テキストの中からパターンを探すには grep を使います. 正規表現によるマッチングができます.

grep -n "Shall I compare thee" sonnets.txt    # -n で一致した行番号も表示

grep はディレクトリ内の複数ファイルにまたがって探すこともできます. 関数名を含むファイルを探す, といった場面で特に役立ちます.

grep -Rl "pasta" meals    # -R 再帰的, -l 一致したファイル名だけを表示

-i (大文字小文字を無視) など, 便利なオプションが他にもあります.

加工する

テキストを加工する代表的なコマンドが sedawk です. どちらも強力で柔軟ですが (特に awk は CSV に強いです), ここでは基本だけ示します.

ある文字列を別の文字列に置換するには, sed を使います.

sed -i 's/Jack/Bill/g' nursery.txt    # ファイル内の Jack をすべて Bill に

複数のコマンドをつなげれば, より複雑な処理もできます (パイプ | は次節で説明します). たとえば, シェイクスピアのソネットで最もよく使われる単語の上位10件は, 次のように求められます.

sed -e 's/\s/\n/g' < sonnets.txt | sort | uniq -c | sort -nr | head -10

並べ替え・重複削除

sort は行を並べ替えます. -u (unique) を付けると, 重複行を取り除けます.

sort -u reps.txt        # 並べ替えつつ重複を削除
sort -ur reps.txt       # -r で逆順

A.5 リダイレクトとパイプ

リダイレクト

> を使うと, コマンドの出力を画面ではなくファイルへ送れます. これを リダイレクト (redirect) といいます.

echo "first line"               # 画面に表示
echo "first line" > note.txt    # note.txt に書き出す

既存のファイルに 追記 したいときは >> を使います. > は上書きしてしまうので注意してください.

echo "second line" >> note.txt
cat note.txt

私はこれを .gitignore への追記によく使います.

echo "*.csv" >> .gitignore

パイプ

| (パイプ) は Bash で最も強力な機能の一つです. あるコマンドの出力を, 次のコマンドの入力として渡します. 単純な操作を数珠つなぎにして, 複雑な処理を組み立てられます (まさに Unix 哲学です).

cat -n sonnets.txt | head -n 100 | tail -n 10    # 91-100行目を表示

たとえば「336行目から始まる14行のソネットだけを取り出す」なら, 次のように書けます.

tail -n +336 sonnets.txt | head -n 14

コマンド履歴の検索にも使えます. 入力したコマンドは ~/.bash_history (zsh なら ~/.zsh_history) に残るので, 次のようにたどれます.

cat ~/.bash_history | grep head

A.6 繰り返し (for ループ)

for の構文

Bash の for ループは, 他の言語のものとよく似ています. 基本の構文は次のとおりです.

for i in LIST
do
  OPERATION $i    # $ は変数を表す
done

セミコロン ; を使えば1行にまとめられます.

for i in LIST; do OPERATION $i; done

例: 数列を表示する

for i in 1 2 3 4 5; do echo $i; done

長い数列は, ブレース展開 {1..n} を使うと書かずに済みます.

for i in {1..5}; do echo $i; done

例: 複数の CSV を結合する

私が実際によく使う, より現実的な例を示します. meals ディレクトリにある曜日ごとの CSV を, 1つの mealplan.csv にまとめたいとします. 素朴にやると, 空のファイルを作り, 各ファイルの中身を追記していくことになります.

touch mealplan.csv
for i in $(ls meals/*day.csv)
do
  cat $i >> mealplan.csv
done

ただしこれだと, 各ファイルの見出し行 (ヘッダー) が繰り返し入ってしまいます. そこで, 最初に1つのファイルからヘッダーだけを書き出し, 残りは2行目以降だけを追記するようにします.

head -1 meals/monday.csv > mealplan.csv       # ヘッダーだけ書き出す
for i in $(ls meals/*day.csv)
do
  tail -n +2 $i >> mealplan.csv               # 2行目以降を追記
done

シェルで結合する利点は, 効率の良さ にあります. すべてのファイルを同時にメモリ (RAM) に載せる必要がないので, ファイル数が多いとき, あるいは1つ1つが巨大なときに大きな差が出ます.

A.7 スクリプト

Hello World

データやファイル構造を探索するときは, シェルに対話的にコマンドを打つのが理にかなっています. 一方で, 一連のコマンドをまとめた シェルスクリプト を書いておくと, 再現可能になります. シェルスクリプトは .sh という拡張子で表します.

たとえば次のような短いスクリプト hello.sh を考えます.

hello.sh
#!/bin/sh
echo -e "\nHello World!\n"
  • 1行目の #!/bin/shシバン (shebang) と呼ばれ, どのプログラムで実行するかを指定します.
  • 2行目が実際に実行したいコマンドです. echo-e はエスケープ文字 (\n = 改行) を解釈させるオプションです.

実行するには, ファイル名を打つか, bash に渡します.

bash hello.sh

Rscript

シェルで実行できるのはシェルスクリプトだけではありません. 同じ仕組みは他のプログラムにも当てはまります. この授業で特に重要なのが, R スクリプトを実行する Rscript です.

Rscript -e "cat('Hello World, from R!')"

より典型的な使い方は, R スクリプトのファイルを丸ごと実行することです. このとき, シェルから R へ追加の引数を渡せると便利な場面が多いです. R 側では, スクリプトの冒頭で次のように引数を受け取ります.

hello.R
args <- commandArgs(trailingOnly = TRUE)
i <- args[1]
j <- args[2]

こうしておくと, シェルからファイル名の後ろに引数を並べて渡せます.

Rscript hello.R 12 9

引数を渡すかどうかは任意ですが, 大きなジョブをまとめて回す (バッチ処理) ようなときに役立ちます.

A.8 権限とパーミッション

スーパーユーザー: root と sudo

Linux には大きく2種類のユーザーがいます.

  • 通常のユーザー
  • スーパーユーザー (root とも呼ばれます)

違いは権限です. スーパーユーザーはシステムの変更, ソフトウェアのインストール, 他ユーザーのフォルダの閲覧などができます. 通常のユーザーにできることはずっと限られています. Unix 系 OS がウイルスなどの脅威に比較的強いのは, (悪意ある) ソフトウェアのインストールにスーパーユーザー権限が要るからです.

では, 通常のユーザーはどうやってソフトのインストールのような操作をするのでしょうか. 答えは sudo (“superuser do”) を使うことです. 実行したいコマンドの前に sudo を付けると, 一時的にスーパーユーザーの権限で実行できます.

ls /root          # 失敗する
sudo ls /root     # 成功する

root として常時ログインすることも一応できますが, 安全装置も「元に戻す」機能も無いため, 一般に悪い習慣とされています.

パーミッションを変える: chmod

先ほど ls -lh で見た ABC ディレクトリを思い出しましょう.

drwxr-xr-x 2 kazuharu users 4.0K Jan 12 22:12 ABC

このパーミッションを変えるのが chmod です. パーミッションの表し方には2通りあります.

8進数表記 (octal) は, 4 (読み取り), 2 (書き込み), 1 (実行) を足し合わせます.

  • 7 = 4 + 2 + 1 で, 読み・書き・実行のすべて.
  • 5 = 4 + 0 + 1 で, 読みと実行のみ (書き込み不可).
  • 所有者・グループ・その他の3者ぶんの数字を並べます.

記号表記 (symbolic) は, 記号で指定します.

  • 対象: u (所有者), g (グループ), o (その他), a (全員)
  • 権限: r (読み), w (書き), x (実行)
  • 変更: + (追加), - (削除), = (設定)

よく使う権限を並べると, 次のようになります.

8進数 記号 パーミッション 意味
777 a=rwx rwxrwxrwx 全員が読み・書き・実行
755 u=rwx,go=rx rwxr-xr-x 所有者は全権, 他は読みと実行
700 u=rwx,go= rwx—— 所有者のみ全権
644 u=rw,go=r rw-r–r– 所有者は読み書き, 他は読みのみ

たとえば ABC ディレクトリを全員フルアクセスにするなら, 次のいずれでも構いません (ディレクトリなので再帰的に -R を付けます).

chmod -R 777 ABC        # 8進数表記
chmod -R a=rwx ABC      # 記号表記

所有者を変える: chown

所有者の変更は chown で行います. 8進数と記号の対応を覚える必要がない分, chmod より簡単です.

chown -R alice ABC      # ABC の所有者を alice にする

演習問題

この章の演習は, クイズではなく実際にターミナルでコマンドを打ちながら進めます. VSCode の統合ターミナル (Ctrl + `) を開いてください. Windows の人は, VSCode で WSL を開いた状態にしておけば, 同じショートカットで Bash のターミナルが開きます.

まず, 練習用のデータをダウンロードします. 架空の売上データの CSV 6年分をこちらで用意してあるので, wget で取得してホームディレクトリに展開してください.

cd ~
wget https://kazuyanagimoto.com/workshop-graduate-2026/static/data/shell-practice.tar.gz
tar -xzf shell-practice.tar.gz    # x: extract, z: gzip, f: file
cd shell-practice

wget は URL からファイルをダウンロードするコマンドで, tar はアーカイブをまとめたり展開したりするコマンドです. サーバーにデータを持っていくときにも頻出のコンビです. macOS には wget が入っていないことがあるので, その場合は wget の行を curl -LO https://... (URL は同じ) に読み替えてください.

各問題は前の問題の結果を前提にしているので, 上から順に取り組んでください. 解答例は畳んであるので, 開く前にまず自分で試しましょう.

data ディレクトリの中身を, サイズやパーミッションが見える形で一覧表示してください. CSV ファイルは何個あり, あなたには書き込み権限があるでしょうか.

ls -lh data

ファイルは6個です. 各行の先頭が -rw- で始まっていれば, 所有者 (あなた) に読み取りと書き込みの権限があります.

2020年代のファイル (sales_2020.csv から sales_2024.csv) だけを, ワイルドカードを使って一覧表示してください.

ls data/sales_202?.csv

? は任意の1文字にマッチするので, 2020 から 2029 だけが対象になります. ls data/sales_202*.csv でも同じ結果です.

data ディレクトリを中身ごと backup という名前でコピーし, その中の sales_2019.csvsales_old.csv にリネームしてください. 最後に backup の中身を一覧表示して確認しましょう.

cp -r data backup
mv backup/sales_2019.csv backup/sales_old.csv
ls backup

ディレクトリのコピーには -r が必要でした. リネームは mv で「同じ場所に別名で移動」します.

durian という果物は, どの年のファイルに入っているでしょうか. ファイルを1つずつ開かずに, コマンドで探してください. 見つかったら, そのファイルの何行目にあるかも表示しましょう (backup にもコピーが入ってしまったので, data だけを対象にしてください).

grep -Rl "durian" data
grep -n "durian" data/sales_2024.csv

1つ目のコマンドで data/sales_2024.csv だけが表示され, 2つ目で5行目にあることがわかります.

data の6ファイルすべてを連結し, 重複行を取り除くと何行になるでしょうか. 3つのコマンドをパイプでつないで, 1行で数えてください. 出てきた数字がなぜその値になるのかも考えてみましょう.

cat data/*.csv | sort -u | wc -l

答えは5行です. 各年のファイルはヘッダーと3つの行がまったく同じで, 違いは2024年だけにある durian の行だからです (ヘッダー1 + 果物3 + durian 1). wc -l を外して実行すると内訳が見えます.

data の6ファイルを, ヘッダーが先頭に1回だけ入るように all_sales.csv に結合してください (本文の「複数の CSV を結合する」の例が参考になります). できたら行数を確認しましょう. ヘッダー1行 + データ19行で20行になれば成功です.

head -1 data/sales_2019.csv > all_sales.csv
for i in data/sales_*.csv
do
  tail -n +2 $i >> all_sales.csv
done
wc -l all_sales.csv

grep -c "id,fruit,price" all_sales.csv の結果が 1 になっていれば, ヘッダーの重複もありません.

data にある CSV ファイルの数を表示する」シェルスクリプト report.sh を, エディタを使わずに echo とリダイレクトだけで作って実行してください. さらに, 実行権限を与えて ./report.sh の形でも実行できることを確認しましょう.

echo '#!/bin/sh' > report.sh
echo 'ls data/*.csv | wc -l' >> report.sh
bash report.sh
chmod u+x report.sh
./report.sh

どちらの実行方法でも 6 と表示されます. chmod の前に ls -lh report.sh と後で見比べると, 所有者の権限に x が増えたことを確認できます.

演習はここまでです. shell-practice と, ダウンロードした shell-practice.tar.gz を削除して片付けましょう. rm -rf を使う前に, いま自分がどこにいて, 何を消そうとしているのかを必ず確認してください.

cd ~
pwd                     # make sure you are in your home directory
ls shell-practice       # make sure this is the sandbox you want to delete
rm -rf shell-practice shell-practice.tar.gz

rm -rf はゴミ箱を経由せず即座に消すのでした. 消す対象を先に ls で見てから実行する癖をつけると安全です.