スクリプト処理関係

書式

パイプ

縦棒(|)で挟んで2つのシェルコマンドを記すと、1つ目のシェルコマンドの出力を2つ目のシェルコマンドの入力として渡すことができる。2つ目のコマンドが受け取った入力を扱うには、readで標準入力を読めばいい。

【例】
$ cat test.sh
#!/bin/bash

#標準入力の内容を変数inputに代入
read input

echo $input:$input
$ echo 'hoge' | bash test.sh
hoge:hoge

変数定義

参考:15

【書式】
変数名=

【例】
$ echo $HOGE

$ export HOGE=foo
$ echo $HOGE
foo

変数名= との間は空けないこと。定義は関数の内外を問わずスクリプト内全体で有効(全てグローバル変数)。

変数を消去するにはunsetコマンドを用いる。

【書式】
unset 変数名

【例】
$ echo $HOGE
foo
$ unset HOGE
$ echo $HOGE

$ 

変数名の前に$は付けない。

クォート記号を含む値を変数に定義し、変数を介してコマンドの引数を指定するとうまくいかないらしい。クォートで囲む値の中身だけを別途変数に定義し、クォート記号は直接記述するようにすればうまく行った[16]

$ cat sync_ng.sh
#!/bin/sh
OPT='-avz -e "ssh -i /home/user/.ssh/id_rsa"'
HOST="user@hoge.com"
PATH="/home/user/foo.txt"
echo "rsync "${OPT} ${HOST}:${PATH} ${PATH}
rsync ${OPT} ${HOST}:${PATH} ${PATH}
$ ./sync_ng.sh
rsync -avz -e "ssh -i /home/user/.ssh/id_rsa" user@hoge.com:/home/user/foo.txt /home/user/foo.txt
Unexpected remote arg: user@hoge.com:/home/user/foo.txt
rsync error: syntax or usage error (code 1) at main.c(1213) [sender=3.0.6]
# コマンドをechoすると正しいようだが、エラー終了してしまう

$ cat sync_ok.sh
#!/bin/sh
OPT="-avz"
AUTHKEY="/home/user/.ssh/id_rsa"
HOST="user@hoge.com"
PATH="/home/user/foo.txt"
echo "rsync "${OPT}" -e ""${AUTHKEY}" ${HOST}:${PATH} ${PATH}
rsync ${OPT} -e "${AUTHKEY}" ${HOST}:${PATH} ${PATH}
$ ./sync_ok.sh
rsync -avz -e /home/user/.ssh/id_rsa user@hoge.com:/home/user/foo.txt /home/user/foo.txt
receiving incremental file list
foo.txt

sent 14 bytes  received 43 bytes  16.29 bytes/sec
total size is 42596  speedup is 747.30
まとまった分量の文字列出力や変数代入を行う際、ヒアドキュメントを使うと便利かもしれない33
$ cat test.sh
#!/bin/bash
HENSU=$(cat << EOS
徒然なるままに日暮らし
硯に向かひて
心にうつりゆくよしなしごとを
そこはかとなく書きつくれば
あやしうこそものぐるほしけれ。
EOS
)
echo "$HENSU"
$ bash test.sh
徒然なるままに日暮らし
硯に向かひて
心にうつりゆくよしなしごとを
そこはかとなく書きつくれば
あやしうこそものぐるほしけれ。

文字出力(echo, printf)

echoコマンドは単純な文字列出力に、printfコマンドは指定した書式で文字列を出力するのに使える。

【書式】
echo オプション 文字列
【返り値】
成功→0
エラーあり→0より大きな値
  
-n
末尾に改行文字を出力しない。
-e
エスケープを認識させる。\n(改行)などを出力する時に使う。
printf 書式 文字列
  
書式 内容 例・備考
\n 改行
# printf abc\ndef
abc
def
	
%s
%nums
指定引数の中身(文字列)
# name="Albert"; printf "My name is %s.\n" $name
My name is Albert.
# name="Albert"; printf "My name is %4s.\n" $name
My name is Albert.
# name="Albert"; printf "My name is %8s.\n" $name
My name is   Albert.
# name="Albert"; printf "My name is %-8s.\n" $name
My name is Albert  .
	
指定引数を文字列として出力する。複数の引数を指定した場合、%sは登場順に1番目の引数、2番目の引数...と参照する。"s"の前に数値numを記せば、少なくともnum文字の幅が確保される。文字列長がこれより短ければ右詰で余った左側は空白が補完される(例:"%5s"" abc" 5文字幅)。numの前にハイフンを記せば左詰となる。
%d
%numd
%strnumd
指定引数の中身(数値)
# i=1; printf "The value of \$i is %d.\n" $i
The value of $i is 1.
# i=1; printf "The value of \$i is %5d.\n" $i
The value of $i is     1.
# i=1; printf "The value of \$i is %05d.\n" $i
The value of $i is 00001.
	
指定引数を数値として出力する。複数の引数を指定した場合、%sは登場順に1番目の引数、2番目の引数...と参照する。"d"の前に数値numを記せば、num文字分だけ幅を確保する(例:"%5d"→" 123" 5桁幅)。更に、数値の前に"0"を記せば上位空桁は0で埋められる(指定しない場合は空白で埋められる)。

文字コードで出力文字を指定する方法34

$ echo -e "\x41"
A

参考文献・サイト

文字列の連結

複数の変数に格納された値を結合するには、単に複数の変数名を続けて記述すれば良い。

例:str1に格納された文字列と、str2に格納された文字列を結合したものをstr12に格納して標準出力に出力する。

shell> str1="hogehoge"
shell> str2="fugofugo"
shell> str12=$str1$str2
shell> echo $str12
hogehogefugofugo
  

変数に文字列を連結する際は、変数名と連結する文字列を区別するため、変数名は{...}で囲む。

例:str1に格納された文字列の後に、"aheahe"を連結したものをstr3に格納して標準出力に出力する。

shell> str1="hogehoge"
shell> str3="${str1}aheahe"
shell> echo $str3
hogehogefugofugo
  

参考サイト:複数の変数を連結する(ITpro シェル・スクリプト・リファレンス)

文字列カウント

${#変数名}で変数に格納された文字列の文字数が得られる21。配列の要素数をカウントするのと同じ。

$ str="hogehoge";echo ${#str}
8

配列変数

一括指定
配列変数名=(要素1 要素2 要素3 ...)
個別指定
配列変数名[インデックス1]=値1
配列変数名[インデックス2]=値2
...

なお、配列キー(インデックス)に使えるのは整数(非負整数?)のみ

要素追加15
配列名+=()
要素追加15

配列変数名をそのまま指定すると、最初の要素だけが返る。 配列変数全体を取得するには変数名[*]または変数名[@]と指定。 配列変数の個別要素を呼び出すには変数名[配列番号]と指定。未定義の番号を指定すると空が返る。 配列変数内の要素数は#変数名[*]。 以下にこれらの事例。

shell> a=(1 2 c d 5)
shell> echo $a
1
shell> echo ${a[3]}
d
shell> echo ${a[*]}
1 2 c d e
shell> echo ${#a[*]}
5
【例】拡張子が".html"であるファイル名を配列変数に代入し、各要素、個数を表示する
$ files=(`ls *.html`);echo ${files[*]};echo ${#files[*]}
index.html inquiry.html products.html
3
  
配列のマージ[17]
【例】
array1=(a b c)
array2=(d e f)
array3=(${array1[@]} ${array2[@]})

echo "${array3[@]}"
→ a b c d e f
配列変数のコピー
$ cat test.sh 
#!/bin/sh
array=(north south east west)
array_copyok=(${array[*]})
array_copyng=$array
echo ${array[*]}
echo ${array_copyok[*]}
echo ${array_copyng[*]}
$ ./test.sh 
north south east west
north south east west
north
配列変数の要素並べ替え
sortコマンドを使うとよい28
【例】ファイルリストをバージョン順に並べ替え
$ cat test.sh 
#!/bin/sh
array=(hoge.3 hoge.2 hoge.10 hoge.11 hoge.1)
array_asc=($(for item in ${array[*]};do echo $item;done | sort -V))
echo ${array[*]}
echo ${array_asc[*]}
$ ./test.sh 
hoge.3 hoge.2 hoge.10 hoge.11 hoge.1
hoge.1 hoge.2 hoge.3 hoge.10 hoge.11
空白を含む要素がある場合の扱い

空白を含む要素があった場合、ループ処理で空白が要素の区切りと判断されてしまい意図した動作にならない場合がある(環境による?)。少なくともMac 10.11のTerminal(GNU bash, version 3.2.57(1)-release (x86_64-apple-darwin15))では以下のようになった。

$ cat test.sh
#!/bin/sh
STRS=("This is a pen." "This is an apple." "This is a pooh doll")
for STR in ${STRS[*]} # 繰り返し処理→for
do
  echo $STR
done
$ ./test.sh
This
is
a
pen.
This
is
an
apple.
This
is
a
pooh
doll.

whileを使って全ての配列キーを参照させて処理するようにすれば、空白を要素区切りとしてみなされることなく処理できた。

$ cat test.sh
#!/bin/sh
STRS=("This is a pen." "This is an apple." "This is a pooh doll")
i=0
while [ $i -lt ${#STRS[*]} ] # 条件を満たす間繰り返す→while
do
  echo ${STRS[$i]}
  i=`expr $i + 1` # i を一つ増やす演算を行う→expr
done
$ ./test.sh
This is a pen.
This is an apple.
This is a pooh doll.
配列にある値が含まれるかどうかのチェック

「全配列要素を出力し、grepを使ってチェックしたい値でフィルタリングし、wlで語数を数え、結果が0なら存在しない、1以上なら存在する」などのチェック方法がある31

$ cat test.sh
#!/bin/bash
array=(red blue green yellow purple)
value='blue'
echo "array: "${array[@]} 
echo "value: "$value
if [ `echo ${array[@]} | grep -o $value | wc -w` -eq 0 ]
then
        echo "The value '$value' was not found in the array."
else
        echo "The value '$value' was found in the array."
fi
$ bash test.sh
array: red blue green yellow purple
value: blue
The value 'blue' was found in the array.

演算

数値演算

普通に「a=1+1」のように記述しても演算結果は代入されない("1+1"という文字列が$aに代入される)。演算結果を代入するには expr コマンドを使う必要がある[3]。あるいは$((計算式))

$ expr 1 + 1 # 演算子の前後には空白を入れる
2
$ expr 1+1
1+1
$ echo $((1 + 1))
2
$ echo $((1+1)) # こちらの書式は演算子の前後には空白を入れなくても機能する
2

なお小数は扱えないので、小数を含む演算を行いたい場合はbcを使う。bcexprとは異なり、パイプを使うなどして演算内容を渡す。

$ expr 10 + 0.1
expr: non-numeric argument
$ echo $((10+0.1))
-bash: 10+0.1: syntax error: invalid arithmetic operator (error token is ".1")
$ echo "10 + 0.1" | bc
10.1

bcコマンドで小数点以下の桁数を指定するにはscaleを用いる。scaleが働くのは割り算で割り切れない場合などに限られるよう。

$ echo "scale=3;10 + 0.1" | bc
10.1 # 10.100 とはならない
$ echo "scale=1;10 + 0.001" | bc
10.001 # 10.0 とはならない
$ echo "scale=1;10/3" | bc
3.3
$ echo "scale=3;10/3" | bc
3.333

有効桁数はlengthで得られる。

$ echo "10+0.1;length(10 + 0.1)" | bc
10.1
3
$ echo "10+0.001;length(10 + 0.001)" | bc
10.001
5

文字列演算

正規表現を使った文字列判定も可能[7]。返り値は以下の通り。

exprによる文字列演算の返り値
部分表現 "\( ... \)" の有無 マッチ マッチしない
あり 最初の部分表現にマッチした文字列 ヌル文字列
なし マッチした文字列の文字数 0
【書式】
expr 文字列 : 正規表現
expr match 文字列 正規表現

【例】
$ expr "hogefugafoo" : "hoge"
4 # 先頭の部分一致はマッチしたと判定される
$ expr "hogefugafoo" : "fuga"
0 # 先頭以外の部分一致はマッチしたと判定されない
$ expr "hogefugafoo" : ".*fuga.*"
11 # 前後に0文字以上の任意文字列を示す".*"を付ければ文字列全体がマッチ

$ cat test.sh
#!/bin/sh
if expr "$1" : ".*\.txt" >/dev/null; then
  echo $1." seems a text file."
else
  echo $1." seems not a text file."
fi
$ ./test.sh hoge.txt
hoge.txt seems a text file.
$ ./test.sh hoge.bin
hoge.bin seems not a text file.

なお、この正規表現には自動的に先頭一致(^)の意味で解釈されるため、明示的に^を正規表現の先頭に付加すると、「unportable BRE」というエラーが表示される。但し、処理は正常に行われるみたい8

文字検出

【書式】
expr index 文字列 文字セット

文字列の中に文字セット中のいずれかの文字が見つかれば、その最初に見つかった位置(先頭が1)を返す。いずれの文字も見つからなければ 0 を返す。

【例】
$ expr index "hoge" "abc"
0 # みつからなかった
$ expr index "hoge" "abcde"
4
$ expr index "hoge" "abcdg"
3
$ expr index "hoge" "abcdh"
1

文字列長

【書式】
expr length 文字列

【例】
$ expr length "hoge"
4

部分文字列

【書式】
expr substr 文字列 開始位置 長さ

文字列開始位置番目から、最大長さ文字の部分文字列を返す。開始位置長さに自然数以外の値を指定した場合はヌル文字列を返す。

【例】
$ expr substr "Conguratulations" 6 5
ratul

論理演算子

[10]

testの条件節で使う場合は、以下の記述も使用可能。詳細はtestの項目参照。

繰り返し処理(for)

書式:for 変数名 in リスト名 do コマンド done

例:配列変数「val」の内容を出力

$ cat test.sh
#!/bin/sh
val=(a b c d e)
for loop in ${val[*]}
do
  echo $loop
done
$ chmod +x ./test.sh
$ ./test.sh
a
b
c
d
e

なお、配列要素に空白を含む場合、リスト名部分はダブルクォートで囲む(そうしないと要素が空白で分割され空白の前後で別々の要素とみなされてしまう)

$ cat test.sh
#!/bin/sh
val=("foo hoo" "hello youtube")
for loop in ${val[*]}
do
  echo $loop
done
$ chmod +x ./test.sh
$ ./test.sh
foo
hoo
hello
youtube

例2(拡張子指定によるファイル名一括変換)
for nm in *.txt
do
mv $mm $[nm%.txt].doc done
…でいいのかな?

【例3】2011で始まるファイル・ディレクトリが増えてきたので
      2011ディレクトリを作ってこの中に移動
$ mkdir 2012
$ ls -d 2012?*
20120105  20120210  20120314  20120418  20120626  20120815  20120924    20121018    20121107
20120110  20120213  20120316  20120425  20120629  20120821  20120925    20121019    20121108
...
$ for file in `ls -d 2012?*'`;do mv $file 2012;done
$ ls 2012
20120105  20120210  20120314  20120418  20120626  20120815  20120924    20121018    20121107
20120110  20120213  20120316  20120425  20120629  20120821  20120925    20121019    20121108
...

条件付き繰り返し(while)

【書式】
while 条件文
do
  処理内容
done
  

条件文にはtestコマンドが使われることが多い。

【例】1から10までの値を出力する
$ cat test.sh
#!/bin/sh
i=1
while [ $i -le 10 ] # $i が10以下の間は処理を繰り返す
do
  echo $i
  i=`expr $i + 1` # 演算にはexprコマンドを使う
done
$ chmod +x test.sh
$ ./test.sh
1
2
3
4
5
6
7
8
9
10
# 以下のように記せば同じ内容をコマンドライン上で直接実行できる
$ i=1;while [ $i -le 10 ];do echo $i;i=`expr $i + 1`;done
  

パイプを使うと子プロセルでシェルが呼び出され、子プロセルが終わると変数は元に戻る。従って、パイプを使ってwhileを呼び出すと、whileループ内で定義した変数は、ループ外には反映されない。

$ cat ./test.sh
#!/bin/sh
count=0
ls -l | while read line;do
  count=$(($count + 1))
  echo $count
done
echo "----"
echo $count
$ ./test.sh
1
2
3
4
5
6
7
8
9
10
11
12
13
----
0 ←whileループの外ではcount変数の値は元の0のまま
【例】
$ i=0;while [ $i -lt 12 ];do date "+awstats%m%Y.www.example.org.txt" --date "2014/04/01 $i month";i=`expr $i + 1`;done
awstats042014.example.org.txt
awstats052014.example.org.txt
awstats062014.example.org.txt
awstats072014.example.org.txt
awstats082014.example.org.txt
awstats092014.example.org.txt
awstats102014.example.org.txt
awstats112014.example.org.txt
awstats122014.example.org.txt
awstats012015.example.org.txt
awstats022015.example.org.txt
awstats032015.example.org.txt
【例】
$ i=0;while [ $i -lt 12 ];do printf "%02d¥n";i=`expr $i + 1`;done
01
02
03
04
05
06
07
08
09
10
11
12  

一括でアップロード(ftp)

Mac OS X(10.4.11)でniftyのHPコンテンツUpload用FTPサイトにアクセスすると、最初のget,put,lsなどのftpコマンド実行時、反応が返ってくるまで1分程度かかる→passiveモードを指定すればこの問題は起こらなくなった。 それを待って次の処理をするのは扱いにくい(待っている間他のことをしていると、返答後アイドル状態が300秒続いて接続が切られてしまう)。そこでシェルスクリプトを利用。

#!/bin/sh
HOST_NAME="(FTPサーバのホスト名)"
USER_NAME="(FTPサイトのログインID)"
PASSWORD="(FTPサイトのログインパスワード)"
LOCAL_DIR="(ローカルのディレクトリ)"
REMOTE_DIR="(FTPサーバのディレクトリ)"
ftp -n ${HOST_NAME} << _EOF_
user ${USER_NAME} ${PASSWORD}
lcd ${LOCAL_DIR}
cd ${REMOTE_DIR}
put file1
put file2
put file3
...
bye
_EOF_
  

参考サイト:

上記事例は個別にアップロードするファイルを指定する例だが、 findコマンドなどを使って条件にあったファイル一覧を取得し、それをアップロードするようにしたい。例えば最近3日以内に修正されたカレントディレクトリ内のhtmlファイル(サブディレクトリは検索しない) find *.html -mtime -3 -maxdepth 0 など。

できたみたい。以下は3日以内に修正されたhtmlファイルおよびcssファイルをアップロードするスクリプト。

HOST_NAME="(FTPサーバのホスト名)"
USER_NAME="(FTPサイトのログインID)"
PASSWORD="(FTPサイトのログインパスワード)"
LOCAL_DIR="(ローカルのディレクトリ)"
REMOTE_DIR="(FTPサーバのディレクトリ)"

cd ${LOCAL_DIR}
FN_HTML=(`find *.html -mtime -3`)
FN_CSS=(`find *.css -mtime -3`)

ftp -n ${HOST_NAME} << _EOF_
user ${USER_NAME} ${PASSWORD}
lcd ${LOCAL_DIR}
cd ${REMOTE_DIR}
prompt
mput ${FN_HTML[*]} 
mput ${FN_CSS[*]}
bye
_EOF_
  

${変数名[*]} とすると配列変数の各要素を空白で区切ってつないだ1つの文字列として得られる。 ftpのpromptコマンドは、mputなどのコマンドで各ファイルの処理を都度尋ねるか、問い合わせせずに全部対象として処理するかを切り替える。

条件分岐(if)

if 条件;then ...;else ...;fi」で条件分岐の記述を行うことができる。なお条件の指定にはtestコマンド(またはその略記法)を用いる。else文を使うと、条件に当てはまらなかった場合の処理も記述できる。当てはまらない場合を更に分岐するには「elif」を使う9


【書式】
if 条件
then
  条件に適合した場合の処理内容
elif 条件
then
  条件に適合した場合の処理内容
  
else
  条件に適合しなかった場合の処理内容
fi

【例1】
if [ "$point" -ge 70 ]
then
  echo "Passed!"
else
  echo "Not Passed."
fi

コマンドラインに一連のコマンドとして入力するには
$ point=75;if [ "$point" -ge 70 ];then echo "Passed.";else echo "Not Passed.";fi
Passed.
$ point=65;if [ "$point" -ge 70 ];then echo "Passed.";else echo "Not Passed.";fi
Not Passed.

【例2】自分自身の行数が3行を越えているかどうか判定
$ cat test.sh
#!/bin/sh
THRE=3
WL=`wc -l $0 | gawk '{print $1}'`
if [ $WL -gt $THRE ]; then
  echo "Over ("$WL" > "$THRE")"
else
  echo "Not Over ("$WL" <= "$THRE")"
fi
$ ./test.sh
Over (8 > 3)
  

条件判断(test)

testコマンドおよび略記法については以下の通り。真(true)の場合の返り値は0、偽(false)の場合の返り値が1。

testコマンド 略記法 説明
test -e ファイル [ -e ファイル ] ファイルが存在(exist)すれば真
test -f ファイル [ -f ファイル ] ファイルが存在し通常ファイル(file)であるなら真
test ! -f ファイル [ ! -f ファイル ] ファイルが存在しないなら真
test -x ファイル [ -x ファイル ] ファイルが存在し実行可能(executable)であるなら真
test -d ファイル [ -d ファイル ] ファイルが存在しディレクトリ(directory)なら真
test -s ファイル [ -s ファイル ] ファイルが存在しサイズが0でなければ真
test -r ファイル [ -r ファイル ] ファイルが存在し読み取り可能(readable)であれば真
test -w ファイル [ -w ファイル ] ファイルが存在し書き込み可能(writable)であれば真
test ファイル1 -nt ファイル2 [ ファイル1 -nt ファイル2 ] ファイル1が存在し、ファイル2より新しい(newer than)なら真
test ファイル1 -ot ファイル2 [ ファイル1 -ot ファイル2 ] ファイル1が存在し、ファイル2より古い(older than)なら真
test -n 文字列
test 文字列
[ -n 文字列 ]
[ 文字列 ]
文字列が空でなければ真
$ cat test.sh
#!/bin/bash
STR1="hoge"
STR2=""

echo -n "STR1 ($STR1) ... "
if [ -n "$STR1" ]
then
  echo "not null"
else
  echo "null"
fi
echo -n "STR2 ($STR2) ... "
if [ -n "$STR2" ]
then
  echo "not null"
else
  echo "null"
fi
$ ./test.sh
STR1 (hoge) ... not null
STR2 () ... null
test -z 文字列 [ -z 文字列 ]
文字列が空であれば真
lsの結果を使ってディレクトリが空かどうかの判定ができる30
$ ls
hoge.txt
$ cat test.sh
#!/bin/bash
if [ -z "$(ls -A $1)" ]; then
   echo "Empty"
else
   echo "Not Empty"
fi
$ ./test.sh .
Not Empty
$ mkdir foo
$ ./test.sh ./foo
Empty
test 文字列1 = 文字列2 [ 文字列1 = 文字列2 ] 文字列1文字列2が一致していれば真
test 文字列1 != 文字列2 [ 文字列1 != 文字列2 ] 文字列1文字列2が一致しなれば真
test 文字列 =~ 正規表現 [ 文字列 =~ 正規表現 ] 文字列拡張正規表現に適合したら真[18] 正規表現を直接test文に書く場合、正規表現パターンは引用符などで囲まない。
$ cat test.sh
#!/bin/bash
STR1="abcde"
STR2="cdeab"
REGEXP1="^.*bcd.*$"
REGEXP2="^(abcde|fghij)$"
# 角かっこは二重にしないとうまく動作しなかった
if [[ $STR1 =~ $REGEXP1 ]] 
then
  echo "hit"
else
  echo "no hit"
fi
if [[ $STR2 =~ $REGEXP1 ]]    
then
  echo "hit"
else
  echo "no hit"
fi
if [[ $STR1 =~ $REGEXP2 ]] 
then
  echo "hit"
else
  echo "no hit"
fi
if [[ $STR2 =~ $REGEXP2 ]]    
then
  echo "hit"
else
  echo "no hit"
fi
$ ./test.sh
hit
no hit
hit
no hit
パターンに適合した部分は $BASH_REMATCH 配列変数で参照できる29
$ cat test.sh
#!/bin/bash
STR="--option=hoge"
PATTERN="^--([0-9A-Za-z]+)=(.*)$"

echo "Argument: $STR"
if [[ $STR =~ $PATTERN ]]
then
  echo "An option was specified."
  echo "  ${BASH_REMATCH[1]} = ${BASH_REMATCH[2]}"
fi
$ bash test.sh
Argument: --option=hoge
An option was specified.
  option = hoge
test 数値1 -eq 数値2 [ 数値1 -eq 数値2 ] 数値1数値2」(equal)なら真
test 数値1 -ne 数値2 [ 数値1 -ne 数値2 ] 数値1数値2」(not equal)なら真
test 数値1 -gt 数値2 [ 数値1 -gt 数値2 ] 数値1数値2」(greater than)なら真
test 数値1 -ge 数値2 [ 数値1 -ge 数値2 ] 数値1数値2」(greater or equal)なら真
test 数値1 -lt 数値2 [ 数値1 -lt 数値2 ] 数値1数値2」(less than)なら真
test 数値1 -le 数値2 [ 数値1 -le 数値2 ] 数値1数値2」(less or equal)なら真

条件を論理結合する方法は以下の通り。

testコマンド 略記法 説明
test ! 条件式 [ ! 条件式 ] (not)条件式が偽であれば真
test 条件式1 -a 条件式2 [ 条件式1 -a 条件式2 ] (and)条件式1条件式2の両方が真であれば真
$ cat test.sh
#!/bin/sh
if [ $# -eq 1 -a $1 = "-h" ];then
  echo "引数 -h だけが指定されました。"
else
  echo "引数は "$#" 個指定されました。"
fi
$ chmod +x test.sh
$ ./test.sh
./test.sh: line 2: [: too many arguments
引数は 0 個指定されました。
$ ./test.sh -h
引数 -h だけが指定されました。
$ ./test.sh -p
引数は 1 個指定されています。
$ ./test.sh -h -p
引数は 2 個指定されています。
test 条件式1 -o 条件式2 [ 条件式1 -o 条件式2 ] (or)少なくとも条件式1条件式2のどちらか一方が真であれば真

-a-o より優先順位が上。この優先順位を変更するには ( ) によるグルーピングを用いる。-a-o の場合は括弧の直前にバックスラッシュを記してエスケープ処理を行う必要がある。また、&& や || で記述することもできる。この場合、エスケープ不要[15]

$ cat test.sh
#!/bin/sh
A=""
B=""
# ($A が空文字ではない ことはない)かつ($B が空文字ではない ことはない)
if [ ! -n "$A" -a ! -n "$B" ];then
# =if [ -z "$A" -a -z "$B" ];then
  echo "All empty"
else
  echo "No"
fi
$ ./test.sh
All empty

複数条件分岐(case)

exprを用いると正規表現を使った条件判定が、caseを使うとワイルドカードによる条件判定が記述できる13exprについてはリンク先参照のこと。caseについては下記参照。

【書式】
case "変数名" in
  パターン1)
    処理内容1
    ;;
  パターン2)
    処理内容2
    ;;
  ...
  *) # 上記いずれの条件にも当てはまらなかった場合(if文のelseに相当)14
    処理内容x
    ;;
esac
*
任意の0文字以上の文字列(例:a*=aで始まる文字列)
?
任意の1文字(例:?b*=2文字目がbある文字列)
[文字クラス]
文字クラスに規定した文字のいずれか1文字。文字クラスは文字の列挙(例:[abc])、区間による記述(例:[a-z])に加え、否定(例:[!abc]または[^abc]=abcのいずれでもない、[!0-9]または[^0-9]=数字でない)も組み合わせ可能。 (例:[A-Z]*=大文字で始まる文字列)
|
選択(例:yellow|red|pink=yellow,red,pinkいずれかの文字列)

ファイルの存在判定

[-f ファイル名]
  

参考文献・サイト:

パス、ファイル名を取り出す

basename は指定したパス文字列からファイル名部分を取り出す。最後のスラッシュより後の文字列を取り出す操作に相当する。第2引数として拡張子(サフィックス)を指定すると、ファイル名の末尾から拡張子部分を除去する。

$ basename '/home/user/hoge.txt'
hoge.txt
$ basename '/home/user/hoge.txt' '.txt'
hoge

【例】拡張子が.gzであれば(ファイルの末尾から.gzを除去すると値が変わるのであれば)解凍を行う
if [ "`basename $FILE`" != "`basename $FILE '.gz'`" ];then
  gzip $FILE
fi
参照:iftestgzip
  

一方、dirname はパス部分を取り出す。最後のスラッシュ以降を削除した文字列を返す。

$ dirname '/home/user/hoge.txt'
/home/user
  

スクリプトで自身をパス、ファイル名を取得すると相対パスになっている。

[test.sh]
#!/bin/sh
echo dirname $0

$ ./test.sh
./test.sh
  

これを絶対パスにするにはpwdを併用するとよい24

【例】
$ cat test.sh
#!/bin/sh
echo $(cd $(dirname $0);pwd)"/"`basename $0`
$ ./test.sh
/home/user/test.sh
  

入力を読み込む

read は指定した入力から1行読み取る。-uオプションの指定がない場合は標準入力から、-uオプションの指定がある場合は-uオプションで指定したファイルディスクリプタから読み込む25

【書式】
read オプション 受領変数名...
-a 配列変数名
行内の各語は指定した配列変数名の要素に格納される。インデックス値は0からの連番。既に配列変数名が定義されていた場合は、内容がリセットされた上で格納される。このオプションを指定すると、受領変数名引数は無視される。
-d 行末文字
このオプションを指定すると、改行ではなく行末文字の1文字目が見つかった時点で行の読み込みを終える。
-e
標準入力が端末由来のものであった場合、行内容を取得するのにreadlineを用いる。
-n 読み込み文字数
読み込み文字数が読み込み文字数に指定した文字数に達した場合、行末に達していなくても行の読み込みを終える。つまり各行の読み込み最大文字数が読み込み文字数に制限される。
-p
プロンプトを標準エラー出力に表示する。その処理はあらゆる入力を読み込む前に行われ、末尾に改行は出力されない。プロンプトは入力が端末から来ている場合にのみ表示される。
-r
バックスラッシュをエスケープ文字として働かないようにする。バックスラッシュは行の一部であるとみなされ、バックスラッシュ+改行は行の継続であるとはみなされなくなる。
-s
サイレントモードにする。端末から入力されている場合、文字は画面に出力されなくなる。
-t
行の入力がで指定した時間内に完了しなければ読み込みを中断しエラーを返す。このオプションは端末から入力している場合、あるいはパイプから入力している場合には無視される。
-u ファイルディスクリプタ
ファイルディスクリプタから読み込む。
受領変数名
受け取った内容を格納する変数の変数名を指定する。指定がない場合の既定変数名は REPLY。
【例1】ファイルリストを出力
$ cat ./test1.sh
#!/bin/sh
ls -l | while read line; do
  echo $line
done
$ ./test1.sh
合計 69044
-rw-rw-r-- 1 user user   258 7月 12 16:25 test.txt
-rwxrwxr-x 1 user user   136 12月 2 14:46 test1.sh
-rwxrwxr-x 1 user user   136 12月 2 14:46 test2.sh
-rwxr-xr-x 1 user user 10230 11月 1 16:30 test.jpg

【例2】ファイルリストを出力(readを使わず同じ内容を実装したもの)
[test2.sh]
#!/bin/sh
for file in `ls`; do
  echo $file
done

$ ./test2.sh
test.txt
test1.sh
test.jpg
  

以下の方法でテキストファイルの内容を読み取って各行に処理を行うことができる26

BUFIFS=$IFS # 変数IFSの値をバックアップ
IFS=        # 変数IFSの値を空にする

exec 3< 入力ファイル名 # ファイルを入力とするファイルディスクリプタ3番を実行
while read FL 0<&3 # 3番の入力・エラーを0番(標準入力)にリダイレクトし、各行 $FL に代入
do
  処理内容
done
exec 3<&- # 3番の入力を標準入力に戻す

IFS=$BUFIFS # 変数IFSの値を書き戻す
  

確認プロンプトを表示することにも使える27

$ cat test.sh
#!/bin/sh
read -p "本当に実行していいですか?[Y/n] " -n 1 -r
echo # プロンプトの後に改行
# 角括弧は二重にしないとエラーになる。正規表現の角括弧で条件節が終わってしまうため?
# なお二重角括弧はbashビルトインというだけで一重と基本的な違いはないらしい[22]
if [[ $REPLY =~ ^[Yy]$ ]] 
then
  echo "あ〜あ、やっちゃった!"
elif [[ $REPLY =~ ^[Nn]$ ]]
then
  echo "よくぞ考え直した!"
fi
$ ./test.sh
本当に実行していいですか?[Y/n] y
あ〜あ、やっちゃった!
$ ./test.sh
本当に実行していいですか?[Y/n] n
よくぞ考え直した!

参考文献・サイト:

引数、既定変数

スクリプト実行時指定した引数を引用するには、$数値と記す。数値には最初の引数を1番として、何番目の引数かを記す。

【スクリプト内容】(test.sh)
#!/bin/sh
echo $1/$2

【実行内容】
shell> test.sh hogehoge fugafuga
hogehoge/fugafuga

その他、既定変数も含め以下に記す[11]

変数 内容
$# 引数の個数
$* 引数全てを含む配列。$* も $@ も挙動は同じ?
$ cat test.sh
#!/bin/bash
echo '$* = '$*
echo '"$*" = '"$*"
echo '$@ = '$@
echo '"$@" = '"$@"
$ ./test.sh hoge foo
$* = hoge foo
"$*" = hoge foo
$@ = hoge foo
"$@" = hoge foo
$@
$0 スクリプト自身のファイル名
$数値 数値番目の引数の値
$$ スクリプト自身のプロセスID(PID)
$! スクリプトが最後に呼び出したバックグラウンドプロセスのプロセスID(PID)
$? 最後(直前)に実行したコマンドの終了コード(戻り値)

bash, kshで最後の引数を得るには ${@: -1} を参照すればよい32

指定時間だけ待つ(sleep)

sleep コマンドは引数に指定した時間だけ停止して待つ。サスペンド中もカウントを続け、フォアグラウンドに来たときに時間が残っている場合は、再び停止して待つ。

【書式1】
sleep 数値単位
【書式2】標準出力に使用方法のメッセージを出力して正常終了する
sleep --help
【書式3】標準出力にバージョン情報を出力して正常終了する。
sleep --version 
  
単位
数値の単位。指定がない場合の既定値は"s"(秒)。指定できる値は"s"(秒)、"m"(分)、"h"(時間)、"d"(日)のいずれか。

出展:man sleep (GNU Shell Utilities 2.1 18 June 2002)

シェルスクリプトを終了する(exit)

exitは指定した数値のステータスでシェル(スクリプト)を終了させる。引数の指定を省略した場合の既定値は、最後に実行したコマンドの終了ステータス。EXIT信号のトラップはシェルが終了する前に行われる。

exit 数値

関数

スクリプト内でにおける関数の定義するには以下のように記す12。関数の引数を関数内で参照する方法はスクリプトの場合と同様(→引数、既定変数)。関数は呼び出すよりも前に定義しておく必要あり。

【書式】
# 定義
関数名()
{
  処理内容
}

# 呼び出し(引数なし)
関数名
# 呼び出し(引数あり)
関数名 引数1 引数2 ...

【例】
#!/bin/sh
sub_sv1()
{
  echo "This is server1."
}
sub_not_sv1()
{
  echo "This is not server1."
}

if [ "`hostname`" = "server1" ];then
  sub_sv1
else
  sub_not_sv1
fi

なお、シェルスクリプトの関数が返す値は、関数の終了ステータス(問題なく終了すれば0、何らかの問題があれば0以外)。数値以外の

関数の引数として配列を直接指定することはできないが、回避方法は存在する。

【方法1】shiftでずらして一つずつ受領していく?22



【方法2】変数名を渡し、変数を評価して値を得る関節参照の手法を使う23

$ cat test.sh
function add_parenthesis()
{
  local arrayname=$1
  eval ref=\"\${$arrayname[*]}\"
  for item in ${ref[*]}
  do
    echo "<$item>"
  done
}

dnabases=( "adenine" "guanine" "thymine" "cytosine" )
# 引数には$をつけない(変数そのものではなく変数名を文字列で指定する)
add_parenthesis dnabases
$ ./test.sh
<adenine>
<guanine>
<thymine>
<cytosine>

終了ステータスを返す(return)

returnコマンドを関数定義の中で使用した場合、指定した終了ステータスを返して関数処理を終了させる。終了ステータスの指定を省略した場合の既定値は、関数内で最後に実行されたコマンドの終了ステータス。 If used outside a function, but during execution of a script by the . (source) command, it causes the shell to stop executing that script and return either n or the exit status of the last command executed within the script as the exit status of the script. If used outside a function and not during execution of a script by ., the return status is false. Any command associated with the RETURN trap is executed before execution resumes after the function or script. (man return, GNU Bash-3.0 2004 Apr 20 BASH_BUILTINS(1))

return 終了ステータス

指定したコマンドを実行する

変数に格納されたコマンド文字列を実行するには eval を使う。

【書式】
eval 引数 ...
引数
引数はすべてつながれ、一つのコマンドとしてシェルにより実行される。 evalの返り値は終了ステータス。 引数が指定されなかった場合、evalは0を返す。
【例】master.txtのコピーを10個つくる
$ cat ./makecopy.sh
#!/bin/sh
i=1
while [ $i -le 10 ]
do
  cmd="cp -f master.txt copy"$i".txt"
  echo $cmd
  eval $cmd
  i=`expr $i + 1`
done
$ ls
makecopy.sh    master.txt
$ ./makecopy.sh
cp -f master.txt copy1.txt
cp -f master.txt copy2.txt
cp -f master.txt copy3.txt
cp -f master.txt copy4.txt
cp -f master.txt copy5.txt
cp -f master.txt copy6.txt
cp -f master.txt copy7.txt
cp -f master.txt copy8.txt
cp -f master.txt copy9.txt
cp -f master.txt copy10.txt
$ ls
makecopy.sh   master.txt    copy1.txt    copy2.txt
copy3.txt     copy4.txt     copy5.txt    copy6.txt
copy7.txt     copy8.txt     copy9.txt    copy10.txt

参考文献・サイト

別のコマンドに実行を移す

execは指定したコマンドを実行する。 コマンドが何らかの理由で実行できなかった場合、execfailのシェルオプションが有効になっていない限り非対話的なシェルは終了する。execfailが有効であった場合、シェルはfailureを返す。ファイルが実行できなかった時、対話的シェルはfailureを返す。コマンドを指定しなかった場合、あらゆるリダイレクトが現在のシェルで有効となり、0のステータスを返す。リダイレクトにエラーがあった場合、1のステータスを返す。

exec  コマンド コマンド引数
コマンド、コマンド引数
コマンドを指定すると、新たなプロセスは生成されず指定したコマンドにシェルが切り替わる。コマンド引数はコマンドに対する引数として解釈される。
-l
0番目(最初の)引数の最初にスラッシュを追加して実行する。これはloginコマンドが実行される際行われていることである。
-c
空の環境でコマンドが実行される。
-a
名前を実行するコマンドの0番目の引数として渡す。
【例】
$ echo test.sh
#!/bin/bash
CMD="ls"
# -n オプションがあるとコマンド自体を表示、ないとコマンド実行結果を表示
if [ "$1" = "-n" ]
then
  echo $CMD
else
  exec $CMD
fi
$ ./test.sh
hoge.php  hoge.txt  hoge.log  test.sh
$ ./test.sh -n
ls

起動スクリプトに関して

RHEL 5 や CentOS 5 の起動スクリプトに以下の記述を行っておくと、 プロセスIDの記録などの自動的に行ってくれるなどコードの記述が楽になる。

# Source function library.
. /etc/rc.d/init.d/functions

# Source networking configuration.
. /etc/sysconfig/network

以下はこれを用いた事例

#!/bin/bash
#
# foo  This script takes care of starting and stopping
#      foo daemon
#
# Source function library.
. /etc/rc.d/init.d/functions

# Source networking configuration.
. /etc/sysconfig/network

RETVAL=0  # 返り値初期値
prog="foo" # 表示用プログラム名
progpath="/usr/sbin/foo" # プログラムの絶対パス
CONF=`ls /etc/foo.conf 2>/dev/null` # 設定ファイルの絶対パス(存在しなければ空)

# 起動用サブルーチン
start() {
  # プログラムが存在しかつ実行可能である or ステータス4で終了する
  [ -x "$progpath" ] || exit 4
  # 設定ファイル名が空 and ステータス6で終了する
  [ -z "$CONFS" ] && exit 6
  # プログラム名、設定ファイル名(拡張子除去)を出力
  echo -n $"Starting $prog for "`basename $CONF .conf`": "
  # プログラムをデーモンとして起動(書式はプログラムにより異なる)
  daemon $progpath $CONF
  # 前行のデーモン起動処理の返り値を返り値に代入
  RETVAL=$?
  # 返り値が0(正常起動)ならlockファイル作成(ファイル名は表示用プログラム名)
  [ $RETVAL -eq 0 ] && touch /var/lock/subsys/$prog
  # 改行
  echo
  # 返り値を返す
  return $RETVAL
}

# 停止用サブルーチン
stop() {
  # プログラム名を出力
  echo -n $"Shutting down $prog: "
  # プログラム名を停止
  killproc $prog
  # 前行のデーモン起動処理の返り値を返り値に代入
  RETVAL=$?
  # 改行
  echo
  # 返り値が0(正常終了)ならlockファイル削除
  [ $RETVAL -eq 0 ] && rm -f /var/lock/subsys/$prog
  # 返り値を返す
  return $RETVAL
}

# 引数による条件分岐
case "$1" in
  start)
        start
        ;;
  stop)
        stop
        ;;
  restart|reload)
        stop
        start
        RETVAL=$?
        ;;
  condrestart)
        if [ -f /var/lock/subsys/$prog ]; then
            stop
            start
            RETVAL=$?
        fi
        ;;
  status)
        status $prog
        RETVAL=$?
        ;;
  *)
        echo $"Usage: $0 {start|stop|restart|condrestart|status}"
        exit 2
esac

exit $RETVAL

サンプルスクリプト

ログファイル名として自己スクリプト名の拡張子を「.log」に置換した値をセット
【例】ただ実行日時をログに出力するだけのスクリプト
$ cat timestamp.sh
#!/bin/sh
LOG="/var/log/"`basename "$0" | sed -e 's/\.sh$/.log/'`
echo `date "+%Y-%m-%d %H:%M:%S %Z(%z)"` >>${LOG}
$ ./timestamp.sh
$ cat /var/log/timestamp.log
2012-02-13 15:49:22 JST(+0900)
上にある古い記録を消す
【例】
$ cat remove_old.sh
#!/bin/sh
# 第1引数に指定したファイルを処理対象とする
LOG=$1
# 最大行数(これを越えると古い上部の行を削除する)
MAXLINES=100 
# 指定ファイルの行数
LINES=`cat $1 | wc -l`
# 最大行数より多い
if [ ${LINES} -gt ${MAXLINES} ];then
  # 末尾指定行を一時ファイルに書き出し
  tail -n ${MAXLINES} ${LOG} >${LOG}".tmp" 
  # 元のファイルに上書き
  mv -f ${LOG}".tmp" ${LOG}
  echo "The number of lines (${LINES}) reachs the limit (${MAXLINES}) to remove the upper part."
# 最大行数以下
else
  echo "The number of lines (${LINES}) doesn't reach the limit (${MAXLINES})."
fi

$ cat test.txt | wc -l 
99
$ ./remove_old.sh test.txt
The number of lines (99) doesn't reach the limit (100).
$ ls -l >>test.txt
$ cat test.txt | wc -l 
105
$ ./remove_old.sh test.txt
The number of lines (105) reachs the limit (100) to remove the upper part.
$ cat test.txt | wc -l 
100