Posted at

Shellの基本を学ぶ(5)制御構文


while

test が true の間実行

while test; do

;; some code
done


until

test が false の間実行

while test: do

;; some code
done


for

それぞれのワードがVARに入る。ワードのクオートは不要

for VAR in WORDS; do

;; some code
done

例えば次のようなコマンドの実行。lsの実行結果を一つづつループで回している。

$ for i in $(ls); do echo $i; done

answer
bin
hello_world.sh
hw
hw.h
logfile
ls.txt
output.sh
tn.sh
variables

ちなみに、ダブルクオートで変数をくくると1つの変数として見なされる。

$ for i in hello world tsuyoshi; do echo $i; done

hello
world
tsuyoshi
$ for i in "hello world tsuyoshi"; do echo $i; done
hello world tsuyoshi


Sample

サンプルは、引数を指定すると、該当の拡張子の前に第3引数の拡張子を挿入してくれるプログラム。

ちなみに、echo mv ...のパートはデバッグテクニックらしく、これで一旦実行して、実際に思った通りだったら、echoを外して実行する。特にforが該当の拡張子だけで回るのはカッコいい。

forsample

#!/bin/bash

if [[ $# -ne 2 ]]; then
echo "Two parameters requires"
fi

for f in *"$1"; do
base=$(basename "$f" "$1")
echo mv "$f" "${base}$2.$1"
done


実行

実行時のtouch の使い方もカッコいい

$ touch file{1..10}.txt

$ touch image{1..5}.img
$ ls
file1.txt file3.txt file6.txt file9.txt image3.img
file10.txt file4.txt file7.txt image1.img image4.img
file2.txt file5.txt file8.txt image2.img image5.img
$ ./forsample txt bak
mv file1.txt file1.bak.txt
mv file10.txt file10.bak.txt
mv file2.txt file2.bak.txt
mv file3.txt file3.bak.txt
mv file4.txt file4.bak.txt
mv file5.txt file5.bak.txt
mv file6.txt file6.bak.txt
mv file7.txt file7.bak.txt
mv file8.txt file8.bak.txt
mv file9.txt file9.bak.txt


C-Style Loop

for (( INIT; TEST; UPDATE )); do

;; some code
done

counter

#!/bin/bash

for (( i=0; i<10; ++i )); do
echo "number ${i}"
done


実行

$ ./counter

number 0
number 1
number 2
number 3
number 4
number 5
number 6
number 7
number 8
number 9

ワンライナーでやろうと頑張ってみてうまくいかなかったが、do の後に;をつけていたのを削除するとうまくいった。

$ for (( i=0; i<10; ++i )); do echo "number ${i}"; done

number 0
number 1
number 2
number 3
number 4
number 5
number 6
number 7
number 8
number 9

drawline

サンプルが面白かったので、そのまま写経してみた。

#!/bin/bash

if [[ ! $1 ]]; then
echo "need line length argument" >&2
exit 1
fi

if [[ $1 =~ ^[0-9]+$ ]]; then
length="$1"
else
echo "Length has to be a number" >&2
exit 2
fi

char="="
if [[ $2 ]]; then
char="$2"
fi

line=
for (( i=0; i<length; ++i )); do
line="${line}${char}"
done

printf "%s\n" "$line"
exit 0


実行

$ ./drawline 10

==========
$ ./drawline 10 --
--------------------

ここでポイントは、最後の printf "%s\n" "$line" でこれが printf "$line\n" だと Substitution なので、最初の文字が-の場合、オプションと認識されてうまくいかない。フォーマットを使うことで回避できている。


Break and Continue


  • break: ループをやめる

  • continue: 現在のイテレーションをスキップして、次から始める

他の言語と同じ。for, while, until で使える。次のサンプルは、ファイルの中をサーチして、該当の行が来たら表示するもの。

mysearch

#!/bin/bash

if [[ ! $1 ]]; then
echo "First argument missing" >$2
exit 1
fi

flag=
while read -r; do
if
[[ $REPLY =~ $1 ]]; then
echo "$1 found: $REPLY"
flag="done"
break
fi
done

if [[ ! $flag ]]; then
echo "$1 not found."
fi

exit 0

data.txt

This is the text.

You need to find a text.
It might be foo.
Also might be bar.
But it is crosstrek.
Keep on loving Subaru.
See you soon.

Regards,
Tsuyoshi


実行

$ ./mysearch crosstrek < ./data.txt

crosstrek found: But it is crosstrek.
$ ./mysearch wrx < ./data.txt
wrx not found.

例によって元のサンプルにセンスを感じたので写経。$REPLY の使い方が渋い

他の Pre-defined の変数はこちら。9.1. Internal Variables

デフォルトの変数を使うことで、シンプルに感じる。

#!/bin/bash

if [[ ! $1 ]]; then
echo "First argument missing" >&2
exit 1
fi

found=""

while read -r; do
if
[[ ! $found ]]; then
if
[[ $REPLY =~ $1 ]]; then
found="yes"
else
continue
fi
fi
echo "$REPLY"
done

exit 0


実行

最後の行が表示されないが、それはもうすぐわかるらしい。

$ ./stripto crosstrek < data.txt

But it is crosstrek.
Keep on loving Subaru.
See you soon.

Regards,


case


case WORD in
PATTERN1)
some code;;
PATTERN2)
some code;;
:
esac

他の言語と同じ


Sample

animals

#!/bin/bash

case $1 in
tiger)
echo "Super cool!";;
carp)
echo "So so";;
giant)
echo "Boo!.";;
*)
echo "I don't think about it";;
esac


実行

$ ./animals tiger

Super cool!
$ ./animals giant
Boo!.
$ ./animals star
I don't think about it


サンプル2

もうすこし複雑なパターンマッチも可能な様子

filetype

#!/bin/bash

case $1 in
*.png|*.jpg|*.gif)
echo "$1 is image";;
*.cs|*.java|*.js|*.sh)
echo "$1 is programming language source code";;
*)
echo "Don't ask me, I don't know";;
esac

exit 0


実行

$ ./filetype some.jpg

some.jpg is image
$ ./filetype Program.cs
Program.cs is programming language source code
$ ./filetype some.txt
Don't ask me, I don't know


Command Groups

{} で括られたコマンド。複数のコマンドを一つのステートメントに入れる。I/O のリダイレクトをグループに適用できる。if や while ループに適用できる。リターンステータスは、最後のコマンド実行の結果になる。

{cmd; cmd2; cmd3;} というスタイル。セミコロンで新しい行を表現する。ブレースの周りにはスペースが必要。セミコロンで終了すること。


|| and &&

論理演算ではない用途でも使う。コマンドの実行結果によって、後続のコマンドの実行を決める。


&&

先行したコマンド実行が成功した時のみ実行する。例えば次のはディレクトリ作成が成功した時のみ、そのディレクトリに移動する。

$ mkdir newdir && cd newdir


||

直前のコマンドが失敗した時のみ、次のコマンドを実行する。次のコマンドは、引数がない時のみ、エラーメッセージを表示する。

[[ $1 ]] || echo "missing argument">&2

例えば次のスクリプトは常に異常終了する。$1があったら、それはそれで正常終了だし、echoでエラーメッセージを表示しても、表示自体は成功するので、常に、exit 1になる。

[[ $1 ]] || echo "missing argument" >&2 && exit 1

こういう時には、コマンドグルーピングを使う

[[ $1 ]] || { echo "missing argument">&2; exit 1;}


サンプル

先ほどのサンプルを改造する。検索した文字列のある行以降を表示する。前は、最後の行が表示されない問題があった。その原因はwhile read -r; do で最後のEOFを判断しているが、最後の行がTsuyoshiEOFとなっている場合は、ここでtrueとなるため、最後の行が表示されない。read -f が false になった時のみ、[[ $REPLY ]] を評価する。もし、他に文字列が含まれていたらループを実行を続行する。

他には、コマンドグループを使って、文字列チェックを一業で記述している。

#!/bin/bash

[[ $1 ]] || { echo "First argument missing" >&2; exit 1; }

found=""

while read -r || [[ $REPLY ]]; do
if
[[ ! $found ]]; then
if
[[ $REPLY =~ $1 ]]; then
found="yes"
else
continue
fi
fi
echo "$REPLY"
done

exit 0


実行

ちゃんと最後の行が表示されている。

$ ./stripto crosstrek < data.txt

But it is crosstrek.
Keep on loving Subaru.
See you soon.

Regards,
Tsuyoshi
invincible:forloop ushio$ ./stripto
First argument missing