概要: UNIXは現在のほとんどのサーバおよび macOSの基盤となるオペレーティングシステムであり、 UNIXのスキルは Webアプリケーション開発やシステム管理、ネットワーク管理などに欠かせないものとなっている。 この記事では、おもにアプリ開発者の役に立ちそうな UNIX/Linux に関する基礎知識を紹介する。 最終的な目標はシェルスクリプトを使った自動処理が書けることである。 記事は3部に分かれており、 1. UNIXの基本的な概念 (ファイル、プロセス、標準入出力)、 2. シェルおよび各種コマンドの使い方、 および 3. 環境変数およびシェルスクリプトの基礎 となっている。
Linuxの世界では、実際には "RedHat", "Ubuntu", "Debian" といった 複数の異なる "Linux" が存在している。これらは厳密には Linux ではなく、 異なる Linux の ディストリビューション と呼ばれている。
Windows や macOS の場合は、OSの核となる部分 (カーネル) および付属ツールなどをまとめて「OS」と呼んでいるが、Linux の場合は意味が異なる。 本来 "Linux" といえばカーネルのみをさしており 「Linuxだけ」で OSとしては使うことはない。 Linux カーネルにさまざまな部品 (ファイルシステム、パッケージ管理など) を追加して OS として 使えるようにしたものが「ディストリビューション」である。
Linuxの各ディストリビューションのおもな違いはファイルシステム構造の差異 (設定ファイルの違いなど) および、システム管理の方法の差異である。 とくに、パッケージ管理方式は各ディストリビューションによって大きく違っている。 しかしこの記事では UNIXのシステム管理の方法までは扱わないので、 内容の大部分はどのディストリビューションにもあてはまる。
こんにち、ファイルという単位を使ってディスクにデータを保存することは常識となっている。
UNIXにおいては、ファイルはただの「決まった長さをもつバイト列 (0
と1
の羅列)」
でしかなく、中身はなんでもよい。したがって、UNIXではあらゆるデータはファイルとして保存される:
(さらにUNIXでは「すべてはファイルである」という考え方をおしすすめているので、 入出力装置である画面 (端末) やメモリすらもファイルとして扱われている。)
それぞれのファイルはファイル名によって区別する。 UNIXでは、各ファイルには名前のほかに、以下のようなメタデータが付与されている:
多数のファイルは フォルダ (UNIXではディレクトリ directory と呼ぶ) を使って整理する。
UNIXでは、すべてのファイル・フォルダはひとつの巨大な木構造 (tree) をなしている、と考える。
同じ名前のファイル・フォルダが複数存在している場合、 ファイルの正確な位置は、パス名 (path) で表すことができる。 パス名は、ディスク上におけるファイルの「住所」である。
X
のパス名を書け。
H
のパス名を書け。
C
のパス名を 2つ書け。
UNIX では、いくつかのパス名は 「お約束」として決められている。
/bin
: 実行可能ファイル (プログラム)/home
: 各ユーザのホームディレクトリ/etc
: 設定ファイル/dev
: デバイスファイルls パス名
… パス名に含まれているファイル一覧を表示する。
$ ls /bin [ dd launchctl pwd tcsh bash df link realpath test cat echo ln rm unlink ...
ls -l パス名
… より詳細なメタデータとともに表示する。
$ ls -l /bin total 9536 -rwxr-xr-x 2 root wheel 101472 Mar 21 15:13 [ -r-xr-xr-x 1 root wheel 1310352 Mar 21 15:13 bash -rwxr-xr-x 1 root wheel 119008 Mar 21 15:13 cat -rwxr-xr-x 1 root wheel 120576 Mar 21 15:13 chmod -rwxr-xr-x 1 root wheel 136704 Mar 21 15:13 cp ...
cat パス名
… ファイルの内容を表示する。
UNIX ではどんなファイルも同様に扱われるので、 実は$ cat /etc/shells # List of acceptable shells for chpass(1). # Ftpd will not allow users to connect who are not using # one of these shells. /bin/bash /bin/csh /bin/dash ...
cat
コマンドを使えば
あらゆるファイルの内容を (バイト列として) 表示できる。
しかし実際には JPEG などの画像ファイルをバイト列として表示しても
意味不明な文字列が表れるだけである。
less パス名
… ファイルの内容を少しずつ表示する。
$ less /etc/services
lessでは以下のキー操作が使える:
/Applications/Safari.app/Contents/MacOS/…
実は「パス名」と呼ばれているものには 2つの種類がある。 上で説明した「パス名」は「絶対パス名」のことであった。
つまり、上のディレクトリ X
の位置は、次の2通りで表せる:
/A/E/X
./X
.
(ドット)」で表す:
./X
, ./K
など
..
」で表せる。
../../../../../../../Application
=
/Application
カレント・ディレクトリが E
のとき…
A
への相対パス名は?
H
への相対パス名は?
K
への相対パス名は? (2つある)
cd パス名
… カレント・ディレクトリを指定されたパス名に変更する。
pwd
… カレント・ディレクトリを表示する。
$ pwd /home/euske $ cd /etc $ pwd /etc
ls
(パス名を省略) … カレント・ディレクトリに含まれているファイル一覧を表示する。
ls -l
(パス名を省略) … より詳細なメタデータとともに表示する。
ファイルシステム上で「実行可能 (executable)」フラグ (x
) がついている
ファイルはプログラムとして実行可能である。
通常、ここには機械語で書かれた命令列が記録されている:
-rwxr-xr-x 1 root wheel 154352 Mar 21 15:13 /bin/ls
実行されたプログラムは、OS上で プロセス (process) として走り続ける。 UNIXはマルチタスクOSなので、通常は複数のプロセスが並列に実行される。
(実際の CPU は一度にひとつの プログラムしか実行できないため、各プロセスは高速に切り替えられ 少しずつ (10ms程度) 実行される (時分割処理)。 これらプロセス切り替え処理は、OS の カーネル (kernel) という部分がおこなう。)
ただしPC上のプロセス(アプリ)が入出力装置としてもっぱらGUIを使うのに対し、 サーバ上のプロセスは入出力装置としてネットワークを使う。 この違いを除けば、PCでもサーバでも UNIXプロセスが動いているという点は同じである。
(現代のLinuxでは init のかわりに systemd
、
macOSでは launchd
が使われている。)
ps x
… 自分が所有するプロセス一覧を表示する。
$ ps x PID TT STAT TIME COMMAND 1021 ?? Ss 0:14.87 /System/Library/ExtensionKit/…/Contents/MacOS/WallpaperVideoExtension 1110 ?? S 8:48.23 /usr/sbin/distnoted agent 1164 ?? S 23:39.51 /usr/sbin/cfprefsd agent … 78971 s002 R+ 0:00.00 ps x
ps ax
… すべてのプロセス一覧を表示する。
$ ps ax PID TT STAT TIME COMMAND 1 ?? Ss 50:47.04 /sbin/launchd 661 ?? Ss 25:50.01 /usr/libexec/logd 673 ?? Ss 16:53.34 /usr/libexec/configd 698 ?? Ss 0:58.62 /usr/sbin/syslogd … 79138 s002 R+ 0:00.00 ps ax
ps aux
… より詳細な情報 (所有者など) を表示する。
ps alx
… 別の詳細な情報 (親プロセスIDなど) を表示する。
pstree
… プロセスの木構造を視覚的に表示する。
(注: pstreeコマンドは標準の macOS には存在しないので、
Homebrew経由でインストールする)
最近では、サーバ上では生身のプロセスを直接動かすのではなく、 Dockerなどの「コンテナ」を実行することが多い。
UNIX のプロセスには、シグナル (signal) を送って制御することができる。
シグナルにはいくつかの種類がある:
SIGTERM
… プロセスを正常終了させる。
SIGKILL
… プロセスを強制終了させる。(SIGTERMよりも強力で、最後の手段)
SIGSTOP
… プロセスを一時的に停止させる。
SIGINT
… プロセスの所属する端末 (標準入力) で Control+C が押されたときに送られる。
たいていのプロセスは終了する (が、無視させることもできる)。
SIGSEGV
… プロセスが異常な動作をしたときに、カーネルによって送られる。
いわゆる「クラッシュした」状態。
kill プロセスID
… 指定したプロセスに SIGTERM
を送る。
$ sleep 60 または $ find / … (別のウィンドウで) $ ps x $ kill 12345
kill -KILL プロセスID
… 指定したプロセスに SIGKILL
を送る。
現代のUNIXはさまざまな入出力装置をサポートしているが、 プロセスが入出力装置に直接アクセスすることはほとんどない。 ほぼすべて OS (カーネル) を介している。
UNIX の各プロセスは、つねに 「標準入力 (stdin)」 「標準出力 (stdout)」 「標準エラー出力 (stderr)」 という 3つの入出力装置にアクセス可能である。 これらはプロセスが画面に文字を表示したり、 キーボードから文字を入力するために使用する。 これらをまとめて 標準入出力 (Standard I/O)」と呼ぶ。
端末 (terminal, TTY) とは、コンピュータと接続して文字情報をやりとりする機器である。 もともとはタイプライタのような物理的な機械だったが、現在ではGUIにより 仮想的にエミュレートされるアプリになっている。
標準出力 (および標準エラー出力) は、プロセスからの出力を表示するのに使われる:
$ ./hello hello, world.
いっぽう標準入力は、ユーザが文字を入力するのに使われる:
$ ./greet your name? euske greetings, euske
UNIXにおける標準入出力の最大の特徴は、これが切り替え可能だということである。
通常の状態では、プロセスの 標準入力・標準出力・標準エラー出力は、 どれも端末に接続されている。
(後述する) シェルの機能を使うと、 標準出力を端末ではなくファイルに切り替える (リダイレクトする) ことができる:
$ ./hello > output.txt (テキストファイル output.txt が生成される)
>
の出力先として
うっかり存在するファイル名を指定してしまうと、
そのファイルは何の警告もなく上書きされ、
空のファイルされる。
また、標準入力を端末ではなくファイルにリダイレクトすることも 可能である:
(テキストファイル input.txt を作成する) $ ./greet < input.txt your name? greetings, test
さらに、UNIX には「パイプ (pipe)」という機能がある。 これを使うと 「あるプロセスの標準出力を、別のプロセスの標準入力に」 リダイレクトすることができる:
$ ./hello | ./greet your name? greetings, hello, world.
パイプによる複数プロセスの接続は UNIX (シェル) の特徴的な機能のひとつである。 これをうまく使うと、複雑な処理をいくつかのコマンドの組み合わせによって 実現することができる。
(ls コマンドの出力を検索し、さらにそれをソートして最初の10行を表示する) $ ls -l | grep euske | sort | head -n10
<
, >
, |
による
標準入力・標準出力の切り替えは、
プロセスを起動する瞬間にしか指定できない。
いちどプロセスが起動してしまうと、あとから
切り替えることはできないので注意。
$ ls -l > output.txt
grep
コマンドを使うと、
標準入力から特定のパターンが含まれる行だけを表示できる:
$ 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 -rw-r--r-- 1 root wheel 0 Mar 21 15:13 kern_loader.conf -r--r--r-- 1 root wheel 2451 Mar 21 15:13 man.conf …
sort
コマンドを使うと、
標準入力の各行を昇順あるいは降順に並べ替え (ソート) できる:
$ ls -l | sort (各列の行頭からアルファベット順にソート) … $ ls -l | sort +2 (空白で区切られた 2番目の列を基準にソート) … $ ls -l | sort -n +4 (数値としてソート) … $ ls -l | sort -n -r +4 (reverse, 逆順に表示) …
このように、UNIX のコマンドは "-
" や "+
" で始まる
文字列を与えることにより異なった動きをするものが多い。
(これは何か規則があるわけではなく、ただの慣例である。)
このような文字列を一般にコマンドの「オプション (option)」と呼ぶ。
標準入出力は UNIXの入出力装置のうちもっとも基本的なもので、 どんなプロセスでも使用可能である。 PCの場合、これはデフォルトでは端末エミュレータアプリだが、 サーバには通常端末が存在しないので、サーバ上のプロセスの標準出力は、 ふつうはログファイルか、ネットワークを介した syslog などのログ収集プロセス、 あるいは (AWSのようなクラウド環境の場合) CloudWatch などのログ収集サービスに 接続されている。
(以下の穴埋め問題の中には透明なテキストが書かれており、コピー・ペーストすれば正解が見れるようになっている)
UNIX を使ううえで中心的な役割を果たしているのが シェル (shell) と呼ばれるプログラムである。 シェルは複数のプロセス全体を統括する殻 (shell) となる、 以下のような機能をもつ:
UNIX 上で「コマンド (command)」と呼ばれるものは、 実はほとんどシェルによって起動されるプロセスである。 通常の PC では、アプリ (これもプロセス) は一度起動したら 長時間走らせておくのが普通だが、たいていのコマンドは 数ミリ秒〜数秒しか生存しない。 UNIX は「湯水のようにプロセスを消費する」OSである。
とはいえ、UNIX的にみれば、シェルも他のコマンドと同じく、ひとつのプロセスにすぎない。
現在では sh
, csh
, bash
, zsh
など
いろいろなシェルが開発されている。
ほとんどの場合、シェルは以下の動作を繰り返しているだけである:
先に述べたように、UNIX コマンドのほとんどは実はプロセスであり (例外も存在する)、 シェルの主な機能はコマンド文字列を解析し、プロセスを起動することである。 たとえば
という行は、以下の行と同じである:$ ls -l /etc
$ /bin/ls -l /etc
この行が入力されたとき、シェルは以下のことをおこなう:
/bin/ls
というプログラムを子プロセスとして起動する。
ls
… 0番目の引数 (コマンド名あるいはプログラムのパス名)
-l
… 1番目の引数
/etc
… 2番目の引数
ここで注意したいのは、引数である
「-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
以下のコマンドラインの引数を 0番目からすべて挙げよ:
ls /etc /bin
ls "/etc /bin"
ls "/etc" "/bin"
ls "" "/bin"
"ls /etc /bin"
シェルでは、ディレクトリ上に複数のファイルがあるとき、 それらのファイル名を複数の引数として展開する機能がある (ファイル名展開, 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 …
/etc
以下に次のようなファイルがあるとする:
a.conf
b.conf
issue
/etc
であるとして、
以下のコマンドラインの出力を答えよ:
echo *
echo *.conf
echo *.conf /etc/*
echo /etc/issue*
echo "*"
さらにシェルは、あるコマンドが (標準出力に) 出力した文字列を まるごと引数として展開することができる。
たとえば、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
シェル上でファイル操作を行うためのおもなコマンドは以下のとおり:
ls
$ ls … (カレント・ディレクトリのファイル一覧を表示) $ ls /etc … (ディレクトリ /etc のファイル一覧を表示)
cd
(Change Directory)
$ cd /etc (カレント・ディレクトリを /etc に変更) $ cd (カレント・ディレクトリを自分のホームディレクトリに変更)
pwd
(Print Working Directory)
$ pwd (現在のパス名を表示)
cat
・ less
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/ 内に複製する)
-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/ 内に移動する)
-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/ ディレクトリ内とその中のファイルをすべて削除する)
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つの方法がある:
-
」で始まらなけれさえすればよいので、
-i
がカレント・ディレクトリにあることを利用して、
以下のようにする:
$ cp ./-i a.txt (カレント・ディレクトリの -i というファイルがコピーされる)
--
" が表れると、
それ以後の引数をオプションとして解釈することをやめ、
ファイル名として解釈するように実装されている。
このことを利用して:
$ cp -- -i a.txt (-i はオプションでなく実際のファイル名として解釈される)