概要: 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 などのログ収集サービスに 接続されている。
ls | sort
を実行したとき...
ls
の標準入力および標準出力はそれぞれ何か?
sort
の標準入力および標準出力はそれぞれ何か?
よく整理整頓された環境は日々のストレスを減らし、ミスを減らし、思考を明晰にする助けとなる。 なお、調理の世界にも同様の規範がある。
UNIX を使ううえで中心的な役割を果たしているのが シェル (shell) と呼ばれるプログラムである。
シェルの基本動作:
UNIX 上で「コマンド (command)」と呼ばれるものは、 実はほとんどシェルによって起動されるプロセスである。 たいていのコマンドは数ミリ秒〜数秒しか生存しない。 (UNIX は「湯水のようにプロセスを消費する」)
しかし UNIX的にみれば、シェルもまたひとつのプロセスにすぎず、特別な存在ではない。 その証拠に、シェルには複数の種類が存在する。
(ターミナルを開く) % bash (bashを起動する) $ tcsh (tcshを起動する) % ksh (kshを起動する) $ ls (lsを実行する) ... $ exit (kshを終了する) % exit (tcshを終了する) $ exit (bashを終了する) % exit (ターミナルを閉じる)
先に述べたように、シェルの主な機能はコマンド文字列を解析し、プロセスを起動することである。 たとえば:
という行は、以下の行と同じである:$ ls -a /etc
さらにこれは、プログラミング的には、関数呼び出しの一種と考えることができる:$ /bin/ls -a /etc
ls("-a", "/etc")
この行が入力されたとき、シェルは以下のことをおこなう:
/bin/ls
というプログラムを子プロセスとして起動する。
ls
… 0番目の引数 (コマンド名あるいはプログラムのパス名)
-a
… 1番目の引数
/etc
… 2番目の引数
ここで注意したいのは、引数である
「-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
以下のコマンドラインの引数を 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 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 …
/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
以下のコマンドの出力結果を予想してみよう:
$ echo "$(echo foo) bar"
シェル上でファイル操作を行うためのおもなコマンドは以下のとおり:
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 -sh . (カレントディレクトリの総使用量のみを表示する)
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 はオプションでなく実際のファイル名として解釈される)
とある空のディレクトリ内で、 以下のコマンド列を実行すると何が表示されるか予想してみよう:
$ echo > hello $ echo > world $ echo *
less
pwd
ps
kill
grep
echo
export
mv
rm
chmod
UNIX では、各プロセスに環境変数 (environment variable) というものが 付属している。これは各種の設定を保持しておくための文字列型の変数で、 プログラムの変数と同様に、好きな数だけ作成することができる。
現在のシェルに付属している環境変数一覧を見るためには env
コマンドを使う:
$ env SHLVL=3 HOME=/root OLDPWD=/root PAGER=less …
順序がばらばらで見にくいので sort
で並べ変えてみる:
$ env | sort HOME=/root OLDPWD=/root PAGER=less …
各プロセスに付属している環境変数の値を見るには、以下のコマンドを実行する:
$ ps axeww
各プロセスに付属する環境変数は、
プロセス中のどこからでも文字列値としてアクセス可能である。
たとえば、環境変数 USER
の値を取得したいとき:
const user = process.env['USER']
String user = System.getenv("USER");
val user: String? = System.getenv("USER")
慣例により、環境変数の名前には大文字が使われることが多い (実際には、英数字であればなんでもよい)。
シェルでは、コマンドライン引数に含まれる環境変数の値が展開される。
たとえば環境変数 USER
に euske
という値が入っている場合、
は、以下のように展開される:$ 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に展開される)
環境変数 USER
に jon
という文字列が、
環境変数 FILE
に athan
という文字列が、
入っているとき、以下のコマンドの出力を予想してみよう:
echo /home/${USER}/${FILE}
echo $USER$FILE
echo "$USER"ny
echo $USERny
各プロセスの環境変数は、一般にそのプロセスの起動時にだけ設定できる。
プロセスの起動時に環境変数を設定するには、実行したいコマンド引数の前に
「変数名=値
」のような形式を追加する。
たとえば 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 …
UNIX の環境変数のなかでも PATH
と HOME
は
とくに重要である。
通常、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 もコマンド検索順序に追加する)
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 におけるホームディレクトリとは、つまるところ:
HOME
が、そのパス名に設定されている
シェルスクリプトとは、シェルによって実行される簡単なプログラムである。
もっとも基本的なシェルスクリプトは、単に実行するコマンド列を1行にひとつずつ
並べたテキストファイルである。
(#
以降の文字列はコメントとして解釈される。)
# 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 …
シェルスクリプトには、通常のコマンドと同様に引数を渡すことができる。
プロセス起動時に与えられた引数は、
第0引数から順に $0
、$1
…
というシェル変数として利用できる。
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引数が表示される)
以下のシェルスクリプトが何をするか予想してみよう。
# showhome.sh ls -l "/home/$1"
/bin/sh
) 向けに書く。
これは BourneシェルがどのUNIXでもデフォルトで用意されているためである。
ただし最近は Linuxのデフォルトである Bash (/bin/bash
) 向けに
書かれるスクリプトも多くなっている。
UNIX では、通常実行するプログラムはその CPU 用にコンパイルされた
命令列が含まれるバイナリファイルである。
しかし UNIX ではそれ以外のファイル (テキストファイル) でも
実行できるような仕組みが用意されている。あるテキストファイルの
先頭が "#!
" という特殊な 2バイトで始まっている場合、
UNIX はその行をもとに別のコマンドを呼び出すことができる。
例をあげて説明しよう。
たとえば、以下のようなテキストファイル 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
でなくてもかまわない):
#!/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
で
指定したディレクトリ内に置いておけば、いまやこのシェルスクリプトは
通常のコマンドと同じように使えるわけである。
$ cat gohome (シェルスクリプト gohome の内容を表示) cd "${HOME}" $ cd /etc (カレントディレクトリを /etc に変更) $ ./gohome (gohome を実行する) $ pwd (カレントディレクトリは依然として /etc のまま) /etc
(ちなみに、cd
コマンドはシェルの内部コマンドで
プロセスを起動しない。そのためシェルのカレントディレクトリの変更が可能なのである。)
以下のスクリプトファイル greet
が実行可能であるとする。
./greet
を実行すると何が起きるか?
#!/usr/bin/python3 import random if random.random() < 0.5: print("Good morning") else: print("Good evening")
すべてのプロセスは、終了時に終了状態を返す。
これは、関数の返り値のようなものである。
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)
さらに、シェルではコマンドの終了状態に応じて ふるまいを変更できる:
a && b
… コマンド a
が正常終了した (成功) 時にコマンド b
を実行する。
cd /etc && ls *.conf (/etcディレクトリにcdできたら、その中の .conf ファイル一覧を表示する)
a || b
… コマンド a
が正常終了しなかった (失敗) 時にコマンド b
を実行する。
cd /foo || echo "/foo does not exist" (/fooディレクトリにcdできなかったら、エラーを表示する)
さらにこれらの条件式は、他のプログラミング言語と同様に (
… )
を使ってグループ化できる。
グループ化されたコマンド列の終了状態は、最後に実行されたコマンドの終了状態となる。
したがって、次のような例も可能:
# /fooディレクトリにcdできれば *.conf一覧を表示し、そうでなければエラーを表示する。 ( cd /foo && ls *.conf ) || echo "/foo does not exist"
シェルでは文字列処理をおこなうための展開がいくつか用意されている。 代表的なものを以下にあげる:
# ファイルの個数を取得する。 files=$(ls | wc -l)
name=foo.csv echo ${name/.csv/.txt} # foo.txt
# 環境変数FILENAMEが定義されていればその値、なければ "default.txt" path=${FILENAME:-default.txt}
さて、シェルスクリプトの真のうまみは条件分岐や繰り返しを使って コマンドを実行させることである。シェルでは、条件分岐や繰り返しをするときも 上で述べたコマンドの終了状態を使う。
0
… そのコマンドの返り値は「真」として扱われる。
0
以外 … そのコマンドの返り値は「偽」として扱われる。
このため UNIX では、ある条件に応じて終了状態を変化させるためだけのコマンド
test
が存在する。
test
コマンドは与えられた条件に応じて真偽値に対応する終了状態を返す:
test -f パス名
: 与えられた パス名 にファイルが存在したら真。
test -d パス名
: 与えられた パス名 にディレクトリが存在したら真。
test 引数A = 引数B
: 与えられた 引数A と 引数B が同一であれば真。
test ! -f パス名
: 与えられた パス名 にファイルが存在しなかったら真。
test ! -d パス名
: 与えられた パス名 にディレクトリが存在しなかったら真。
test 引数A != 引数B
: 与えられた 引数A と 引数B が同一でなければ真。
なお、見やすさのため、test
コマンドと同様の機能をもつ
[
というコマンドが用意されている。
この場合は、以下のように記述する:
[ -f パス名 ]
: 与えられた パス名 にファイルが存在したら真。
[ -d パス名 ]
: 与えられた パス名 にディレクトリが存在したら真。
[ 引数A = 引数B ]
: 与えられた 引数A と 引数B が同一であれば真。
[ ! -f パス名 ]
: 与えられた パス名 にファイルが存在しなかったら真。
[ ! -d パス名 ]
: 与えられた パス名 にディレクトリが存在しなかったら真。
[ 引数A != 引数B ]
: 与えられた 引数A と 引数B が同一でなければ真。
test
コマンドはこれ以外にも多くの演算や条件式の組み合わせが可能である。
詳しくは man test
を参照のこと。
以下のコマンドの終了状態を予想してみよう。
[ a = b ]
[ $a != b ]
(変数 a
の値が 123
のとき)
[ -f /etc/services ]
[ ! -d $HOME ]
if文の使い方は、通常のプログラミング言語とほとんど同じである。
if 条件式; then 処理 fiここでいう
条件式
とは、
実際にはひとつのコマンドのことである。
;
を忘れないように!
if cd /foo; then echo "success" # cd /foo に成功 fi
if cd /foo; then echo "success" # cd /foo に成功 else echo "fail" # cd /foo に失敗 fi
与えられたパス名に対して、
それがファイルであれば cat
を実行し、
ディレクトリであれば ls
を実行するコマンド
dog
を作りたい。以下のシェルスクリプトを完成させよう:
#!/bin/sh if; then cat $1 else ls $1 fi
これも通常のプログラミング言語におけるwhile文とほとんど同じである。
while 条件式; do 処理 done
;
を忘れないように!
while [ -d /foo/bar ]; do # フォルダ /foo/bar が存在している間はメッセージを出力。 echo "/foo/bar exists" sleep 1 done
シェルスクリプトの for文は、与えられた各引数を指定された変数に代入しながら
繰り返し処理を実行する。
Python における for
文、
あるいは JavaScript における for … of
構文に似ている。
for 変数名 in 引数1 引数2 …; do 処理 done
;
を忘れないように!
for file in *.txt; do # *.txtで終わる各ファイルを *.txt.orig に名前変更する。 echo "Renaming: ${file}" mv ${file} ${file}.orig done
for name in $(cat files.txt); do # files.txtの各行をファイル名一覧とみなし、それらのファイルの内容を表示する。 echo "--- ${name} ---" cat ${name} done
$*
は、シェルのすべての引数
($1
, $2
, $3
, ...) の一覧に展開される。
(なお、変数 $#
は、引数の個数に展開される。)
for arg in $*; do # シェルの各引数をひとつずつ表示する。 echo "Arg: ${arg}" done
いくつかのディレクトリの中に、
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
mv
コマンドを使ってファイル名を変更している。
これにともなうリスクを説明せよ。
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
UNIXでは、ちょっとした作業をするために1行のシェルスクリプト (ワンライナー) を 即席で書いて実行することがある。よくあるのが各ファイルについて何らかの 処理をおこなうというものである:
for file in myfiles/*; do # ...${file}に対してなんらかの処理をおこなう... done
しかしこのような処理は多くのファイルを一度に変更することが多く、危険である。 このようなとき、おすすめの安全策がある。
echo
で表示するだけにしておく。
# dir1/の各.txtファイルに footer.txt を追加して dir2/ に保存する。 # 注意: > が入っているのでコマンドライン全体を "〜" で囲むこと。 for file in dir1/*.txt; do echo "cat ${file} footer.txt > dir2/${file}"; done
sh
に通す。
for file in dir1/*.txt; do echo "cat ${file} footer.txt > dir2/${file}"; done | sh
TODO リストを管理するための簡単なスクリプト todo を作りたい。
これは特定のテキストファイルに「やることの一覧」を記録する。
このスクリプトは、以下のように使うものとする:
todo
だけを実行すると、そのテキストファイルが表示される。
todo edit
と実行すると、そのテキストファイルが
何らかのテキストエディタ (なんでもよい) で編集可能になる。
todo やること
と実行すると、文字列「やること
」が
テキストファイルに追加される。
TODOFILE
によって
決まるものとする。
以下の空欄を埋めよ:
#!/bin/sh if [ $# = 0 ]; then # 引数がない場合は、テキストファイルの内容を表示。 catelif [ $1 = "edit" ]; then # 引数が "edit" の場合は、テキストファイルを編集。 else # 引数がそれ以外の場合は、テキストファイルに内容を追加。 cat "$*" >> fi