ちょっとだけシェルスクリプト。
csh 系ではなく sh 系で bash や zsh や ksh や POSIX shell などの話です。
このページでは、コマンドラインから入力を「強調文字」にしています。
シェルの話題なので、入力がはっきりわかるようにするためです。
プロンプトは sh ということで「$ 」を使用しています。
* 使いましょ
シェルを使うときにファイル名の展開として ls コマンドを使うと思いますが、echo * という使い方もしっておいてください。
これはシェルのとっても基本的なことなんですが、最近「あれれ?」ということがあったので。
これは、シェルスクリプトでも大切なシェルのファイル名の展開の機能です。$ echo *.c | fold array.c bignum.c class.c compar.c dir.c dln.c dmyext.c enum.c error.c eval.c file.c gc.c hash.c inits.c io.c lex.c main.c marshal.c math.c numeric.c object.c pack.c prec.c process.c random.c range.c re.c regex.c ruby.c signal.c sprintf.c st.c string.c struct.c time.c util.c variable.c version.c
? も便利
ファイル名の展開で、? を使っていますか?
文字数がはっきりわかっている場合、? は、とても便利です。
ディレクトリの名前が、「年/月/日」のような固定の場合
のように使うことができます。$ echo 2002/??/?? | fold 2002/01/05 2002/01/07 2002/01/10 2002/01/11 2002/01/17 2002/01/18 2002/01/19 2002/01/21 2002/01/23 2002/01/25 2002/01/28 2002/01/30 2002/02/01 2002/02/04 2002/02/05 2002/02/06 2002/02/07 2002/02/10 2002/02/13 2002/02/15 2002/02/18 2002/02/21 2002/02/22 2002/03/01 2002/03/07 2002/03/08
find コマンドでも、ファイル名指定時にワイルドカード文字(* や ?)が指定できます。
ある文字数以上のファイルを探したいという場合には、どうしますか? 次の例は、15 文字以上のファイル名を探しています。
いまのシステムは、「ファイル名は 14 文字まで」という制約はないと思いますが、確認したい場合などに。$ find * -name '???????????????*' doc/irb/irb-tools.rd.ja doc/forwardable.rd.ja ext/pty/expect_sample.rb ext/pty/README.expect.ja lib/irb/input-method.rb lib/irb/ws-for-case-2.rb lib/irb/extend-command.rb lib/shell/builtin-command.rb lib/shell/command-processor.rb lib/shell/process-controller.rb lib/shell/system-command.rb lib/resolv-replace.rb sample/dualstack-fetch.rb sample/dualstack-httpd.rb win32/config.status.in
古い Unix には、ファイル名が 14 文字までということがありました。 こういうシステムで使用する予定のファイル名の場合、事前チェックを行なっておかないと、思わぬことになったりしたのでした。
for 文
ファイルを一つ一つ処理する場合 for 文を使います。
のように「#! /bin/sh -x for i in *.c do echo $i done exit 0
*.c」と書いておけば、カレントディレクトリの .c 拡張子のファイル名を展開して、一つ一つのファイルを処理することが可能です。
これを間違っても
とは書かないでくださいね。 「`コマンド名`」という使い方はとても便利なのですが、今回の場合は、シェルの持っているファイル名を展開する機能を使えば OK です。? #! /bin/sh -x ? ? for i in `ls | grep '\.c$'` ? do ? echo $i ? done ? ? exit 0
case 文
「*」の便利な使い方ができるものに case 文があります。
使用していますか?
私は、文字列でのパターンマッチの場合に使用しています。
$ cat j.sh #! /bin/sh -x for i in ruby* do case `file $i` in *ELF*) echo BIN ;; *"C prog"*) echo C ;; *comman*) echo script ;; *) echo other esac done exit 0 $ sh j.sh BIN other C C BIN C C script $ file ruby* ruby-1.7.2: ELF 32-bit LSB executable, Intel 80386, version 1, dynamically linked (uses shared libs), stripped ruby.1: troff or preprocessor input text ruby.c: C program text ruby.h: C program text ruby.o: ELF 32-bit LSB relocatable, Intel 80386, version 1, not stripped rubyio.h: C program text rubysig.h: C program text rubytest.rb: commands text for ./miniruby
case 文での「*」は、簡単な文字列のパターンマッチをシェルで使えるようになるのです。
同じことを if 文で作成するとなると、結果を grep コマンドなどでパターンマッチする必要があり、難しくなってしまいます。
文字列でのパターンマッチがある場合には、case 文を使ってみてください。
if 文
if 文では、test コマンドを使って条件判断をしていたりします。
しっていますか?
最近のシェルは内部に組み込んでいる test コマンドですが、if 文では、コマンドの結果で条件判断しているのです。
たとえば
このシェルスクリプトの「#! /bin/sh -x foo="It's show time!" if [ "$foo" = "It's show time!" ]; then echo t else echo e fi exit 0
[」は、test コマンドでシェル変数と文字列の比較を実施して、結果を返しています。
これにより条件を確認しているのです。
つまり、ファイルにキーワードがあるかどうかの確認は、if 文と grep コマンドで書けてしまいます。
このシェルスクリプトでは、$ cat j.sh #! /bin/sh -x if grep Ruby README; then echo t else echo e fi exit 0 $ sh j.sh * What's Ruby Ruby is the interpreted scripting language for quick and * Features of Ruby * How to get Ruby The Ruby distribution can be found on: * Ruby home-page The URL of the Ruby home-page is: There is a mailing list to talk about Ruby. This is what you need to do to compile and install Ruby: 6. Optionally, run 'make test' to check whether the compiled Ruby t
grep コマンドが成功しているので、echo t が実行されています。
grep の出力がじゃまな場合は、/dev/null にリダイレクトしましょう。
いかがですか?$ cat j.sh #! /bin/sh -x if grep Ruby README > /dev/null; then echo t else echo e fi exit 0 $ sh j.sh t
if 文を有効に使うためにも exit で正常終了かエラー終了か必ず明示しましょう。
「0」が正常終了です。 それ以外は異常終了です。
終了ステータス 状態 0 正常終了 1 〜 127 異常終了
grep コマンドのマニュアルを参照してみてください。
検索結果により終了ステータスが違うことを確認ください。
普段使用しているコマンドも終了ステータスを返しています。
シェル変数 $? に格納されているので確認してみてください。
/usr/include/stdlib.h には、次のように定義されています。
ちなみに、「$ grep EXIT /usr/include/stdlib.h #define EXIT_FAILURE 1 /* Failing exit status. */ #define EXIT_SUCCESS 0 /* Successful exit status. */
-1」と指定する人をみかけたりするのですが、シェルによっては動作が確定していないのであまりお勧めできません。
シェルによっては 255 になることもありますし、エラーとするものもあるようです。
シェルスクリプトに、引数はどのように渡りますか?
引数を自由に扱えないと、便利なシェルスクリプトは書けません。
Unix のコマンド echo を自作してみて、シェルスクリプトの勉強をしてみませんか?
$* と $@
シェルスクリプトへ引数を渡した場合、一つ一つとりだすのに for 文と $* を使う方法があります。
次のスクリプトと実行結果をみてください。
渡された引数が、一つ一つ echo に渡されるのですが、渡される単位の違いを確認してみてください。
必要なときに必要な使い方ができますように。$ cat j.sh #! /bin/sh -x for i in $* do echo $i done echo ----------- for i in "$*" do echo $i done echo ----------- for i in $@ do echo $i done echo ----------- for i in "$@" do echo $i done exit 0 $ sh j.sh a b c "d e f" a b c d e f ----------- a b c d e f ----------- a b c d e f ----------- a b c d e f
find コマンドの結果などを一行ずつ処理したい場合があります。
シェルスクリプトではどのように書きますか?
while 文と read を組み合わせると可能です。
パイプを使って、一行ずつ処理を行ないたい場合に便利です。$ cat j.sh #! /bin/sh -x while read i do echo == $i == done exit 0 $ find * -type d | sh j.sh | head == cygwin == == djgpp == == doc == == doc/irb == == doc/net == == ext == == ext/Win32API == == ext/curses == == ext/dbm == == ext/etc ==
find と xargs
あるディレクトリ以下のすべてのファイルを、あるコマンドの対象として処理したい場合、どうしますか?
find と xargs ですね。
大抵のコマンドは、複数の引数を渡すことが可能です。
「*」のようにシェルのファイル名展開機能を使えばかなりのことができますが、引数に渡すことができる上限があったりします。
この上限を越えないように引数を適当に渡してくれるのが、xargs です。
find との組合わせで、とても強力になります。
ちなみに find コマンドには、-exec オプションがありコマンドを実行してくれます。
この方法は、一ファイルに一コマンドを実行してしまうので、プロセス数を多く消費してしまいます。
xargs を使えば、一つのコマンドに複数ファイルを渡して処理してくれるので、プロセスを起動する回数がすくなくてすみます。
この差は、けっこうシステムリソースの消費量の差となって、目にみえるくらいに違ったりします。
試してみてください。
$ find ~/src -type f -name "*.c" | xargs grep '$Id'
Unix 系のシステムでは、出力に「標準出力 stdout」と「標準エラー出力 stderr」があることはご存じですか? エラーがあった場合、標準エラー出力へ出力します。 普通の処理結果は標準出力です。 なぜ、分けているのでしょうか? Unix 系のシステムでは、パイプ処理がとても重要です。 パイプで処理する場合、正常な結果は次のコマンドの入力として処理されて OK ですが、もしエラーの場合は、困ってしまいます。 このとき標準エラー出力を使用することで、エラーのためのメッセージがパイプへ渡ることなく、確認することが可能です。
う、bash って print 文、ないのですね。
あらまぁ。
標準エラー出力は、リダイレクトだけかな。
シェルスクリプトにするとこのようになります。
標準エラー出力は、2 番です。$ cat j.sh #! /bin/sh -x echo stdout echo stderr 1>&2 exit 0 $ sh /tmp/j.sh > /dev/null stderr
「
標準入力 stdin 0 標準出力 stdout 1 標準エラー出力 stderr 2
1>&2」というのは、標準出力 1 番を標準エラー出力 2 番へ「つなぐ」ということです。
このスクリプトでは、パイプではなくて、標準出力をリダイレクトで /dev/null に捨ててしまいました。
これでも、標準エラー出力の結果が表示されています。
ちなみに POSIX shell や ksh や zsh では
と、書くことができます。 簡単ですね。$ cat j.sh #! /bin/zsh -x print stdout print -u2 stderr exit 0
Unix 系のシステムでは、結果としての出力に余計なメッセージをださないものが多いですね。 なれないと、なにを出力しているのかよくわからないと思うかもしれません。 ですが、パイプという機能をうまく使うことを考えているため、各コマンド一つ一つは、メッセージがシンプルなのです。 このおかげで、複数のコマンドを組み合わせて、目的を達成することが可能になっています。 シンプルな実行結果と、パイプ、標準入出力に標準エラー出力。 これらの組合わせがとても強力なツールを組み立てているのです。
大量のエラーメッセージを less コマンドで確認したい場合には、今回とは逆に標準エラー出力を標準出力へつなぎます。
または、2 番だけをリダイレクトして、ファイルへ保存します。
$ find / > /tmp/j.txt 2>&1 $ find / 2> /tmp/j.txt
find コマンドは、オープンできないファイルやディレクトリのエラーメッセージを出力します。
これをいっしょに保存したい場合と、エラーだけ集める場合の例になっています。
なにかあった場合のエラーメッセージなどは、標準エラー出力を使用するようにしましょう。 パイプをうまく活用する Unix では、こういうルールが大切になってきます。
{}シェルスクリプト中の一連の出力をすべてエラーも含めて保存したい場合どうしますか? 一つ一つのコマンドの出力をリダイレクトでとりだしますか?
$ cat j.sh
#! /bin/ksh -x
{
print stdout
print -u2 stderr
} >/tmp/j.txt 2>&1
exit 0
$ ksh j.sh
$ cat /tmp/j.txt
stdout
stderr
標準出力もエラーメッセージもいっしょにファイルへ出力されています。
zsh は、ここで紹介した機能がもっともっと便利に使えます。
もし使われているシステムにインストールされていたら、ぜひマニュアルを参照しながら使ってみてください。
簡単なバックアップの仕組みです。
当日のファイル名を作成して、あるディレクトリをバックアップします。
cron で自動運用しましょう。
GNU tar を使用すれば、新しいファイルだけの差分バックアップも可能です。#! /bin/sh -x tarfile=/tmp/`date +%y%m%d`.tar.gz top=/tmp/w cd $top tar czf $tarfile *
ディレクトリの移動です。 数がすくないうちは YYYYMMDD 形式のディレクトリで管理していたのですが、数が増えてきたので YYYY/MM/DD に変更したい場合に。
エラーがあった場合には、処理が止まるようにしています。#! /bin/sh -x PATH=/bin:/usr/bin for i in 2??????? do echo $i year=`echo $i | cut -c 1-4` mon=`echo $i | cut -c 5-6` day=`echo $i | cut -c 7-8` echo $year $mon $day d="$year/$mon/$day" if mkdir -p $d; then : else exit 1 fi if mv $i/* $i/.[a-zA-Z]* $d; then : else exit 1 fi if rmdir $i; then : else exit 1 fi done exit 0