シェル

これまではプログラムを起動するための場としてしか利用していなかった シェルだが,シェル自身も変数や制御構造を持つインタプリタであり, きわめて高度な作業効率を誇る。たとえば,「"Hello" と書かれたファイル hello.txt を作成する」というとき,Rubyであれば

#!/usr/bin/env ruby
open("hello.txt", "w"){|h| h.puts "Hello"}

と書ける。これは数あるプログラミング言語のうちで 相当短く書ける部類なのだが,シェルであればさらに少ない記述量で書ける。

#!/bin/sh
echo Hello > hello.txt

「ファイルを開いてデータを書き込む」という操作がたった1字の記号 「>」で完了する。その他にも,1字か2字の記号に よく使う条件分岐の意味が込められているなど, 活用すれば圧倒的に短くスクリプトが作成できる有用な構文がシェルには 存在する。

ここでは,sh系(sh, ksh, bash, zsh)でプログラミングを行なう上で 知っておくべきことの要点をまとめる。また本書の見本環境として利用している zsh にはプログラムの記述効率を高める記法が多数あるので一部はzshのみと 明記した上でそれを記した。

zshだけでなく,sh系シェルの言語としての仕様は バージョン間の差異がほとんどないため,長期に渡って利用するものを書きやすい。 RubyやPythonのような先鋭言語は活発に仕様拡張が行なわれるため, 将来の仕様変更で自作スクリプトの書き直しを 迫られる可能性がどうしてもつきまとう。 長期間の利用が見込まれる「息の長い」スクリプトを作る必要がある場合は sh系の言語の利用も選択肢の一つとして加えた方がよいだろう。

シェル変数

定義と参照

変数定義は

変数=

の形式で行なう。イコールの前後に空白は許されない。 シェル変数は慣習的に小文字を用いる。値は定義したシェルのみが持ち, 他のシェルやプロセスからは見えない。変数一覧はシェルの内部コマンド set にて得られる。

set
HOME=/home/tar
HOST=hornet
(以下略)

定義したシェル変数の値の参照は

$変数
${変数}

いずれかで行なう。単語区切りが明確なときのみ { } は不要。 たとえばシェル変数 foo の値の直後に文字列 s を付けて出力したい場合は以下のようにする。

foo=cat		(代入)
echo $foo		(変数展開)
cat
echo $foos		(変数 foos を展開)

echo ${foo}s		(変数 foo を展開)
cats

この例で「echo $foos」とした部分は,変数 foos の展開を試みているが,この時点で変数 foos は未定義である。 shでは未定義変数を参照した場合たんに空文字列を返しエラーは発生しない。 これは「空文字列("")を値に持つ変数」と「未定義変数」を 区別できないことを意味しない。両者を区別して扱いたい場合は次項の 条件付き展開を用いる。

条件付き展開

ある変数に値が定義されているかどうかによって展開結果が変わる記法がある。 おおむね「${変数 記号 値} のような形で記すもので, 代表的なものを以下に示す。

${変数:-} 「デフォルト値」 変数 の値が未定義か空文字列なら に展開する。 そうでなければ変数の値に展開する。
${変数:=} 「デフォルト値と代入」変数 の値が未定義か空文字列なら に展開しつつ変数にもその値を代入する。 そうでなければ変数の値に展開する。
${変数:?[]} 「値保持強制」変数 の値が未定義か空文字列なら (省略時はデフォルトのエラーメッセージ) を標準エラー出力に出し,スクリプトを終了する。 そうでなければ変数の値に展開する。
${変数:+} 「値保持連動」変数 の値が未定義か空文字列なら 空文字列("")に展開する。 そうでなければに展開する。

なお,上記の記号はどれもコロン(:)で始まっているが, コロンを省略して書くこともでき,その場合は「未定義か空文字列なら」ではなく, 「未定義なら」に働きが変わる。

特殊な展開

変数の値を文字列加工した結果に展開する規則が使える。 ここではスクリプト処理で頻繁に用いるパス名の操作に利用できる記法を記す。 以下の表記のパターンはシェルのファイル名展開で使えるパターンで *?[ ] の記号が使える。

${変数%パターン} 「末尾最短マッチ除去」変数の値の文字列の末尾から パターン にマッチする最短部分を取り除く。
${変数%%パターン} 「末尾最長マッチ除去」変数の値の文字列の末尾から パターン にマッチする最長部分を取り除く。
${変数#パターン} 「先頭最短マッチ除去」変数の値の文字列の先頭から パターン にマッチする最短部分を取り除く。
${変数##パターン} 「先頭最長マッチ除去」変数の値の文字列の先頭から パターン にマッチする最長部分を取り除く。

これを用いて,パス名からディレクトリ名を得たり, ディレクトリ部分を除去したベース名を得られる。 以下の例はシェル変数 foo/usr/local/bin/cmd.sh という値が代入されている場合の展開の様子を示したものである。

echo ${foo%/*}
/usr/local/bin
echo ${foo#*/}
usr/local/bin/cmd.sh
echo ${foo##*/}
cmd.sh

メイルアドレスが代入されたシェル変数から, ローカル部(@より前の部分)とドメイン部(@より後ろの部分)を取り出す 場合などに利用できる。

環境変数

シェル変数と環境変数

シェル変数は起動しているシェルプロセスのみで有効なものである。 そのうち,指定した変数をそのシェルから起動されるすべての子孫プロセスに 継承させることができる。別プロセスに引き継がれる変数のことを 環境変数 といい,すべてのプロセスがこの情報を親プロセスから 引き継いで始まる。ただしプロセスごとの環境変数表は独立しているため, 子プロセスで設定した変数は親プロセスには反映されない。

shではシェル変数を export することで環境変数化でき,そのシェルから 起動するすべての子孫プロセスにコピーされて渡される。 慣習的に環境変数の名前は大文字が用いられる。 環境変数の値を出力するための外部コマンド printenv を用いて挙動を示す。

FOO=Hello		# シェル変数として定義
printenv FOO		# 何も出力されない
export FOO		# exportする
printenv FOO		# 環境変数なのでOK
Hello

export は指定したシェル変数をそれ以後ずっとexportする設定とする。 export を使わずに, 次のように変数代入の直後にコマンド起動の文を書けば, そこで起動するコマンド1回だけに環境変数を与えることができる。

変数= コマンド...

のように変数代入形式の後ろに続けてコマンド起動の文を書くことで, そこで起動されるコマンドだけに環境変数を与え,シェル自身の変数設定には 影響を与えない。

printenv BAR		# 何も設定していないので出ない
BAR=yes printenv BAR	# printenvコマンドのときだけ設定される
yes
echo $BAR		# 元のシェルの変数には無影響


変数= の組は空白で区切って何個でも与えられる。 この起動方法を利用するとコマンドへの情報授受が簡単確実に行なえる。 たとえば

一時ファイルを作るディレクトリとして, 特に指定がなければ /tmp,環境変数 TMPDIR が設定されている場合はその値のディレクトリを利用する。

のように振る舞うプログラムが存在するが,そのような プログラムに臨時で特定のディレクトリを使わせたい場合は 以下のように起動すればよい。

TMPDIR=/var/tmp command...

実際に上記のように環境変数で挙動を変えるプログラムを自分で書く場合は 以下のような書き出しで作成できる。

#!/bin/sh
tmpdir=${TMPDIR:-/tmp}

Rubyの場合であれば以下のとおり。

#!/usr/bin/env ruby
tmpdir = ENV["TMPDIR"] || "/tmp"

コマンド置換

`cmdline`(バッククォート)

Rubyのバッククォート 同様,内部のコマンド cmdline を起動した結果の文字列に置換される。

$(cmdline)

バッククォートと同様,コマンドを起動した結果の文字列に 置換される。バッククォートと違いネスト可能。

自分が差出人のメイルから,送信先となっている頻度が 一番多い人にメイルを送る手順を例に示す。

cd ~/Mail/inbox
grep -l "Return-Path:.*$USER" *
(マッチするファイル名一覧が出る)
grep '^To:' $(grep -l "Return-Path.*$USER" *)
(自分が差出人のファイルから To: ヘッダの行を抽出)
grep '^To:' $(grep -l "Return-Path.*$USER" *) | \
  ruby -pe 'sub(/.*[ <](.+@[^>]*)>?/,"\\1")'
(送信先のアドレス一覧が出る)
grep '^To:' $(grep -l "Return-Path.*$USER" *) | \
  ruby -pe 'sub(/.*[ <](.+@[^>]*)>?/,"\\1")' | \
  sort | uniq -c | sort -nr | head -1 | awk '{print $2}'
(頻度1位の人のアドレスが出る)
echo 1位です! | nkf -j | \
  Mail -s congratulations $(grep '^To:' $(grep -l "Return-Path.*$USER" *) | \
  ruby -pe 'sub(/.*[ <](.+@[^>]*)>?/,"\\1")' | \
  sort | uniq -c | sort -nr | head -1 | awk '{print $2}')

算術展開

一部のshを除くほぼすべてのsh系のシェルでは, $((数式)) の形で整数の簡単な演算が行なえる。 数式 の部分には数値を値に持つシェル変数が使え, その場合 $ を付けても付けなくてもよい。

i=1024			(シェル変数iに代入)
echo $((i+$i))		(iでも$iでもよい)
2048				(1024+1024=2048)
echo $((3*i*i/512))
6144

ブレース展開

中括弧 { } の内部にカンマで区切った文字列を列挙すると それらを開いた文字列に展開する。前後に文字列を付けると それらと結合した上で展開する。

echo {a,b,c}
a b c
echo file-{a,b,c}
file-a file-b file-c
echo hoge.rb{,bak}
hoge.rb hoge.rb.bak
echo cp Ruby/kadai1/hogehoge.rb{,.bak}
cp Ruby/kadai1/hogehoge.rb Ruby/kadai1/hogehoge.rb.bak

zsh拡張として数値範囲を指定した展開ができる。

echo {1..20}
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
echo file{1..20}
file1 file2 file3 file4 file5 file6 file7 file8 file9 file10 file11
file12 file13 file14 file15 file16 file17 file18 file19 file20
(空のファイルを作るためにtouchコマンドを用いる)
touch file{1..999}
(lsで確認後消去)
ls
rm file{1..999}

制御構造

あるコマンドの実行が成功したとき(終了値0のとき),失敗したとき で分岐したり,繰り返し処理を行なったりできる。

cmdline1 && cmdline2

cmdline1 の実行が成功したときのみ cmdline2 を実行する。

cd
touch myprogram.rb && chmod +x myprogram.rb
(ホームディレクトリには作れるので chmod +x myprogram.rb される)
rm myprogram.rb
touch /myprogram.rb && chmod +x /myprogram.rb
(ルートディレクトリには作れないので chmod されない)
cmdline1 || cmdline2

cmdline1 の実行が失敗したときのみ cmdline2 を実行する。

if cmdline1; then 〜 elif cmdline2; then 〜 else 〜 fi

cmdline1 の実行が成功したときに thenに続くブロックを評価する。 いずれのコマンドも失敗したときは else ブロックを評価する。

#!/bin/sh
if touch hogehoge.rb
then
  echo ./hogehoge.rb 作れました
elif touch /tmp/hogehoge.rb
  echo /tmp/hogehoge.rb 作れました
else
  echo 失敗しました。やめ。
  exit
fi

コマンドの部分には条件判断を行なうだけのコマンドを 使うことが多い。典型的には /bin/test コマンドで これはファイルの存在を確かめたりできる。Rubyの test 関数は 外部コマンド test に由来する。

#!/bin/sh
if /bin/test -f hogehoge.rb
then
  echo hogehoge.rb ファイル,あります。
else
  echo hogehoge.rb ファイル,ありません。
fi

条件式に見えやすいよう,test コマンドには [ という名前のハードリンクが張られているので, これを用いて書き換えると以下のようになる。

#!/bin/sh
if [ -f hogehoge.rb ]; then
  echo hogehoge.rb ファイル,あります。
else
  echo hogehoge.rb ファイル,ありません。
fi

test の代わりに [ で 呼び出したときは行の終わりに ] を付ける。

while cmdline; do 〜; done

cmdline が成功を返す間ブロックを評価し続ける。 cmdline の部分には test コマンドや, 常に成功する /bin/true コマンドを用いることが多い。

#!/bin/sh
while true; do
  echo 誰か止めて〜
  sleep 1
done
for 変数 in 語群; do 〜 ; done

語群 (を展開した結果)を先頭要素から順に 変数 に代入しつつブロックを繰り返す。

次の例はカレントディレクトリに含まれる *.rb ファイルすべてのバックアップファイルを作成する。

for f in *.rb; do
cp $f $f.bak
done

シェル関数

一連の手続をまとめて関数化できる。シェル関数の定義は

関数名() {
  …定義本体…
}

の形式で行なう。関数への引数は $1, $2, $3, ... で受け取る。$* はすべての引数を表す。

# 定義する
foo() {
 echo 123: $1 $2 $3
 echo all: $*
}
# 呼び出す
foo a b c d e f g
123: a b c
all: a b c d e f g

グロッビング

ファイル名に対するパターンマッチングをグロッビングという。 正規表現とは規則が違う。

?

任意の1字にマッチ

*

0字以上の任意の文字列にマッチ

[文字クラス]

文字クラス のいずれかに1字にマッチ。

zsh拡張として次のパターンも使える。

**/

ディレクトリを再帰的に検索

<整数1-整数2>

数値的に整数1以上 整数2以下の文字列にマッチする。 上限のみ,下限のみでもよい。たとえば,カレントディレクトリに 15個のファイル,file1.rb file2.rb ... file15.rb がある場合,

file<4-12>.rb

は,file4.rb file5.rb ... file12.rb の9個のファイル

file<-9>.rb

は,file1.rb file2.rb ... file9.rb の9個のファイル

file<8->.rb

は,file8.rb file9.rb ... file15.rb の8個のファイル にマッチする。

もちろん存在しないファイル名は出てこない。

パターン1~パターン2

パターン1にマッチするものから, パターン2にマッチするものを除外する。 たとえば,上述の file1.rb file2.rb ... file15.rb の15個のファイルがある場合,

file??.*~*15*

と指定すると,"file" の後ろに任意の2字が来て, そのあとピリオドと任意文字列が来るファイル名すべてをまず選び, そこから途中に "15" という文字列が現れるものを 除外するので file10.rb file11.rb file12.rb file13.rb file14.rb がマッチする。

入出力

シェルは標準入出力を処理するフィルタとしても振る舞える。 シェルが直接標準入力を読むには read、 標準出力に送るには echo を用いる。

read 変数 ...

標準入力から1行読み込み、変数 に代入する。 変数を2個以上指定した場合は空白文字でフィールド分割した 各フィールドを先頭の変数から順次代入する。フィールドの方が 多い場合は最後の変数に残りすべてが代入される。

read x
foo bar baz
x -> "foo bar baz"
read x y
foo bar baz
x -> "foo"
y -> "bar baz"
read x y z
foo bar baz
x -> "foo"
y -> "bar"
z -> "baz"

フィールド区切りを空白以外に変更するにはシェル変数 IFS に区切り文字を列挙する。CSVファイルを 読み取って処理する例を示す。

score.csv

山田太郎,50,70,20
公益太郎,90,80,70
飯森花子,91,79,72
鶴岡一人,60,60,40
酒田三吉,52,70,80
三川一二三,12,34,99
cat score.csv | while IFS=, read name math eng jp
do
  sum=$((math + eng + jp))
  echo $name さんは合計 $sum 点です。
  if [ $sum -lt 200 ]; then
    echo "*** $name さんは赤点です ***"
  fi
done > result.txt
cat result.txt
山田太郎 さんは合計 140 点です。
*** 山田太郎 さんは赤点です ***
公益太郎 さんは合計 240 点です。
飯森花子 さんは合計 242 点です。
鶴岡一人 さんは合計 160 点です。
*** 鶴岡一人 さんは赤点です ***
酒田三吉 さんは合計 202 点です。
三川一二三 さんは合計 145 点です。
*** 三川一二三 さんは赤点です ***

目次