2006年9月3日日曜日

sedを使ったテキスト書き換え処理を覚えよう (3)

今回は、sedを用いたshプログラミング、シェルスクリプトの書き方について少しだけ紹介します。



シェルスクリプトは、Unixのコマンドラインインターフェイスであるシェルが実行できるプログラムのことです。シェルといってもいろんな種類がありますが、/bin/shを対象にすることが多いです。cshでスクリプトを書くのは、ごく一部の人たちから、非常に反感を買うようです(笑)。私も、スクリプトを書くときはshにしろと刷り込まれてしまったので、cshは使っていません(ログインシェルは、10年以上前からずっとtcshです)。



sedは、シンプルなものを巧妙に組み合わせて目的の処理を実現するわけで、どことなくパズル的な楽しみ(?)があったりしますが、shのスクリプトもそんなところがあります。





■ forによる繰り返し処理



shスクリプトでのループ処理では、forやwhileなどがありますが、ここではforを紹介します。



基本は、こんなかんじになります。

#!/bin/sh



for i in ABC DEF GHI
do
        echo $i
done



#!/bin/sh」は、これは/bin/shで実行するスクリプトです、というお約束のコメントだと思っている人もいるかもしれませんが、「#!」の2文字がファイルのマジックナンバー(ファイルの先頭にかかれている、ファイルの種別を表す記号のようなもの)です。
ちなみにFreeBSDだと、/usr/share/misc/magicというファイルに、ファイルのマジックナンバーの一覧がのっています。



forループ本体のほうを見てみましょう。forループには必ず、for、in、do、doneのキーワードがあります。



「for i in ABC DEF GHI」が、for文でループ処理を行うときのキーワードです。「i」は変数名で、「in」のあとに列挙した項目が、ループを回るたびに、変数に代入されます。



「do」から「done」までに書いたコマンドが、ループの処理内容です。この例では、echoコマンドで、変数iの内容を表示します。



実行例



% cat loop.sh    (シェルスクリプトの内容を表示)
#!/bin/sh



for i in ABC DEF GHI
do
        echo $i
done
% chmod +x loop.sh  (実行可能フラグを立てる)
% ./loop.sh     (実行する)
ABC
DEF
GHI



inのあとに、ABCといった文字列を指定する変わりに、「*.txt」のようなファイル名にマッチするパターンを指定することもできます。この場合、マッチしたファイル名が、変数へ毎回代入されることになります。



% ls *.txt
file1.txt  file2.txt
% cat loop.sh
#!/bin/sh



for i in *.txt
do
        echo $i
done
% ./loop.sh
file1.txt
file2.txt



複数のファイルを対象に、各ファイルごとに、何かの処理を行いたい場合は、こっちの書き方を使います。



■(オマケ) #!の使い方



#!のあとに続く文字列をコマンド名として、そのコマンドを実行する仕組みになっています。ファイルに「#! COMMAND OPT」と書いてあると、「COMMAND OPT ファイル名」というコマンドが実行されることになっています。そのため、



~~~~~~~~~~~
#! /usr/bin/sed -f



s/ABC/abc/g
~~~~~~~~~~~



という書き方も可能で、このスクリプトを実行するとsedが実行されます。「-f」はsedのオプションで、sedコマンドを書いたファイル名を指定するときに使います。
以上の3行を書いたファイルを、たとえばtest.sedというファイル名にしておき、chmodで実行可能にしておきます。すると、こんな風に実行することも可能です。



% cat test.sed
#! /usr/bin/sed -f



s/ABC/abc/g
% chmod +x test.sed
% ./test.sed
ABCDEFG ABCD AB  (キーボードから入力した文字)
abcDEFG abcD AB  (sedの出力)



このスクリプトを実行すると、実際には、

/usr/bin/sed -f ./test.sed

というコマンドが起動されているのです。






■ 例:ITmediaのRSSをスマートに一括で書き換えるシェルスクリプト

昨日の





ではRSSをダウンロードしてsedで書き換える処理を、

% wget -q -O - http://rss.itmedia.co.jp/rss/1.0/pcuser.xml | sed 's|http://cgi.itmedia.co.jp/rss/pcuser_1.0/plusd|http://plusd.itmedia.co.jp|' > ! pcuser.xml

のようにできると紹介しました。このRSSのほかに、ぜんぶで6個のRSSがありましたので、6個すべての処理を、シェルスクリプトで書いてみました。





#! /bin/sh

DIR="/tmp"

for i in \
    http://rss.itmedia.co.jp/rss/1.0/topstory.xml \
    http://rss.itmedia.co.jp/rss/1.0/plusd.xml \
    http://rss.itmedia.co.jp/rss/1.0/news_bursts.xml \
    http://rss.itmedia.co.jp/rss/1.0/lifestyle.xml \
    http://rss.itmedia.co.jp/rss/1.0/pcuser.xml \
    http://rss.itmedia.co.jp/rss/1.0/games.xml
do
    echo $i
    OUTFILE=`echo $i | sed 's:.*/::'`
    #OUTFILE=`basename $i`

    /usr/local/bin/wget -q -O - $i \
    | /usr/bin/sed '
s|http://cgi.itmedia.co.jp/rss/[^/]*/atit/|http://www.atmarkit.co.jp/|g
s|http://cgi.itmedia.co.jp/rss/[^/]*/\([^/]*\)/|http://\1.itmedia.co.jp/|g
' \
    > ${DIR}/${OUTFILE}
done

(2006/9/5 変更) OUTFILE=`~`のところで、シングルクォート(')が1個、抜けてました。なぜか動いちゃうんですが・・・?!



(2006/9/7 変更) atmarkitという別サイトへのリンクが入ることがあるので、書き換えルールをもう1つ追加しました。



(2007/6/2 追記) ダメだこりゃ・・・あきらめた>>ITmedia


 


基本的にやっていることは、さきほど紹介したforでのループ処理があって、ループ内での処理は、すでに紹介済みのwgetとsedをパイプでつないだもの、ってことです。

行末にある「\」は、本当は1行で書くべきところを、(長すぎるので)途中で改行したときに、改行の意味を打ち消すために入れるものです。

#」はコメントを表す記号です。「#」以降に書いたものが、コメントとして無視されます。

$i」は、実行時に変数iの値で置き換わります。中には、「${DIR}」というように、{ }で囲んでいる部分があるのですが、今回の場合$DIRと書いても同じです。たとえば、変数DIRの値に、"def"という文字列をつなげたい場合、「$DIRdef」と書いてしまうと、変数DIRdefの値、という意味になってしまうので、それを避けるために「${DIR}def」と書きます。



sedとかwgetとかコマンドを実行するときに、/usr/bin/sedや/usr/local/bin/wgetのようにフルパスで指定していますが、これは環境変数PATHの影響を受けないようにするためです。



● ` ` とは

この例のポイントは、出力先のファイル名を、うまく自動的に取り出しているところでしょうか。

出力先のファイル名は、変数OUTFILEで指定していますが、これは、

OUTFILE=`echo $i | sed 's:.*/::'`

というふうにして、代入しています。

変数代入の右辺に、「`」(バッククォート)で囲まれた部分がありますが、「` `」の中をコマンドとして、実行した結果で置き換わります。

$iにはURLが入っているので、sedでURLを書き換えた結果が、変数OUTFILEに代入されることになります。sedへ入力するデータが変数に入っているので、変数の内容を、echoコマンドでいったん標準出力へ出してから、パイプ経由でsedにそそぎこんでいるのです。



● (おまけ) basename
ファイル名部分を切り出す処理は、たぶん、basenameというコマンドを使っても、うまくいくと思います。すべてのプラットフォームのbasenameコマンドが、URLに対しても意図どおり処理してくれるのか、保証はできませんが・・・



OUTFILE=`basename $i`

(実行例)



% basename http://www.asahi.com/index.html
index.html



/usr/local/etc/hoge.confのようなフルパスから、ディレクトリ部分、ファイル名部分を切り出すのに、dirnameコマンド、basenameコマンドがあります。
basenameコマンドは、拡張子を取り除く機能もあります。



% basename index.html .html
index



● 「*」でマッチする範囲

sedでは s:.*/:: という置換処理をやっていますが、これは、URLの一番最後のファイル名部分を切り出してくる処理をやっています(というか不要な部分を削除しています。ちなみにデリミタを「:」にしています)。

http://rss.itmedia.co.jp/rss/1.0/topstory.xml

のときは、topstory.xmlの部分が切り出されてきます。

置換の検索文字列が「.*/」となっていますが、これにマッチするのは

http:/
http://
http://rss.itmedia.co.jp/
http://rss.itmedia.co.jp/rss/
http://rss.itmedia.co.jp/rss/1.0/

というように、何通りもあります。しかし、「*」によるパターンマッチのときは、もっとも長い文字列にマッチすることになっているので、http://rss.itmedia.co.jp/rss/1.0/ にマッチすることになります。


■ まとめ

シェルスクリプト・プログラミングの雰囲気を感じ取れてもらえたでしょうか。

sedとかshとか使いこなせるようになると、めんどくさい単純作業の多くは、自動で処理できるようになりますよ。


■ sedへの招待 全3話+1







2 件のコメント:

  1. >「#!」の2文字がファイルのマジックナンバー(ファイルの先頭にかかれている、ファイルの種別を表す記号のようなもの)です。
    「#!のことをshebangと呼びます」という解説もあるといいかも?
    http://www.in-ulm.de/~mascheck/various/shebang/

    返信削除
  2. 教えていただき、どうもありがとうございます。
    私は、「いげたびっくり」と呼んでます(笑)。
    ところで"#! /"の4バイトで、1つのmagic numberだとする場合もあるようです。
    ついでなので、もう1つ、ほとんどの人には何の役にも立たないネタ。
    Tcl/Tk(というマイナースクリプト言語・・・笑)では、以下のような書き方があります。shell scriptから、tclshやwishを起動するテクニックだそうです。
    #!/bin/sh
    # the next line restarts using wish \
    exec wish8.4 "$0" ${1+"$@"}
    ��ここから先は、Tclスクリプト)
    (1)まずは/bin/shがwish8.4をexecする
    (2)プロセスがwish8.4にすげ変わり、$0なので、このスクリプトをwish8.4が読み込む
    (3)#の行末に\があると、Tclインタープリタは、次の行をコメントの継続だと思うので、execの行は、wishでは実行されない。
    (4)exec以降の行は、Tclスクリプトとして、そのままTclインタープリタが読み込んでいく
    Tcl/Tkのサンプルプログラムでは、多用されているようです。

    返信削除