Posted at

このタイミングでシェルスクリプトの基本を学ぶ

More than 5 years have passed since last update.

よーしお前ら、今日はシェルスクリプトを勉強するぞ。シェルの種類はbashだ。ただ、他のシェルでも基本は同じなので安心してくれ。

シェルスクリプトと言っても、findの使い方とかglob展開とかそういう細かいTIPSはやらん。そういうのは他の記事でも読んでくれ。そうじゃなくて、基本的な処理の流れ、考え方の勉強だ。

だいたいシェルスクリプトなんて普段大してやってなくて、いざ必要になったときに「まあPerlやRubyみたいなもんだろ」とか軽く考えてたらしょーもないことでハマって時間を無駄にしたりするもんだ。

なんでそんなことが起きるのか。

それはシェルスクリプトの基本的な発想が分かってないからだ。PerlやRubyのような普通に使われてるプログラム言語とシェルスクリプトの書き方はけっこう違うところがあって、それを理解せずに書いてしまってるからだ。

そもそもシェルとは何かもう一度思い出してみろ。

今普通にコンピュータを使っていて、テキストファイルを1つ作りたいとしよう。「XXXというディレクトリの下に」「YYYという名前で」「ファイルを作る」という命令を送らなければならない。これを誰に送るか。最終的な送り先はOSだ。もっと正確に言うとKernelだ。

もちろんKernelはこういう命令を受け付ける。しかし、Kernelは機械だ。マシンだ。全然人にやさしくない。その「命令」の書き方がまったくユーザーフレンドリーじゃない。

ファイルを作るという簡単なことだけでCのfopenに相当することをしなくてはならない。引数もなんだかよくわからない。その引数はKernelにとっては必要なのだろうが、人間にはややこしすぎる。厳しい。めんどくさい。

というわけで、直接Kernelに命令を送る作戦はやってやれないことはないが、そんなことやってたらたぶん死ぬ。

そこでシェルの登場だ。シェルは人にやさしい。ユーザーフレンドリーだ。人間からの命令を受け付けることを想定している。そしてそれをKernelに分かる形に変換して渡す。いいやつだ。みんな大好きだろ。俺も大好きだ。

大事なので繰り返す。シェルは人間からの命令を受け付けるために存在する。命令と言っても大したことはない。みんなもいつもやってるだろ。

% touch ~/work/memo.txt

とかいうやつだ。「~/work/memo.txtにファイルを作ってくれ」という意味だ。いや、実を言うとこれは「touchという実行可能なファイルがあるので、~/work/memo.txtを引数としてそいつを呼び出してくれ」だ。そのへんのことはまた後で説明する。

とにかく、みんな日常的にシェルに命令を送ってる。その命令というのは、文字列だ。普通はキーボードから手で入力する。でも、文字列だったらテキストファイルに書いておいてそいつを読ませてもいいと思うだろ。それがシェルスクリプトだ。

シェルは起動したら

%

とかプロンプトを表示して、入力を待ってる。そして改行までを1行の命令として解釈する。

さっきの例で言うと、touch ~/work/memo.txt<CR>とキーボードを打つだろ。そうしたら1行分の命令が入力されたということで、そいつを実行するわけだ。ちなみに<CR>ってのはReturnキー(Enterキー)を押したという意味だ。この記事ではこう書くことにする。

1行解釈して実行したら、また次の命令を待つ。そこでまた適当に命令を入れて<CR>とすると、そいつをまた実行してくれる。

じゃあ、この命令をあらかじめファイルに書いておいたとしよう。

touch ~/work/memo.txt

こんな感じのテキストファイルを作った。そのまま書いただけだ。ファイル名は何でもいいんだが、memo.shとしたとしよう。最後に改行コードも入れておいたので、正確に書くとこうだ。

touch ~/work/memo.txt<CR>

こいつをシェル(bash)に読ませるにはこうすればいい。

% bash memo.sh

bashはこのファイルの中身を読みこむ。touch ~/work/memo.txt<CR>と書いてあるので、touch ~/work/memo.txtまでを1行の命令と解釈して実行する。

これがシェルスクリプトだ。これは手でキーボードから入力したのと全く同じ動作だ。


  • キーボードで命令を入れて<CR>(Returnキーを押す)

  • ファイルに命令を書いて末尾に<CR>(改行)

同じだ。シェルは手で入力した時もファイルに書いてある時も<CR>までを読み取ってそいつを実行する。

もっと言うと、キーボードからの入力を読み取るというのはUNIX的に言うと「標準入力」という「ファイル」を読み取ることだ。テキストファイルを読み取るのと同じだ。細かいマニアックなところで違いはあるかもしれないが、それは全然本質的ではない。

というわけで、シェルスクリプトというのは、仕組みとしては普段のコマンドラインの操作と同じということをまず理解しろ。

もちろん、シェルスクリプトとして書くときは複雑な命令を書くことが多い。普通は変数も使うし、ifとか条件分岐を使うことも多い。一方、手入力の場合はそんな難しいことはせずに、目で確認しながら入力するだろう。

ただ、それはそういう使い方をしているというだけの話だ。手入力のような簡素なシェルスクリプトを書いてもいいし、逆に手入力の時にifで条件分岐したっていい。

もう一度繰り返すぞ。コマンドラインの入力とファイルに書いてあるシェルスクリプトは同じだ。

そして、さっきシェルはユーザーフレンドリーだと言ったのを覚えているか。そう、シェルは人間からの手入力を前提としてる。設計思想としてそうだ。なので手で打った時に楽になるような文法になってる。

例えばlsでルートディレクトリの中を見たかったとしよう。ついでに-aオプションもつけてみよう。

% ls -a /

こうだ。当たり前の書き方だが、よく考えてみると普通のプログラム言語とはだいぶ違う。

例えば、CやJavaのように書くとしたらこうなるだろう。

# こんな書き方はできない

% ls("-a", "/")

めんどくさい。毎日こんなのをやってたら死ぬ。もちろん実際にはこんなふうにはなってなくて、ls -a /のように書ける。

ここで大事なのは以下だ。


  • 文字列は"で囲まなくても良い。文字をずらずら並べたらそれが文字列だ。


  • ,も必要ない。単語はスペースで区切る

  • 引数のカッコもいらない。1つ目に書いたものがコマンドで、それ以降が全部引数だ

つまり基本的な書き方は以下のようになる。

コマンド 引数1 引数2 ...

いつも普通に書いてると思うが、この形が重要だ。覚えておけよ(また後で出てくる)

ちょっとここで例を出そう。変数の代入だ。変数の代入はシェルスクリプトの中でも間違えやすいものだ。例えば、「fooという変数にbarという文字列を入れる」というときの正しい書き方はこうだ。

foo=bar

よくある間違いとしてこういうのがある。

# 間違い

foo = bar

=の周りにスペースを入れているパターンだ。普通のプログラム言語をやってたらこう書きたくなるのも分かる。

でもさっきの「コマンド 引数1 引数2 ...」という形に当てはめて考えてみると、これは「fooというコマンドを、=barを引数として実行する」という意味になる。

だいたい、本当にfooというコマンドが存在して、それに=とbarを引数に渡したかったらどう書く? foo = bar と書くだろ。こう書いて変数の代入になったら困るだろ。そういうことだ。

つまり、=を引数にしたいことは普通にありえるので、代入はそれとは違う形でなくてはならない(=は予約語でも何でもないことに注意)。「コマンド 引数1 引数2 ...」という形が大前提としてあって、代入はそれよりは頻度が少ないのでそれより特殊な書き方をルールとしているということだ。

もう1つ、ifの例にいくぞ。「fooという変数の値がbarという文字列に等しかったら処理1を行う」というときの正しい書き方はこうだ。

if [ bar = $foo ]

then
# 処理1
fi

普通はif [ $foo = bar ]と書くと思うが、説明の都合で順番を逆にした。どっちでも正しい書き方だ。

よくある間違いとして、こんなのがある。

# 間違い

if[bar = $foo]
then
# 処理1
fi

適当にスペースを詰めている書き方だ。

でもさっきと同じだ。「コマンド 引数1 引数2 ...」という形に当てはめて考えてみると、「if[barというコマンドを =$foo]を引数として実行する」という意味になる。そしてif[barなんてコマンドは(普通は)ないのでエラーなるわけだ。逆に本当にあったらそれが実行される。というわけで、これも全く違う意味になっていることが分かるだろう。

ここで大事なのは、「スペースのありなしで意味が変わる」ということだ。「引数はスペースで区切る」というルールなので、スペースがあるかないかは普通のプログラム言語で言うと,があるかないかと同じぐらい違う意味になる。これでさっきの例でスペースを無くしてエラーになったことも納得できるだろう。

だらだら話をしてきたが、強引にまとめに入るぞ。


  • 手入力のコマンドラインとファイルに書いてあるシェルスクリプトは本質的に同じだ

  • 普通の言語で言うところの func("param1", "param2") がシェルスクリプトの func param1 param2 にあたる

  • スペースの有無は重要だ

ここまででシェルスクリプトの基本的な構造が分かっただろう。

なぜシェルスクリプトが書きにくいのか、間違えやすいのか。それはシェルの文法が手入力を前提としているからだ。対話的にキーボードから入力して使いやすいようになっている。その範囲内でifやらなんやらのプログラム言語っぽい文法を兼ね備えている。なので、はっきり言って本格的なスクリプトは書きにくい。もしシェルの力では難しいようなことになったら、そのときは他の言語で実装することを考えたほうがいいだろう。

他にも色々と言いたいことがあるが、キリがないので今日はこのくらいにしておこう。