アプリ開発者のための
UNIXの基礎

Yusuke Shinyama

概要: この記事では、アプリ開発者の役に立ちそうな UNIX/Linux の使い方に関する最低限の知識をまとめた。

目的: この記事を理解すると、まとめ問題に答えることができる。 (逆に、現時点でこれができる人には本記事は不要。)

目次

  1. 実行環境の準備
  2. UNIXとは
  3. ファイルシステム
  4. プロセス
  5. 標準入力・標準出力・標準エラー出力
  6. シェル
  7. 環境変数
  8. ファイル操作コマンド
  9. シェルスクリプトの基礎
  10. 条件分岐・繰り返し
  11. ワンライナー (one-liner) を書く時の注意点
  12. おすすめの書籍

0. 実行環境の準備

必要に応じて、次のどれかの環境を使うこと:

1. UNIXとは

1970年ごろに作られたオペレーティングシステム (OS)。 以後の多くの OS が UNIX を継承あるいは模倣している。 以下ではこれらの「UNIX風OS」も含めて UNIX と呼称する。

UNIX v7 Windows Linux FreeBSD NetBSD OpenBSD Solaris HP-UX macOS 模倣 模倣

従来の (UNIXでない) OS と比べると、UNIX は非常に単純であった:

1.1. オペレーティングシステム (OS) とは?

コンピュータの基本的な処理 (ディスク操作、 ネットワーク処理、画面表示など) を提供する基本ソフトウェア。 ほとんどの人は、このオペレーティングシステムを使った アプリケーション (応用ソフトウェア) を書いている。 実際には、アプリはコンピュータで動いているソフトウェア全体のごく一部である。 また、OS は多くの仮想化処理 (ファイルシステム、遠隔操作、同時並列処理) を実現している。

オペレーティングシステム (OS) アプリ アプリ アプリ

1.2. Linuxディストリビューションとは?

Linuxの世界では、実際には "RedHat", "Ubuntu", "Debian" といった 複数の異なる "Linux" が存在している。これらは厳密には Linux ではなく、 異なる Linux の ディストリビューション と呼ばれている。 本来 "Linux" は OSの核となる部分 (カーネル) をさしており「生の Linuxだけを使う」 ということはあり得ない。核となる Linux カーネルにさまざまな部品 (ファイルシステム、パッケージ管理など) を追加して一般的な OS として 使えるようにしたものが「ディストリビューション」である。

Linuxの各ディストリビューションのおもな違いはファイルシステムの構造の差異 (/etc以下にある設定ファイルの違いなど) およびシステム管理の方法の差異である。 とくに、パッケージ管理方式は各ディストリビューションによって大きく違っている。 しかしこの記事では UNIXのシステム管理の方法までは扱わないので、 内容の大部分はどのディストリビューションにもあてはまる。

2. ファイルシステム

UNIXは階層型ファイルシステム (hierarchical filesystem) という概念を導入している。 これはすべてのファイル・フォルダはひとつの巨大な木構造 (tree) をなしている。 また、UNIXでは「すべてはファイルである」という考え方をおしすすめている。 そのため、メモリや画面 (端末) も memtty といった 特殊ファイルとして表現されている。 なお、 UNIXではフォルダのことを ディレクトリ (directory) と呼ぶ。

2.1. パス名とは

ファイルシステム中のあらゆるファイルの位置は、 パス名 (path) で表すことができる:

ディスク A B C B E C K H K X /
演習 1. パス名の練習
  1. ディレクトリ X のパス名を書け。
  2. ファイル H のパス名を書け。
  3. ファイル C のパス名を 2つ書け。

UNIXにおける「お約束」パス名

UNIX では、いくつかのパス名は 「お約束」として決められている。

演習 2. やってみよう

UNIXでは、各ファイルには以下のようなメタデータが付与されている:

2.2. カレント・ディレクトリ (カレント・フォルダ) とは

A B C B E C K H K X カレント・ディレクトリ

2.3. 絶対パス名と相対パス名

実は「パス名」と呼ばれているものには 2つの種類がある。 上で説明した「パス名」は「絶対パス名」のことであった。

A B C B E C K H K X カレント・ディレクトリ 目的のディレクトリ

つまり、上のディレクトリ X の位置は、次の2通りで表せる:

2.4. 相対パス名の表し方

演習 3. 相対パスの練習

カレント・ディレクトリが E のとき…

A B C B E C K H K X カレント・ディレクトリ
  1. ディレクトリ A への相対パス名は?
  2. ファイル H への相対パス名は?
  3. ファイル K への相対パス名は? (2つある)
演習 4. やってみよう

3. プロセス

ファイルシステム上で「実行可能 (executable)」フラグ (x) がついている ファイルはプログラムとして実行可能である。実行されたプログラムは、システム上で一定時間 プロセス (process) として走り続ける。UNIXはマルチタスクOSなので、通常は 複数のプロセスが並列に実行される。とはいえ実際の CPU は一度にひとつの プログラムしか実行できないため、各プロセスは高速に切り替えられ 少しずつ (10ms程度) 実行される (時分割処理)。 これらプロセス切り替え処理は、OS の カーネル (kernel) という部分がおこなう。

UNIXプロセスの特徴

UNIX 起動時には、まず最初のプロセスである init (プロセスID: 1) が実行される。ここから他のすべてのプロセスが起動される。 UNIX の各プロセスは、init から先祖-子孫関係が存在し、 全体として木構造を形成している。

$ pstree
systemd─┬─agetty
        ├─automount───3*[{automount}]
        ├─avahi-daemon───avahi-daemon
        ├─crond
        ├─dbus-daemon
        ├─nmbd
        ├─ntpd───{ntpd}
        ├─smbd─┬─cleanupd
        │      ├─smbd
        │      └─smbd-notifyd
        ├─sshd
       ...
演習 5. やってみよう

実行中のプロセスを止めるには

UNIX のプロセスには、シグナル (signal) を送ることができる。 シグナルにはいくつかの種類がある:

演習 6. やってみよう

4. 標準入力・標準出力・標準エラー出力

UNIX の各プロセスには 「標準入力 (stdin)」 「標準出力 (stdout)」 「標準エラー出力 (stderr)」 という 3つの仮想的な装置が付属している。 これらはプロセスが画面に文字を表示したり、 キーボードから文字を入力するために使用する。

まず、通常の状態では、プロセスの 標準入力・標準出力・標準エラー出力は、 どれも「端末 (画面)」に接続されている。 (昔はこれはタイプライタや電話線だったが、現在ではおもに ウィンドウ上の仮想端末が使われる。)

プロセス 標準入力 標準出力 標準エラー出力 端末 端末 端末

標準出力は、プロセスからの出力を表示するのに使われる:

$ ./hello
hello, world.

いっぽう標準入力は、ユーザが文字を入力するのに使われる:

$ ./greet
your name?
euske
greetings, euske

さて UNIX では、後述するシェルの機能を使うと、 標準出力を端末ではなくファイルに切り替える (リダイレクトする) ことができる:

$ ./hello > output.txt
(テキストファイル output.txt が生成される)
hello 標準入力 標準出力 標準エラー出力 端末 output.txt 端末

また、標準入力を端末ではなくファイルにリダイレクトすることも 可能である:

(テキストファイル input.txt を作成する)
$ ./greet < input.txt
your name?
greetings, test
greet 標準入力 標準出力 標準エラー出力 input.txt 端末 端末

さらに、UNIX には「パイプ (pipe)」という機能がある。 これを使うと 「あるプロセスの標準出力を、別のプロセスの標準入力に」 リダイレクトすることができる:

$ ./hello | ./greet
your name?
greetings, hello, world.
hello 標準入力 標準出力 標準エラー出力 端末 パイプ 端末 greet 標準入力 標準出力 標準エラー出力 端末 端末
演習 7. やってみよう

パイプによる複数プロセスの接続は UNIX (シェル) の特徴的な機能のひとつである。 これをうまく使うと、複雑な処理をいくつかのコマンドの組み合わせによって 実現することができる。

(ls コマンドの出力を検索し、さらにそれをソートして最初の10行を表示する)
$ ls -l | grep euske | sort | head -n10
注意: <> による 標準入力・標準出力の切り替えは、 プロセスを起動する瞬間にしか指定できない。 いちどプロセスが起動してしまうと、あとから 切り替えることはできないので注意。
注意: UNIX は無愛想な OS なので、 > の出力先として うっかり存在するファイル名を指定してしまうと、 そのファイルは何の警告もなく上書きされ、 空のファイルされる。

5. シェル

以上に挙げたような各プロセスの起動、標準入力・標準出力の切り替えなどを おこなうプログラムがシェル (shell) である。 シェルは UNIX を使ううえで中心的な役割を果たしている。 UNIX 上で「コマンド (command)」と呼ばれるものは、 実はほとんどシェルによって起動されるプロセスである。 しかもたいていのコマンド (プロセス) は数ミリ秒〜数秒しか生存しない。 通常の PC では、アプリ (これもプロセス) は一度起動したら 長時間走らせておくのが普通だが、 UNIX は「湯水のようにプロセスを消費する」OSである。

シェルの役割には、以下のものがある:

実は UNIX では、シェルも普通のプロセスである。 そのため (sh, csh, bash, zsh など) いろいろなシェルが開発されている。 ほとんどの場合、シェルは以下の動作を繰り返しているだけである:

  1. 端末 (標準入力) からコマンド文字列が入力されるのを待つ。
  2. 文字列を解析し、スペースで区切られた引数のリストを作成する (引数展開)。
  3. 必要に応じて標準入力・標準出力・標準エラー出力を切り替えて、子プロセスを起動する。
  4. 子プロセスが終了するのを待ち、終了状態を受けとる。
  5. 1. に戻る。

5.1. コマンド引数の展開

先に述べたように、UNIX コマンドのほとんどは実はプロセスであり (例外も存在する)、 シェルの主な機能はコマンド文字列を解析し、プロセスを起動することである。 たとえば

$ ls -l /etc
という行は、以下の行と同じである:
$ /bin/ls -l /etc

この行が入力されたとき、シェルは以下のことをおこなう:

  1. /bin/ls というプログラムを子プロセスとして起動する。
  2. このとき、各引数を文字列としてプロセスに渡す:
  3. 子プロセスが終了するまで待つ。

ここで注意したいのは、引数である 「-l」や「/etc」をどのように利用するかは 各コマンド次第ということである。 すべてのコマンドにおいて 「-l」がオプションであると決まっているわけではないし 「/etc」がパス名として解釈されるとも限らない。 この意味で、UNIX の使い方を学ぶことは API の使い方を学ぶのに似ている。 個々の関数・メソッドの引数がどのような意味をもつのか学習し、 それらを組み合わせて必要な処理を実現する。

注意: もし実際に 「-l」という名前のディレクトリがあったとすると、 そのままでは ls のパス名として指定できない。 だが ls ./-l というトリックを使えば指定できる。)

たとえば echo というコマンドは、シェルから与えらえた引数をただそのまま 表示するだけのコマンドである:

$ echo abc 1234
abc 1234
$ echo -l /etc
-l /etc

一般にシェルの引数は「 」(スペース) で 区切られるが、"〜" で囲むことにより スペースが入っている文字列を「ひとつの引数」として認識させることができる:

$ ls -l /etc
…
$ ls "-l /etc"
ls: invalid option -- ' '
$ "ls -l" /etc
ls -l: command not found
演習 8. シェルの引数展開

以下のコマンドラインの引数を 0番目からすべて挙げよ:

  1. ls /etc /bin
  2. ls "/etc /bin"
  3. ls "/etc" "/bin"
  4. ls "" "/bin"
  5. "ls /etc /bin"

5.2. ファイル名の展開

シェルでは、ディレクトリ上に複数のファイルがあるとき、 それらのファイル名を複数の引数として展開する機能がある (ファイル名展開, filename expansion)。

たとえば、カレント・ディレクトリ内に a, bb, ccc というファイルがあるとき…

$ ls
a   bb   ccc
以下の * (ワイルドカード) を指定すると
$ cat *
以下の引数を与えたのと同じである:
$ cat a bb ccc

ワイルドカードには、パターンを指定することもできる。 たとえば /etc 内に多くの「.conf」で終わるファイルがあるとき…

$ ls -l | grep .conf
-rw-r--r--    1 root     root          5613 Jun 18  2020 ca-certificates.conf
drwxr-xr-x    2 root     root           520 Jul  5  2020 conf.d
-rw-r--r--    1 root     root          1102 Jul  6  2020 dhcpcd.conf
…
以下のパターンは
$ cat /etc/*.conf
以下のような引数に展開される:
$ cat /etc/ca-certificates.conf /etc/dhcpcd.conf

繰り返すが、ここで展開されたパス名をどのように扱うかは 各コマンド次第である。 シェルがファイル名展開したからといって、 各コマンドがこれらをファイル名として扱う保証はない。

ここで、echo コマンドは シェルが各引数をどのように展開したかを表示するのに使える:

$ echo /etc/*.conf
/etc/ca-certificates.conf /etc/dhcpcd.conf …
演習 9. ファイル名展開

/etc 以下に次のようなファイルがあるとする:

カレント・ディレクトリも /etc であるとして、 以下のコマンドラインの出力を答えよ:
  1. echo *
  2. echo *.conf
  3. echo *.conf /etc/*
  4. echo /etc/issue*
  5. echo "*"

Dockerの原理

各プロセスに所属する Control Group (cgroup) という機能を使うと、 あるプロセス (およびその子孫プロセスすべて) からアクセスできる ファイルやネットワークを制限することができる。

つまるところ、docker がやっているのは、ある Control Group 内で プロセスを起動し、それ以降のすべての子プロセスを 特定の空間に閉じ込める (containする) ことによって 各プロセスが独立したマシン上で動いているかのように 見せかけているだけなのである。

5.3. コマンド出力の展開

さらにシェルは、あるコマンドが (標準出力に) 出力した文字列を まるごと引数として展開することができる。

たとえば、ls コマンドが以下のような出力をしたとする:

$ ls
a   bb   ccc

このコマンドの出力は a, bb, ccc という 3つの引数に展開される:

$ echo $(ls)
a bb ccc

ここでコマンドの出力からは、余分な空白が除かれていることに注意。 これは通常のコマンドライン引数で

$ echo a   bb   ccc
a bb ccc
とタイプしたのと同じことである。 標準出力全体をひとつの文字列として扱うには、 コマンド出力の展開部分を "〜" で囲めばよい:
$ echo "$(ls)"
a
bb
ccc

6. 環境変数

UNIX では、各プロセスに環境変数 (environment variable) というものが 付属している。これは各種の設定を保持しておくための文字列型の変数で、 プログラムの変数と同様に、好きな数だけ作成することができる。

現在の環境変数一覧を見るためには envコマンドを使う:

$ env
SHLVL=3
HOME=/root
OLDPWD=/root
PAGER=less
…

順序がばらばらで見にくいので sortで並べ変えてみる:

$ env | sort
HOME=/root
OLDPWD=/root
PAGER=less
…
注意: Windowsの環境変数と違い (実際にはWindowsもそうなのだが)、 UNIXにおける環境変数は各プロセスごとに独立している。 あるプロセスの環境変数を別のプロセスの環境変数にすることはできない。 ただし、親プロセスの環境変数は、子プロセスに引きつがれるため、 最初に共通の環境変数を設定しておけば、以後すべての子プロセスで 共通の設定を利用することができる。
親プロセス 子プロセス 子プロセス USER=yusuke USER=yusuke DATABASE=alpha USER=yusuke DATABASE=beta

6.1. 環境変数を使う

各プロセスにおける環境変数は、 プログラム中のどこからでも文字列値として アクセス可能である。 たとえば、環境変数 USER の値を取得したいとき:

Node (JavaScript) の場合

const user = process.env['USER']

Java の場合

String user = System.getenv("USER");

Kotlin の場合

val user: String? = System.getenv("USER")

慣例により、環境変数の名前には大文字が使われることが多い (実際には、英数字であればなんでもよい)。

シェルの場合

シェルでは、コマンドライン引数に含まれる環境変数の値が展開される。 たとえば環境変数 USEReuske という値が入っている場合、

$ ls /home/${USER}/foo
は、以下のように展開される:
$ ls /home/euske/foo

echoコマンドは、環境変数の内容を確認するためによく使われる:

$ echo ${USER}
euske

未定義の環境変数を展開してもエラーにはならず、空文字列として扱われる:

$ echo /home/${USSR}/foo
/home//foo

なお、${〜}の中カッコは省略可能である:

$ echo $USER
euske

高度な機能

シェルには変数を展開するさいにさまざまな演算をおこなう機能がある。 たとえば

$ echo ${USER/xxx/yyy}  (USER中の xxx を yyyに置換した文字列に展開される)
$ echo ${LANG:-ja}      (LANGが定義されていない場合、デフォルト値としてjaに展開される)
演習 10. 環境変数の展開

環境変数 USERjon という文字列が、 環境変数 FILEathan という文字列が、 入っているとき、以下のコマンドの出力を答えよ:

  1. echo /home/${USER}/${FILE}
  2. echo $USER$FILE
  3. echo "$USER"ny
  4. echo $USERny

6.2. 環境変数を設定する

各プロセスの環境変数は、一般にそのプロセスの起動時にだけ設定できる。 プロセスの起動時に環境変数を設定するには、実行したいコマンド引数の前に 「変数名=」のような形式を追加する。 たとえば date コマンドは、 環境変数 TZ の値によって異なる時間帯の時刻を返す:

$ date
Mon Oct 31 06:56:48 JST 2022
$ TZ=UTC date
Sun Oct 30 21:56:48 UTC 2022
$ TZ=America/New_York date
Sun Oct 30 17:56:48 EDT 2022

また、親プロセスが持っている環境変数はすべて子プロセスに継承される。 そのため親プロセスに環境変数を設定しておけば、すべての子プロセスで共通の値が使える:

$ echo $XYZ       (環境変数XYZは未定義)

$ XYZ=abc123 zsh  (環境変数XYZにabc123を設定して zshを起動)
$ echo $XYZ       (環境変数XYZに値が入っている)
ABC123

例外はシェルで、シェルの中ではいつでも環境変数を作成・変更できる。 実際には、環境変数はより一般的なシェル変数 (shell variable) の一部として扱われる。 シェル変数の一覧 (環境変数も含む) を表示するには setコマンドを使う:

$ set
BASH=/bin/bash
COLUMNS=100
DISPLAY=:1
EDITOR=vi
…

シェル変数は、ただ 「変数名=」のように書けば 値を設定できる。

$ XYZ=abc123  (シェル変数XYZにabc123を設定)
$ echo $XYZ   (シェル変数XYZの値を表示する)

ただし、すべてのシェル変数が環境変数として扱われるわけではないことに注意。 環境変数として扱われるのは、シェル変数のうち 「export属性」をもつものだけである。 Export属性をもったシェル変数は、何も指定しなくても 以後シェルから起動したすべてのプロセスに環境変数として渡される:

$ XYZ=abc123  (シェル変数XYZにabc123を設定)
$ echo $XYZ   (シェル変数XYZの値を表示する)
abc123
$ env         (XYZは環境変数ではない)
…
$ export XYZ  (XYZにexport属性をつける)
$ env         (XYZが環境変数として渡されている)
…
XYZ=abc123
…
シェル 変数 環境変数

6.3. 重要な環境変数: PATHとHOME

UNIX の環境変数のなかでも PATHHOME は とくに重要である。

環境変数 PATH

通常、UNIX シェルでは第0引数としてコマンド (プログラム) のパス名を指定する:

$ /bin/ls -l /etc
しかし実際には以下のように書いても動く:
$ ls -l /etc
これを可能にしているのは環境変数 PATH のためである。 環境変数 PATH の中身を見てみると、 コロン (:) で区切られたパス名 (ディレクトリ名) の一覧が含まれているのがわかる:
$ echo $PATH
/usr/local/bin:/usr/bin:/bin

これは「あるコマンドが実行されたとき、 /usr/local/bin/コマンド名/usr/bin/コマンド名/bin/コマンド名 のいずれかのプログラムを実行せよ」ということを表している。 PATH にはこのようなコマンドの「検索順序」が記録されている。 その証拠に、PATH をおかしな値にすると動かない:

$ PATH=xxx
$ ls
ls: command not found
$ /bin/ls

なお、PATH はよくシステムのデフォルト値をカスタマイズして 使うことが多い。PATH に独自のディレクトリを追加するには、以下のようにする:

PATH=$PATH:/home/euske/bin  (/home/euske/bin もコマンド検索順序に追加する)

環境変数 HOME

UNIX では、各ユーザのホームディレクトリは /home/ユーザ名 とされているが、 これはただの慣例であり、実際にはどこであってもかまわない。 環境変数 HOME は、そのユーザの所有する ホームディレクトリのパス名を指定する。 これは起動時には通常そのユーザのデフォルト値が設定されているが、 この値を変更することで、どんなディレクトリでもホームディレクトリとして 使用することができる:

$ echo $HOME  (環境変数HOMEの内容を表示する)
/home/euske
$ cd          (ホームディレクトリに移動)
$ pwd         (カレント・ディレクトリを表示)
/home/euske
$ HOME=/etc   (環境変数HOMEを変更する - ホームディレクトリが変更される)
$ cd          (ホームディレクトリに移動)
$ pwd         (カレント・ディレクトリを表示)
/etc

また、シェルのコマンドライン上では ~ が 自分のホームディレクトリに展開されるが、 この値も実際には環境変数 HOME を展開しているだけである:

$ echo $HOME/work
/home/euske/work
$ echo ~/work
/home/euske/work

UNIX では、各ユーザのデフォルトのホームディレクトリ (やシェル) は /etc/passwd というテキストファイル内に記載されている。 UNIX におけるホームディレクトリとは、つまるところ:

だけのものなのである。

7. ファイル操作コマンド

シェル上でファイル操作を行うためのおもなコマンドは以下のとおり:

ls
指定したディレクトリに含まれるファイル一覧を表示する。
$ ls(カレント・ディレクトリのファイル一覧を表示)
$ ls /etc(ディレクトリ /etc のファイル一覧を表示)
cd (Change Directory)
カレント・ディレクトリを変更する。
$ cd /etc
(カレント・ディレクトリを /etc に変更)
$ cd
(カレント・ディレクトリを自分のホームディレクトリに変更)
pwd (Print Working Directory)
カレント・ディレクトリのパス名を表示する。
$ pwd
(現在のパス名を表示)
catless
ファイルの内容を表示する。 もともと cat は 複数のファイルを連結 (conCATenate) するためのコマンドだった。
$ less /etc/services
(/etc/services の内容を表示)
$ cat /etc/hosts /etc/services
(/etc/hosts の内容と /etc/services の内容を連結して表示)
mkdir (MaKe DIRectory)
空のディレクトリを新規作成する。
$ mkdir foo
(カレント・ディレクト下に foo を作成)
cp (CoPy)
指定したファイルを宛先パス名に複製する。
$ cp a.txt b.txt
(a.txt を新しい b.txt という名前で複製する)
$ cp a.txt dest/
(a.txt を dest/ ディレクトリ内に同じ名前で複製する)
$ cp a.txt b.txt c.txt dest/
(a.txt, b.txt, c.txt の各ファイルを dest/ 内に同じ名前で複製する)
$ cp *.txt dest/
(〜.txt で終わるすべてのファイルを dest/ 内に同じ名前で複製する)

cp で複製できるのは、通常はファイルのみである。 あるディレクトリ内のファイルを (ディレクトリごと) 複製したい場合には -r オプションを使う:

$ cp -r foo dest/
(foo ディレクトリ内とその中のファイルをすべて dest/ 内に複製する)
注意: UNIX は無愛想な OS なので、 コピー先としてうっかり存在するファイル名を指定してしまうと、 そのファイルは何の警告もなく上書きされる。 これを防ぐためには -i オプションを使う:
$ cp -i a.txt b.txt
(a.txt を新しい b.txt という名前で複製するが、
 すでに同名のファイルがある場合は確認する)
mv (MoVe)
指定したファイルを宛先パス名に変更あるいは移動する。
$ mv a.txt b.txt
(a.txt を新しい b.txt という名前に変更する)
$ mv a.txt dest/
(a.txt を dest/ ディレクトリ内に移動する)
$ mv a.txt b.txt c.txt dest/
(a.txt, b.txt, c.txt の各ファイルを dest/ 内に移動する)
$ mv *.txt dest/
(〜.txt で終わるすべてのファイルを dest/ 内に移動する)
注意: UNIX は無愛想な OS なので、 移動先としてうっかり存在するファイル名を指定してしまうと、 そのファイルは何の警告もなく上書きされる。 これを防ぐためには -i オプションを使う:
$ mv -i a.txt b.txt
(a.txt を新しい b.txt という名前に変更するが、
 すでに同名のファイルがある場合は確認する)
rm (ReMove)
指定したファイルを削除する。
$ rm a.txt
(ファイル a.txt を削除する)
$ rm *.txt
(〜.txt で終わるすべてのファイルを削除する)

rm で削除できるのは、通常はファイルのみである。 あるディレクトリ内のファイルを (ディレクトリごと) 削除したい場合には -r オプションを使う:

$ rm -r dest/
(dest/ ディレクトリ内とその中のファイルをすべて削除する)
注意: UNIX は無愛想な OS なので、 rm コマンドは削除が成功しても何も表示しない。 削除するファイルをひとつずつ確認するためには -i オプションを使う:
$ rm -i *.txt
(〜.txt で終わる各ファイルを yes/no で確認しながら削除する)
du (DiskUse)
指定したディレクトリのファイル使用量を表示する。
$ du ~
(ホームディレクトリの使用量を各ディレクトリごとに表示する)
$ du -s .
(カレントディレクトリの総使用量のみを表示する)

パス名の指定方法に注意

cp, mv, rm などのコマンドでは 「-」で始まる引数はオプションとみなされる。 もし実際に "-i" というファイルを複製したい場合、 cp -i a.txt などとやってもうまくいかない:

$ cp -i a.txt
cp: missing destination file operand after 'a'
(エラーが出て実行できない)

このような場合には、2つの方法がある:

  1. 引数が「-」で始まらなけれさえすればよいので、 -i がカレント・ディレクトリにあることを利用して、 以下のようにする:
    $ cp ./-i a.txt
    (カレント・ディレクトリの -i というファイルがコピーされる)
    
  2. 多くの UNIX 標準コマンドは、引数に "--" が表れると、 それ以後の引数をオプションとして解釈することをやめ、 ファイル名として解釈するように実装されている。 このことを利用して:
    $ cp -- -i a.txt
    (-i はオプションでなく実際のファイル名として解釈される)
    

8. シェルスクリプトの基礎

シェルスクリプトとは、シェルによって実行される簡単なプログラムである。 もっとも基本的なシェルスクリプトは、単に実行するコマンド列を1行にひとつずつ 並べたテキストファイルである。 (# 以降の文字列はコメントとして解釈される。)

hello.sh
# 3つのコマンドを実行する。
echo Hello!
date
ls /etc

シェルスクリプトを実行するには、シェルの引数としてスクリプトの ファイル名を与えればよい。シェルスクリプトの実行には通常 Bourne shell (/bin/sh) が使われる:

$ sh hello.sh
Hello!                                                 (echo が実行される)
Sun Nov 13 14:48:02 JST 2022                           (date が実行される)
ImageMagick-7         logrotate.d           screenrc   (ls /etc が実行される)
X11                   lynx.cfg              securetty
alpine-release        lynx.lss              services
…

8.1. シェルスクリプトに与えられた引数を利用する

シェルスクリプトには、通常のコマンドと同様に引数を渡すことができる。 プロセス起動時に与えられた引数は、 第0引数から順に $0$1 … というシェル変数として利用できる。

showargs.sh
echo "Arg0: $0"
echo "Arg1: $1"
echo "Arg2: $2"

上のシェルスクリプトを実行すると、以下のように表示される。 第0引数はスクリプトの名前 (パス名) になっているのがわかる:

$ sh showargs.sh foo bar baz
Arg0: showargs.sh  (第0引数が表示される)
Arg1: foo          (第1引数が表示される)
Arg2: bar          (第2引数が表示される)
演習 11. シェルスクリプトの説明

以下のシェルスクリプトが何をするか推測せよ。

# showhome.sh
ls -l "/home/$1"

8.2. 通常のコマンドとして使う

UNIX では、通常実行するプログラムはその CPU 用にコンパイルされた 命令列が含まれるバイナリファイルである。 しかし UNIX ではそれ以外のファイル (テキストファイル) でも 実行できるような仕組みが用意されている。あるテキストファイルの 先頭が "#!" という特殊な 2バイトで始まっている場合、 UNIX はその行をもとに別のコマンドを呼び出すことができる。

例をあげて説明しよう。 たとえば、以下のようなテキストファイル foobar を作成する:

foobar
#!/bin/cat
foo
bar

UNIX では、実行するプログラム (ファイル) には必ず 実行可能フラグが有効になっていなければならない。 そのため、まず chmod コマンドを使ってこのファイルの 実行可能フラグを立てる:

$ chmod +x foobar  (ファイル foobar を実行可能にする)

このファイルをシェルから実行すると、以下のように表示される:

$ ./foobar
#!/bin/cat
foo
bar

一体、何が起こっているのか? 実は foobar をシェルから「実行」すると、 UNIX 内部では以下のコマンドが実行されている:

/bin/cat foobar

ここで実際に実行されるプログラム (第0引数) は /bin/cat であり、 またその第1引数としてファイル自体へのパス名 foobar が渡される。 つまり cat に対して自分自身を表示するような プログラムになっているのである。

この機能を使うと、シェルスクリプトを (わざわざ sh を使って実行しなくても) 通常のコマンドと同じように使うことができる。 たとえば、先ほどの hello.sh を以下のように変更してみよう (拡張子は .sh でなくてもかまわない):

hello
#!/bin/sh
echo Hello!
date
ls /etc

これを通常のコマンドとして実行するには、以下のようにする:

$ chmod +x hello
$ ./hello     (/bin/sh hello と同様の結果が得られる)
Hello!
Sun Nov 13 14:48:02 JST 2022
...

このファイルを環境変数 PATH で 指定したディレクトリ内に置いておけば、いまやこのシェルスクリプトは 通常のコマンドと同じように使えるわけである。

注意: シェルスクリプトは子プロセスとして実行される。UNIX では、 子プロセスは親プロセスの環境変数やカレントディレクトリを 変更することはできないので、シェルスクリプトの中で 環境変数やカレントディレクトリを変更しても親プロセスには影響を与えない。 したがって、それを呼び出したシェル (親プロセス) の 環境変数やカレントディレクトリを変更するようなシェルスクリプトを 書くことは不可能である:
$ cat gohome  (シェルスクリプト gohome の内容を表示)
cd "${HOME}"
$ cd /etc     (カレントディレクトリを /etc に変更)
$ ./gohome    (gohome を実行する)
$ pwd         (カレントディレクトリは依然として /etc のまま)
/etc

(ちなみに、cd コマンドはシェルの内部コマンドで プロセスを起動しない。そのためシェルのカレントディレクトリの変更が可能なのである。)

演習 12. スクリプトファイル

以下のスクリプトファイル greet が実行可能であるとする。 ./greet を実行すると何が起きるか?

greet
#!/usr/bin/python3
import random
if random.random() < 0.5:
    print("Good morning")
else:
    print("Good evening")

8.3. 終了状態による分岐

すべてのプロセスは、終了時に終了状態を返す。 これは、関数の返り値のようなものである。 UNIXの慣例では、正常終了した(成功した)プロセスは 終了状態 0 を返し、そうでないプロセスは 0以外の 終了状態を返すことになっている。 あるコマンドの終了状態を見るには、コマンド実行直後に シェル変数 $? を表示すればよい:

$ ls /etc
ImageMagick-7         logrotate.d           screenrc
X11                   lynx.cfg              securetty
alpine-release        lynx.lss              services
…
$ echo $?
0          (コマンドは成功したので終了状態は0)
$ ls /nonexistent
ls: /nonexistent: No such file or directory
$ echo $?
1          (エラーで終了したので終了状態は0)

さらに、シェルではコマンドの終了状態に応じて ふるまいを変更できる:

さらにこれらの条件式は、他のプログラミング言語と同様に () を使ってグループ化できる。 グループ化されたコマンド列の終了状態は、最後に実行されたコマンドの終了状態となる。 したがって、次のような例も可能:

# /fooディレクトリにcdできれば *.conf一覧を表示し、そうでなければエラーを表示する。
( cd /foo && ls *.conf ) || echo "/foo does not exist"

8.4. 文字列の操作

シェルでは文字列処理をおこなうための展開がいくつか用意されている。 代表的なものを以下にあげる:

コマンド展開を使った置換
コマンド出力の展開 を参照。
# ファイルの個数を取得する。
files=$(ls | wc -l)
${変数名/文字列1/文字列2}
文字列1文字列2 に置換する:
name=foo.csv
echo ${name/.csv/.txt}  # foo.txt
${変数名:-デフォルト値}
変数名が定義されていない (あるいは空文字列の) ときに デフォルト値 に置換される:
# 環境変数FILENAMEが定義されていればその値、なければ "default.txt"
path=${FILENAME:-default.txt}

9. 条件分岐・繰り返し

さて、シェルスクリプトの真のうまみは条件分岐や繰り返しを使って コマンドを実行させることである。シェルでは、条件分岐や繰り返しをするときも 上で述べたコマンドの終了状態を使う。

このため UNIX では、ある条件に応じて終了状態を変化させるためだけのコマンド test が存在する。

9.1. testコマンド

test コマンドは与えられた条件に応じて真偽値に対応する終了状態を返す:

なお、見やすさのため、testコマンドと同様の機能をもつ [ というコマンドが用意されている。 この場合は、以下のように記述する:

testコマンドはこれ以外にも多くの演算や条件式の組み合わせが可能である。 詳しくは man test を参照のこと。

9.2. if … then 〜 fi文

if文の使い方は、通常のプログラミング言語とほとんど同じである。

書式:
if 条件式; then
    処理
fi
ここでいう条件式とは、 実際にはひとつのコマンドのことである。
注意: 条件式の最後の ; を忘れないように!
例1:
if cd /foo; then
    echo "success"  # cd /foo に成功
fi
例2 (else節を追加):
if cd /foo; then
    echo "success"  # cd /foo に成功
else
    echo "fail"     # cd /foo に失敗
fi
演習 13. dog

与えられたパス名に対して、 それがファイルであれば cat を実行し、 ディレクトリであれば ls を実行するコマンド dog を作りたい。以下のシェルスクリプトを完成させよ:

dog
#!/bin/sh
if         ; then
    cat $1
else
    ls $1
fi

9.3. while … do 〜 done文

これも通常のプログラミング言語におけるwhile文とほとんど同じである。

書式:
while 条件式; do
    処理
done
注意: 条件式の最後の ; を忘れないように!
例:
while [ -d /foo/bar ]; do
    # フォルダ /foo/bar が存在している間はメッセージを出力。
    echo "/foo/bar exists"
    sleep 1
done

9.4. for … do 〜 done文

シェルスクリプトの for文は、与えられた各引数を指定された変数に代入しながら 繰り返し処理を実行する。 Python における for文、 あるいは JavaScript における for … of構文に似ている。

書式:
for 変数名 in 引数1 引数2 …; do
    処理
done
注意: 引数の最後の ; を忘れないように!
例1:
for file in *.txt; do
    # *.txtで終わる各ファイルを *.txt.orig に名前変更する。
    echo "Renaming: ${file}"
    mv ${file} ${file}.orig
done
例2:
for name in $(cat files.txt); do
    # files.txtの各行をファイル名一覧とみなし、それらのファイルの内容を表示する。
    echo "--- ${name} ---"
    cat ${name}
done
例3:
変数 $* は、シェルのすべての引数 ($1, $2, $3, ...) の一覧に展開される。 (なお、変数 $# は、引数の個数に展開される。)
for arg in $*; do
    # シェルの各引数をひとつずつ表示する。
    echo "Arg: ${arg}"
done
演習 14. for文の演習

いくつかのディレクトリの中に、 list.txt というファイルが入っているとする。 各ディレクトリ中の list.txtディレクトリ名_list.txt という名前に変更したい。 そのため、以下のようなスクリプトを書いた:

#!/bin/sh
for name in $*; do
    echo "Renaming: ${name}/list.txt −> ./${name}_list.txt"
    mv ${name}/list.txt ./${name}_list.txt
done
注意: 複数のファイルを一括変更するような処理の場合、 入力ファイルと出力ファイルを同じにしないこと!
for file in *.txt; do
    # ❌ ${file} を読む前に ${file} が上書きされてしまう。
    cat ${file} footer.txt > ${file}
done

このような場合は、ファイルを一度別の名前に移してから処理をおこなう。 (あるいは一時ファイルを作成し、それを元のファイルに上書きする。)

  for file in *.txt; do
    # まず ${file} を ${file}.orig に名前変更する。
    mv ${file} ${file}.orig
    # ${file}.orig に加えた変更をもとのファイルとして保存する。
    cat ${file}.orig footer.txt > ${file}
done

10. ワンライナー (one-liner) を書く時の注意点

UNIXでは、ちょっとした作業をするために1行のシェルスクリプト (ワンライナー) を 即席で書いて実行することがある。よくあるのが各ファイルについて何らかの 処理をおこなうというものである:

for file in myfiles/*; do
    # ...${file}に対してなんらかの処理をおこなう...
done

しかしこのような処理は多くのファイルを一度に変更することが多く、危険である。 このようなとき、おすすめの安全策がある。

  1. 危険なコマンドを echo で表示するだけにしておく。
    # dir1/の各.txtファイルに footer.txt を追加して dir2/ に保存する。
    # 注意: > が入っているのでコマンドライン全体を "〜" で囲むこと。
    for file in dir1/*.txt; do echo "cat ${file} footer.txt > dir2/${file}"; done
    
  2. 出力が正しいことを確認したら、このシェルスクリプト全体を sh に通す。
    for file in dir1/*.txt; do echo "cat ${file} footer.txt > dir2/${file}"; done | sh
    
演習 15. まとめ問題
  1. 以下の用語の違いを説明せよ:
    • プログラム
    • プロセス
    • コマンド
  2. 以下のコマンドがそれぞれ何をするか簡単に説明せよ:
    • less
    • pwd
    • ps
    • kill
    • grep
    • echo
    • export
    • mv
    • rm
    • chmod
  3. 空のディレクトリで、 以下のコマンド列を実行すると何が表示されるか答えよ:
    $ echo > hello
    $ echo > world
    $ echo *
    
  4. TODO リストを管理するための簡単なスクリプト todo を作りたい。 これは特定のテキストファイルに「やることの一覧」を記録する。
    このスクリプトは、以下のように使うものとする:
    • todo だけを実行すると、そのテキストファイルが表示される。
    • todo edit と実行すると、そのテキストファイルが 何らかのテキストエディタ (なんでもよい) で編集可能になる。
    • todo やること と実行すると、文字列「やること」が テキストファイルに追加される。
    • そのテキストファイルの位置は、環境変数 TODOFILE によって 決まるものとする。

    以下の空欄を埋めよ:

    todo
    #!/bin/sh
    if [ $# = 0 ]; then
        # 引数がない場合は、テキストファイルの内容を表示。
        cat         
    elif [ $1 = "edit" ]; then
        # 引数が "edit" の場合は、テキストファイルを編集。
                
    else
        # 引数がそれ以外の場合は、テキストファイルに内容を追加。
        cat "$*" >>         
    fi
    

11. おすすめの書籍


Yusuke Shinyama