UNIX 講習会 (4)

目次へ

Last modified: Wed May 16 13:21:21 2001

2001/5/16


4. シェルの使い方 (2)

標準出力と標準エラー出力のつづき

リダイレクションの指定は順番によって違う。 次の2つはどうちがうのか?

$ ls >file 2>&1
$ ls 2>&1 >file

リダイレクションの有効範囲はパイプの中だけ。

$ ls /var/spool/cron 2>&1 | cat -n
$ ls /var/spool/cron | cat -n 2>&1 

シェルスクリプトと実行可能ファイル

ふつう、シェルを使うときは「対話的に」実行する。 シェルを引数なしで起動するとこうなる。しかし、 シェルに引数を与えると、シェルはそのテキストファイルの中身を あたかもキーボードから入力されたかのように実行する。 このようなファイルを「シェルスクリプト (shell script)」とよぶ。

シェルスクリプトは B shell (sh) でかく。実行方法には 2とおりある。

  1. % sh ファイル名
  2. 実行可能ファイルにして そのままコマンドみたいに実行。
実行可能ファイルには 2種類ある。 実行できるファイルは最初に「マジックナンバー」がかいてあって、 UNIX はこれで判断する。
  1. 機械語 (mode: 755)
  2. 「#!」で始まるファイル (mode: 755)
    このファイルの 2行目以降が #! 以降のプログラムの 第1引数に流れる。perl でも、cat でも。

シェルスクリプトの書き方

シェルスクリプトは基本的にコマンド列。実行する順にだらだら書けばよい。

ls -l; cat hoge
echo ore wa ore;
1行1コマンド。 1行に複数のコマンドを入れる場合は ; が必要。

シェルはプログラミング言語なので、変数も条件分岐もループもできる。

if
if コマンド; then コマンド; fi
if コマンド; then コマンド1; else コマンド2; fi
コマンドが正常に終了すれば true とみなす (exit 0)、then 以下を実行。
コマンドがエラー終了すれば false とみなす (exit != 0)。
#!/bin/sh
if ls hogehoge; then
  echo "hogehoge はあるよ";
else
  echo "hogehoge はないよ";
fi

test は 条件式によって終了状態を変える。これを 使うと、if でもっと複雑な条件かける :

test
条件判断 例: test "$1" = 234
test \( "$a" eq "hoge" \) -o \( "$a" eq "moge" \)
test -f "$f"

スクリプト lessdir:

#!/bin/sh
if test -d $1; then
  ls $1;
else
  less $1;
fi
$1, $2, $3 ... には、シェルスクリプトを実行したときの コマンド引数が入る。
じつは test は [ ] で代用できる :
#!/bin/sh
if [ -d $1 ]; then
  ls $1;
else
  less $1;
fi
でも、ls | grep abc | lessdir とかが動くようになってほしい :
#!/bin/sh
if [ ! "$1" ]; then
  less;               # 引数が省略された場合
elif [ -d $1 ]; then
  ls -F $1;           # 引数があって、それがディレクトリだった場合
else
  less;               # 引数があって、それがファイルだった場合
fi

問: なぜ 最初の $1 には "" をつけるが必要あるか?

# はシェルではコメントとみなされる。だから最初の一行は無視される。

さらに、exec を使うと、生成されるプロセスが増えなくてすむ。

#!/bin/sh
if [ ! "$1" ]; then
  exec less
elif [ -d $1 ]; then
  exec ls -F $1
else
  exec less
fi

問: このスクリプトにはバグがある。どこか。

for をつかうと、複数の word に対してなにかできる。

for
for 変数名 in リスト; do コマンド1; .. ; done
たとえばこんなの
for i in *; do
  echo "Rename: $i"
  mv $i $i.2
done
さらに、` ` を使うと、コマンドの出力を word列として とり出せる (改行は除かれる)。
for i in `cat filelist`; do
  echo "Rename: $i"
  mv $i $i.2
done
シェル変数は
i=1
hoge=agagag
とかやる。=の前後にスペースあけないこと。 コマンドとみなされるぞ!

シェルで計算するには expr を ` ` で使えば OK。 exprはじつはシェルのコマンドではない。シェルは計算も一人ではできない。

i=1
while [ $i -le 10 ]; do
  echo $i
  i=`expr $i + 1`   # expr の引数は開けること!
done
一行でもかけるよ。
i=1;while [ $i -le 10 ];do echo $i;i=`expr $i + 1`;done

スペースに注意せよ! 開けないと引数とみなしてくれない。

例 : if [ $x = 10 ] then echo $x;exit(○)
if[$x = 10] then echo$x; exit(×)
また、シェル変数を使うときの = は前後を開けてはならない。
例 : x="abc" (○)
x = abc (×)

シェルスクリプトのまとめ

シェルスクリプトでよく使うコマンド (内部コマンドも外部コマンドもある)

[ ... ]
条件判断。その終了状態によって真偽を判定する。
       例: [ "$1" = 234 ]
       [ \( "$a" eq "hoge" \) -o \( "$a" eq "moge" \) ]
       [ -f "$f" ]
       
if
if コマンド; then コマンド; fi
       if コマンド; then コマンド1; else コマンド2; fi
       if コマンド; then コマンド1; elif コマンド2; then ...
echo
表示
expr
計算。この結果は ` ` で受けとる。
       例: `expr 2 * 3 + $a`
       
` ... `
値をとる。
例: `ls`
eval
評価
for
for 変数名 in リスト; do コマンド1; .. ; done
case もの in
       パターン1) コマンド1; .. ;;
       パターン2) コマンド2; .. ;;
       esac

シェルスクリプトでパターンにマッチさせたいときはこれを使うしかない。

while
while コマンド; do コマンド1; .. ; done
$?
終了ステータス
$$
現在の PID
$1, $2, ..., $*, $@
コマンド引数。$* と $@ は単体だと同じだが、"$*" と "$@" はちがう。

知っていると便利なテキスト処理コマンド (斜体 は引数。[ ] 内は省略可能)。

cat [-n] [file1 file2 ...]
ファイルを連結し表示する。
grep [-ivl] pattern [file1 file2 ...]
指定されたパターンを含む行をとり出す。 オプション -i は case ignore, -v は 含まない行, -l はファイル名のみ表示。
head [-行数] [file1 file2 ...]
ファイルの先頭から指定された数の行をとり出す。 省略時は 10行。
tail [+行番号] [-行数] [file1]
ファイルの末尾をとり出す。+指定ではその行番号以降、 -指定だと末尾から指定行数。
wc [file1 file2 ...]
ファイル中の行数、単語数、文字数を表示。
tr [-c] list1 list2
ファイル中の list1 にある文字を list2 にある文字に おきかえる。-c は list1 以外。
sed -e script [file1 file2 ...]
ストリームエディタ。script にいろいろ書くことによって、 テキストの各行を操作できる。s/パターン1/パターン2/ は置換。
awk script [file1 file2 ...]
awk。
cut [-f fields] [file1 file2 ...]
テキスト中の指定したフィールド (タブで区切られた) を切り出す。
join file1 file2
複数のカラムを結合する。
sort [-rn] [file1]
テキストを行ごとにソート。オプション -r は逆順。 -n は番号でソート。
uniq [-c] [file1]
あらかじめソートされた行の重複行をひとつにまとめる。 オプション -c はその時にカウントをつける。
fold [-桁数] [file1 file2 ...]
テキストを指定された桁数で折り返す。
expand [file1 file2 ...]
テキスト中のタブをスペースに展開する。
unexpand [file1 file2 ...]
テキスト中のスペースをタブにまとめる。
nkf {-e | -s | -j} [file1 ...]
漢字コードを変換する。

このほかに知っていて損はないもの。

find path [cond1 cond2 ...]
与えられたパス以下にある、条件に合致するファイル名をすべて表示する。
-name パターン
与えられたパターンに合致するファイル名。

問: find -name *.txt間違い。なぜか?

-atime 日数
指定された日数以内にアクセス(a:読み/変更)されたファイル。
-mtime 日数
指定された日数以内に変更(m)されたファイル。
-ctime 日数
指定された日数以内に作成(c)されたファイル。
-user ユーザ
指定されたユーザの所有するファイル。
-group グループ
指定されたグループのファイル。
-path パターン
指定されたパターンに一致するパスにあるファイル。
-type {f | d | l}
ファイル、ディレクトリあるいはリンク。
(, ), !, -a, -o
条件の論理演算。

また、以下のアクションが指定可能。

-exec コマンド ;
すべての見つかったファイル名に対してコマンドを実行。 「{}」の部分が見つかったファイルのパスに展開される。

問: find . -exec ls ;間違い。なぜか?

xargs コマンド [引数1 引数2 ...]
標準入力の各行を引数に展開してコマンドに渡す。 find と組み合わせて使う。

各コマンドの詳細はmanすること。


サンプルを読む

#!/bin/sh
#  lpo:
#	-P printer	: speficy printer
#	-m		: speficy twocolumns
#	-1		: onecolumn
#	-p		: onecolumn, portlait-duplex
#	-2		: twocolumn, landscape-duplex

set -- `getopt P:K:Rtm1d2 $*`
if [ $? != 0 ]; then
    echo "usage: lpo [-Pprinter] [-m1d2] [file ...]"
fi

p=pst
m=0
a=-np
c=1
t=0
r=0

while [ $1 != -- ]; do
  case $1 in
  -P)	shift; p=$1;;
  -K)   shift; c=$1;;
  -R)   r=1;;
  -t)   t=1;;
  -m)	m=1;;
  -1)   m=0; a=-p; p=ps;;
  -p)   m=0; a=-p; p=psdp;;
  -2)   m=1; p=psdl;;
  esac
  shift;
done
shift;

spool() {
  if [ $m = 1 ]; then
    psmulti -nodecor $* | lpr -P$p -K$c;
  else
    lpr -P$p -K$c $*;
  fi
}

if [ $# = 0 ]; then
  if [ $t = 1 ]; then
    a2ps $a | spool; exit;
  else
    spool; exit;
  fi
fi

for i in $*; do
  case $i in
  *.ps)
    spool $i;;
  *.ps.gz)
    gzip -dc $i | spool;;
  *.pdf)
    tmp=/tmp/prt.$$
    acroread -toPostScript -size a4 -shrink -pairs $i $tmp
    spool $tmp
    rm $tmp;;
  *.dvi)
    /usr/local/bin/dvips -f $i | spool;;
  *.gif)
    giftopnm $i | pnmtops | spool;;
  *)
    if [ $r = 1 ]; then spool $i; exit; fi
    if [ "X$a" = "X-np" ]; then m=0; fi
    a2ps $a $i | spool;;
  esac
done

^ front   新山 祐介 euske@cl.cs.titech.ac.jp