概要: この記事では、アプリ開発者の役に立ちそうな UNIX/Linux の使い方に関する最低限の知識をまとめた。
目的: この記事を理解すると、まとめ問題に答えることができる。 (逆に、現時点でこれができる人には本記事は不要。)
必要に応じて、次のどれかの環境を使うこと:
1970年ごろに作られたオペレーティングシステム (OS)。 以後の多くの OS が UNIX を継承あるいは模倣している。 以下ではこれらの「UNIX風OS」も含めて UNIX と呼称する。
従来の (UNIXでない) OS と比べると、UNIX は非常に単純であった:
コンピュータの基本的な処理 (ディスク操作、 ネットワーク処理、画面表示など) を提供する基本ソフトウェア。 ほとんどの人は、このオペレーティングシステムを使った アプリケーション (応用ソフトウェア) を書いている。 実際には、アプリはコンピュータで動いているソフトウェア全体のごく一部である。 また、OS は多くの仮想化処理 (ファイルシステム、遠隔操作、同時並列処理) を実現している。
Linuxの世界では、実際には "RedHat", "Ubuntu", "Debian" といった 複数の異なる "Linux" が存在している。これらは厳密には Linux ではなく、 異なる Linux の ディストリビューション と呼ばれている。 本来 "Linux" は OSの核となる部分 (カーネル) をさしており「生の Linuxだけを使う」 ということはあり得ない。核となる Linux カーネルにさまざまな部品 (ファイルシステム、パッケージ管理など) を追加して一般的な OS として 使えるようにしたものが「ディストリビューション」である。
Linuxの各ディストリビューションのおもな違いはファイルシステムの構造の差異 (/etc以下にある設定ファイルの違いなど) およびシステム管理の方法の差異である。 とくに、パッケージ管理方式は各ディストリビューションによって大きく違っている。 しかしこの記事では UNIXのシステム管理の方法までは扱わないので、 内容の大部分はどのディストリビューションにもあてはまる。
UNIXは階層型ファイルシステム (hierarchical filesystem) という概念を導入している。
これはすべてのファイル・フォルダはひとつの巨大な木構造 (tree) をなしている。
また、UNIXでは「すべてはファイルである」という考え方をおしすすめている。
そのため、メモリや画面 (端末) も mem
や tty
といった
特殊ファイルとして表現されている。
なお、 UNIXではフォルダのことを ディレクトリ (directory) と呼ぶ。
ファイルシステム中のあらゆるファイルの位置は、 パス名 (path) で表すことができる:
X
のパス名を書け。
H
のパス名を書け。
C
のパス名を 2つ書け。
UNIX では、いくつかのパス名は 「お約束」として決められている。
/bin
: 実行可能ファイル (プログラム)/home
: 各ユーザのホームディレクトリ/etc
: 設定ファイル/dev
: デバイスファイルls パス名
… パス名に含まれているファイル一覧を表示する。
$ ls /etc ImageMagick-7 logrotate.d screenrc X11 lynx.cfg securetty alpine-release lynx.lss services …
ls -l パス名
… より詳細なメタデータとともに表示する。
$ ls -l /etc total 516 drwxr-xr-x 2 root root 434 Jul 5 2020 ImageMagick-7 drwxr-xr-x 4 root root 114 Jul 5 2020 X11 -rw-r--r-- 1 root root 7 May 29 2020 alpine-release drwxr-xr-x 4 root root 164 Jan 9 2021 apk …
cat パス名
… ファイルの内容を表示する。
UNIX ではどんなファイルも同様に扱われるので、 実は$ cat /etc/issue Welcome to Alpine Linux 3.12 Kernel \r on an \m (\l)
cat
コマンドを使えば
あらゆるファイルの内容を (バイト列として) 表示できる。
しかし実際には JPEG などの画像ファイルをバイト列として表示しても
意味不明な文字列が表れるだけである。
less パス名
… ファイルの内容を少しずつ表示する。
$ less /etc/services
lessでは以下のキー操作が使える:
UNIXでは、各ファイルには以下のようなメタデータが付与されている:
/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
) がついている
ファイルはプログラムとして実行可能である。実行されたプログラムは、システム上で一定時間
プロセス (process) として走り続ける。UNIXはマルチタスクOSなので、通常は
複数のプロセスが並列に実行される。とはいえ実際の CPU は一度にひとつの
プログラムしか実行できないため、各プロセスは高速に切り替えられ
少しずつ (10ms程度) 実行される (時分割処理)。
これらプロセス切り替え処理は、OS の
カーネル (kernel) という部分がおこなう。
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 ...
ps x
… 自分が所有するプロセス一覧を表示する。
$ ps x PID TTY STAT TIME COMMAND 19857 ? Ss 0:00 /lib/systemd/systemd --user 19858 ? S 0:00 (sd-pam) 19872 ? R 0:00 sshd: euske@pts/0 19874 pts/0 Ss 0:00 -bash 19880 pts/0 R+ 0:00 ps x
ps ax
… すべてのプロセス一覧を表示する。
$ ps ax PID TTY STAT TIME COMMAND 1 ? Ss 0:43 /sbin/init 2 ? S 0:00 [kthreadd] 3 ? I< 0:00 [rcu_gp] 4 ? I< 0:00 [rcu_par_gp] …
ps aux
… より詳細な情報 (所有者) を表示する。
ps alx
… より詳細な情報 (親プロセスID) を表示する。
pstree
… プロセスの木構造を視覚的に表示する。
(注: pstreeコマンドは標準の macOS には存在しないので、
brewを使ってインストールする)
UNIX のプロセスには、シグナル (signal) を送ることができる。 シグナルにはいくつかの種類がある:
SIGTERM
… プロセスを正常終了させる。
SIGKILL
… プロセスを強制終了させる。(SIGTERMよりも強力で、最後の手段)
SIGINT
… プロセスの所属する端末 (標準入力) で Control+C が押されたときに送られる。たいていのプロセスは終了するが、プロセスはこれを無視することもできる。
SIGSEGV
… プロセスが異常な動作をしたときに、カーネルによって送られる。
ほとんどのプロセスは終了する。
kill プロセスID
… 指定したプロセスに SIGTERM
を送る。
$ sleep 60 または $ find / … (別のウィンドウで) $ ps x $ kill 12345
kill -KILL プロセスID
… 指定したプロセスに SIGKILL
を送る。
UNIX の各プロセスには 「標準入力 (stdin)」 「標準出力 (stdout)」 「標準エラー出力 (stderr)」 という 3つの仮想的な装置が付属している。 これらはプロセスが画面に文字を表示したり、 キーボードから文字を入力するために使用する。
まず、通常の状態では、プロセスの 標準入力・標準出力・標準エラー出力は、 どれも「端末 (画面)」に接続されている。 (昔はこれはタイプライタや電話線だったが、現在ではおもに ウィンドウ上の仮想端末が使われる。)
標準出力は、プロセスからの出力を表示するのに使われる:
$ ./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.
$ ls -l > output.txt
grep
コマンドを使うと、
標準入力から特定のパターンが含まれる行だけを表示できる:
$ 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 …
sort
コマンドを使うと、
標準入力の各行を昇順あるいは降順に並べ替え (ソート) できる:
$ ls -l | sort (各列の行頭からアルファベット順にソート) … $ ls -l | sort +2 (空白で区切られた 2番目の列を基準にソート) … $ ls -l | sort -n +4 (数値としてソート) … $ ls -l | sort -n -r +4 (reverse, 逆順に表示) …
このように、UNIX のコマンドは "-
" や "+
" で始まる
文字列を与えることにより異なった動きをするものが多い。
(これは何か規則があるわけではなく、ただの慣例である。)
このような文字列を一般にコマンドの「オプション (option)」と呼ぶ。
パイプによる複数プロセスの接続は UNIX (シェル) の特徴的な機能のひとつである。 これをうまく使うと、複雑な処理をいくつかのコマンドの組み合わせによって 実現することができる。
(ls コマンドの出力を検索し、さらにそれをソートして最初の10行を表示する) $ ls -l | grep euske | sort | head -n10
<
や >
による
標準入力・標準出力の切り替えは、
プロセスを起動する瞬間にしか指定できない。
いちどプロセスが起動してしまうと、あとから
切り替えることはできないので注意。
>
の出力先として
うっかり存在するファイル名を指定してしまうと、
そのファイルは何の警告もなく上書きされ、
空のファイルされる。
以上に挙げたような各プロセスの起動、標準入力・標準出力の切り替えなどを おこなうプログラムがシェル (shell) である。 シェルは UNIX を使ううえで中心的な役割を果たしている。 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 "*"
各プロセスに所属する Control Group (cgroup) という機能を使うと、 あるプロセス (およびその子孫プロセスすべて) からアクセスできる ファイルやネットワークを制限することができる。
つまるところ、docker がやっているのは、ある Control Group 内で プロセスを起動し、それ以降のすべての子プロセスを 特定の空間に閉じ込める (containする) ことによって 各プロセスが独立したマシン上で動いているかのように 見せかけているだけなのである。
さらにシェルは、あるコマンドが (標準出力に) 出力した文字列を まるごと引数として展開することができる。
たとえば、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
UNIX では、各プロセスに環境変数 (environment variable) というものが 付属している。これは各種の設定を保持しておくための文字列型の変数で、 プログラムの変数と同様に、好きな数だけ作成することができる。
現在の環境変数一覧を見るためには env
コマンドを使う:
$ env SHLVL=3 HOME=/root OLDPWD=/root PAGER=less …
順序がばらばらで見にくいので sort
で並べ変えてみる:
$ env | sort HOME=/root OLDPWD=/root PAGER=less …
各プロセスにおける環境変数は、
プログラム中のどこからでも文字列値として
アクセス可能である。
たとえば、環境変数 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
が、そのパス名に設定されている
シェル上でファイル操作を行うためのおもなコマンドは以下のとおり:
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 はオプションでなく実際のファイル名として解釈される)
シェルスクリプトとは、シェルによって実行される簡単なプログラムである。
もっとも基本的なシェルスクリプトは、単に実行するコマンド列を1行にひとつずつ
並べたテキストファイルである。
(#
以降の文字列はコメントとして解釈される。)
# 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 …
シェルスクリプトには、通常のコマンドと同様に引数を渡すことができる。
プロセス起動時に与えられた引数は、
第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"
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 /etc
これを通常のコマンドとして実行するには、以下のようにする:
$ chmod +x hello $ ./hello (/bin/sh hello と同様の結果が得られる) Hello! Sun Nov 13 14:48:02 JST 2022 ...
このファイルを環境変数 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 /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)
さらに、シェルではコマンドの終了状態に応じて ふるまいを変更できる:
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
を参照のこと。
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
less
pwd
ps
kill
grep
echo
export
mv
rm
chmod
$ echo > hello $ echo > world $ echo *
todo
だけを実行すると、そのテキストファイルが表示される。
todo edit
と実行すると、そのテキストファイルが
何らかのテキストエディタ (なんでもよい) で編集可能になる。
todo やること
と実行すると、文字列「やること
」が
テキストファイルに追加される。
TODOFILE
によって
決まるものとする。
以下の空欄を埋めよ:
#!/bin/sh if [ $# = 0 ]; then # 引数がない場合は、テキストファイルの内容を表示。 catelif [ $1 = "edit" ]; then # 引数が "edit" の場合は、テキストファイルを編集。 else # 引数がそれ以外の場合は、テキストファイルに内容を追加。 cat "$*" >> fi