アプリ開発者のための
UNIX/Linux入門

Yusuke Shinyama, May. 2024

概要: UNIXは現在のほとんどのサーバおよび macOSの基盤となるオペレーティングシステムであり、 UNIXのスキルは Webアプリケーション開発やシステム管理、ネットワーク管理などに欠かせないものとなっている。 この記事では、おもにアプリ開発者の役に立ちそうな UNIX/Linux に関する基礎知識を紹介する。 最終的な目標はシェルスクリプトを使った自動処理が書けることである。 記事は3部に分かれており、 1. UNIXの基本的な概念 (ファイル、プロセス、標準入出力)、 2. シェルおよび各種コマンドの使い方、 および 3. 環境変数およびシェルスクリプトの基礎 となっている。

目次

  1. UNIXとは
  2. ディスクとファイル
  3. プロセス
  4. 第1部のおさらい
  5. シェル
  6. ファイル操作コマンド
  7. 第2部のおさらい
  8. 環境変数
  9. シェルスクリプトの基礎
  10. 条件分岐・繰り返し
  11. 第3部のおさらい
  12. おすすめの書籍

第1部 - UNIXの基本的な概念

1. UNIXとは

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

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

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

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

Linuxの世界では、実際には "RedHat", "Ubuntu", "Debian" といった 複数の異なる "Linux" が存在している。これらは厳密には Linux ではなく、 異なる Linux の ディストリビューション と呼ばれている

Windows や macOS の場合は、OSの核となる部分 (カーネル) および付属ツールなどをまとめて「OS」と呼んでいるが、Linux の場合は意味が異なる。 本来 "Linux" といえばカーネルのみをさしており 「Linuxだけ」で OSとしては使うことはない。 Linux カーネルにさまざまな部品 (ファイルシステム、パッケージ管理など) を追加して OS として 使えるようにしたものが「ディストリビューション」である。

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

2. ディスクとファイル

こんにち、ファイルという単位を使ってディスクにデータを保存することは常識となっている。

UNIXにおいては、ファイルはただの「決まった長さをもつバイト列 (01の羅列)」 でしかなく、中身はなんでもよい。したがって、UNIXではあらゆるデータはファイルとして保存される:

ファイル1 ファイル2 010010111010001... 11110111011100111111...

(さらにUNIXでは「すべてはファイルである」という考え方をおしすすめているので、 入出力装置である画面 (端末) やメモリすらもファイルとして扱われている。)

それぞれのファイルはファイル名によって区別する。 UNIXでは、各ファイルには名前のほかに、以下のようなメタデータが付与されている:

2.1. ディレクトリとは

多数のファイルは フォルダ (UNIXではディレクトリ directory と呼ぶ) を使って整理する。

UNIXでは、すべてのファイル・フォルダはひとつの巨大な木構造 (tree) をなしている、と考える。

ディスク A B C B E C K H K X /

2.2. パス名とは

同じ名前のファイル・フォルダが複数存在している場合、 ファイルの正確な位置は、パス名 (path) で表すことができる。 パス名は、ディスク上におけるファイルの「住所」である。

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

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

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

演習 2. やってみよう

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

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

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

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

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

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

2.5. 相対パス名の表し方

演習 3. 相対パスの練習

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

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

3. プロセス

ファイルシステム上で「実行可能 (executable)」フラグ (x) がついている ファイルはプログラムとして実行可能である。 通常、ここには機械語で書かれた命令列が記録されている:

-rwxr-xr-x  1 root  wheel  154352 Mar 21 15:13 /bin/ls

実行されたプログラムは、OS上で プロセス (process) として走り続ける。 UNIXはマルチタスクOSなので、通常は複数のプロセスが並列に実行される。

カーネル プロセス プロセス プロセス

(実際の CPU は一度にひとつの プログラムしか実行できないため、各プロセスは高速に切り替えられ 少しずつ (10ms程度) 実行される (時分割処理)。 これらプロセス切り替え処理は、OS の カーネル (kernel) という部分がおこなう。)

Slack VSCode Chrome
PC上のプロセス(アプリ)
Node nginx Spring (Java)
Linuxサーバ上のプロセス

ただしPC上のプロセス(アプリ)が入出力装置としてもっぱらGUIを使うのに対し、 サーバ上のプロセスは入出力装置としてネットワークを使う。 この違いを除けば、PCでもサーバでも UNIXプロセスが動いているという点は同じである。

UNIXプロセスの特徴

(現代のLinuxでは init のかわりに systemd、 macOSでは launchd が使われている。)

演習 5. やってみよう

プロセスとコンテナ

最近では、サーバ上では生身のプロセスを直接動かすのではなく、 Dockerなどの「コンテナ」を実行することが多い。

カーネル コンテナ コンテナ プロセス プロセス プロセス

3.1. シグナル

UNIX のプロセスには、シグナル (signal) を送って制御することができる。

プロセス シグナル

シグナルにはいくつかの種類がある:

演習 6. やってみよう

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

現代のUNIXはさまざまな入出力装置をサポートしているが、 プロセスが入出力装置に直接アクセスすることはほとんどない。 ほぼすべて OS (カーネル) を介している。

OS プロセス ディスク ネットワーク 端末

UNIX の各プロセスは、つねに 「標準入力 (stdin)」 「標準出力 (stdout)」 「標準エラー出力 (stderr)」 という 3つの入出力装置にアクセス可能である。 これらはプロセスが画面に文字を表示したり、 キーボードから文字を入力するために使用する。 これらをまとめて 標準入出力 (Standard I/O)」と呼ぶ。

端末とは?

端末 (terminal, TTY) とは、コンピュータと接続して文字情報をやりとりする機器である。 もともとはタイプライタのような物理的な機械だったが、現在ではGUIにより 仮想的にエミュレートされるアプリになっている。

IBM 2741 Communications Terminal   Terminal icon2
物理的な端末と、現代の端末エミュレータ

標準出力 (および標準エラー出力) は、プロセスからの出力を表示するのに使われる:

$ ./hello
hello, world.

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

$ ./greet
your name?
euske
greetings, euske

UNIXにおける標準入出力の最大の特徴は、これが切り替え可能だということである。

通常の状態では、プロセスの 標準入力・標準出力・標準エラー出力は、 どれも端末に接続されている。

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

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

$ ./hello > output.txt
(テキストファイル output.txt が生成される)
hello 標準入力 標準出力 標準エラー出力 端末 output.txt 端末
注意: UNIX は無愛想な OS なので、 > の出力先として うっかり存在するファイル名を指定してしまうと、 そのファイルは何の警告もなく上書きされ、 空のファイルされる。

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

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

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

$ ./hello | ./greet
your name?
greetings, hello, world.
hello 標準入力 標準出力 標準エラー出力 端末 パイプ 端末 greet 標準入力 標準出力 標準エラー出力 端末 端末

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

(ls コマンドの出力を検索し、さらにそれをソートして最初の10行を表示する)
$ ls -l | grep euske | sort | head -n10
注意: <, >, | による 標準入力・標準出力の切り替えは、 プロセスを起動する瞬間にしか指定できない。 いちどプロセスが起動してしまうと、あとから 切り替えることはできないので注意。
演習 7. やってみよう

サーバにおける標準入出力

標準入出力は UNIXの入出力装置のうちもっとも基本的なもので、 どんなプロセスでも使用可能である。 PCの場合、これはデフォルトでは端末エミュレータアプリだが、 サーバには通常端末が存在しないので、サーバ上のプロセスの標準出力は、 ふつうはログファイルか、ネットワークを介した syslog などのログ収集プロセス、 あるいは (AWSのようなクラウド環境の場合) CloudWatch などのログ収集サービスに 接続されている。

OS プロセス ディスク ネットワーク 端末 標準出力
PC上のプロセス
OS プロセス ディスク ネットワーク (syslog等) 端末 標準出力
サーバ上のプロセス

第1部のおさらい

  1. 以下の穴埋め問題の中には透明なテキストが書かれており、コピー・ペーストすれば正解が見れるようになっている:
  2. ターミナルを開いて ls | sort を実行したとき...

第2部 - シェルの使い方

ソフトウェア開発における4S

よく整理整頓された環境は日々のストレスを減らし、ミスを減らし、思考を明晰にする助けとなる。 なお、調理の世界にも同様の規範がある

5. シェルとは

UNIX を使ううえで中心的な役割を果たしているのが シェル (shell) と呼ばれるプログラムである。

シェルの基本動作:

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

UNIX 上で「コマンド (command)」と呼ばれるものは、 実はほとんどシェルによって起動されるプロセスである。 たいていのコマンドは数ミリ秒〜数秒しか生存しない。 (UNIX は「湯水のようにプロセスを消費する」)

しかし UNIX的にみれば、シェルもまたひとつのプロセスにすぎず、特別な存在ではない。 その証拠に、シェルには複数の種類が存在する。

演習 8. いろいろなシェル (bash, tcsh, ksh) を体験する
(ターミナルを開く)
% bash (bashを起動する)
$ tcsh (tcshを起動する)
% ksh (kshを起動する)
$ ls (lsを実行する)
...
$ exit (kshを終了する)
% exit (tcshを終了する)
$ exit (bashを終了する)
% exit (ターミナルを閉じる)

5.1. コマンド引数の展開

先に述べたように、シェルの主な機能はコマンド文字列を解析し、プロセスを起動することである。 たとえば:

$ ls -a /etc
という行は、以下の行と同じである:
$ /bin/ls -a /etc
さらにこれは、プログラミング的には、関数呼び出しの一種と考えることができる:
ls("-a", "/etc")

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

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

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

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

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

$ echo abc 1234
abc 1234
$ echo -a /etc
-a /etc

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

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

以下のコマンドラインの引数を 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  wheel    1051 Mar 21 15:13 asl.conf
-rw-r--r--   1 root  wheel    1935 Mar 21 15:13 autofs.conf
…
以下のパターンは
$ cat *.conf
以下のような引数に展開される:
$ cat asl.conf autofs.conf

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

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

$ echo /etc/*.conf
/etc/asl.conf /etc/autofs.conf …
演習 10. ファイル名展開

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

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

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
演習 11. コマンド出力展開

以下のコマンドの出力結果を予想してみよう:

$ echo "$(echo foo) bar"

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

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

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 -sh .
(カレントディレクトリの総使用量のみを表示する)

パス名の指定方法に注意

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 はオプションでなく実際のファイル名として解釈される)
    
演習 12. シェルの挙動を予測する

とある空のディレクトリ内で、 以下のコマンド列を実行すると何が表示されるか予想してみよう:

$ echo > hello
$ echo > world
$ echo *

第2部のおさらい

  1. 以下の用語の違いを説明できる?
  2. 以下のコマンドがそれぞれ何をするか簡単に説明すると?

第3部 - シェルスクリプト入門

8. 環境変数

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

各プロセス
  • コマンドライン引数
  • カレントディレクトリ
  • 標準入力・標準出力・標準エラー出力
  • 環境変数

現在のシェルに付属している環境変数一覧を見るためには envコマンドを使う:

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

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

$ env | sort
HOME=/root
OLDPWD=/root
PAGER=less
…
演習 13. やってみよう

各プロセスに付属している環境変数の値を見るには、以下のコマンドを実行する:

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

8.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に展開される)
演習 14. 環境変数の展開

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

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

8.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
…
シェル 変数 環境変数

8.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 におけるホームディレクトリとは、つまるところ:

だけのものなのである。

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

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

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

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

$ sh hello.sh
Hello!                                        (echo が実行される)
Sun Nov 13 14:48:02 JST 2023                  (date が実行される)
Applications  Volumes      etc         sbin   (ls / が実行される)
Library	      bin          home        tmp
…

9.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引数が表示される)
演習 15. シェルスクリプトの説明

以下のシェルスクリプトが何をするか予想してみよう。

# showhome.sh
ls -l "/home/$1"
注意: UNIXではさまざまなシェルが使用可能だが、シェルスクリプトは一般に Bourneシェル (/bin/sh) 向けに書く。 これは BourneシェルがどのUNIXでもデフォルトで用意されているためである。 ただし最近は Linuxのデフォルトである Bash (/bin/bash) 向けに 書かれるスクリプトも多くなっている。

9.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 /

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

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

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

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

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

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

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

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

9.3. 終了状態による分岐

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

$ ls /
Applications  Volumes      etc         sbin
Library	      bin          home        tmp
…
$ 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"

9.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}

10. 条件分岐・繰り返し

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

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

10.1. testコマンド

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

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

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

演習 17. test コマンドの終了状態

以下のコマンドの終了状態を予想してみよう。

  1. [ a = b ]
  2. [ $a != b ] (変数 a の値が 123 のとき)
  3. [ -f /etc/services ]
  4. [ ! -d $HOME ]

10.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
演習 18. dogコマンド

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

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

10.3. while … do 〜 done文

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

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

10.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
演習 19. 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.5. ワンライナー (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
    

第3部のおさらい

TODO リストを管理するための簡単なスクリプト todo を作りたい。 これは特定のテキストファイルに「やることの一覧」を記録する。
このスクリプトは、以下のように使うものとする:

以下の空欄を埋めよ:

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

12. おすすめの書籍


Yusuke Shinyama