bash シェルスクリプト

シェルスクリプトって書いては忘れ、書いては忘れの繰り返しだな。

シェルスクリプトではなく、コマンドとその例については、
id:kaishitaeiichi:20101116:1289914288

Hello, World!


$ echo '#!/bin/bash' >> hello.sh
$ echo 'echo "Hello, World!"' >> hello.sh
$ echo 'echo "Hello, World!"' >> hello.sh
$ cat hello.sh
#!/bin/bash
echo "Hello, World!"
echo "Hello, World!"
$ chmod 744 hello.sh
$ ./hello.sh
Hello, World!
Hello, World!
$ echo $?
0

ファイルに、コマンドを書き並べて、実行権限を与えれば、
シェルスクリプトとして実行できる。

予備知識

使用するシェルを明記

シェルスクリプトファイルの先頭行で使用するシェルを明記する。

#!/bin/bash
権限

適切な権限を与える。


$ chmod 744 abc.sh
$ ls -l abc.sh
-rwxr--r-- 1 eiichi eiichi 33 2010-12-04 01:17 abc.sh

シェルスクリプトが実行できていても、シェルスクリプトを実行したユーザによって、ファイルのcpコマンドなど、シェルスクリプト中のファイルの入出力処理が失敗することがよくある。読み書きするファイルやディレクトリの、所有者、所有グループ、パーミッションの設定が悪いことが多い。chown, chmodで適切な設定をする。

実行

/home/eiichi/abc.sh
./abc.sh

相対パスのときは、.をつけないとだめ。PATHに.を加えるのはセキュリティ上良くないらしい。

実行結果

シェルスクリプトというわけじゃないけど、"直前に"実行したコマンドの終了ステータスを、$?で確認できる。$?は特殊変数と呼ばれるもので、ほかにもいろいろある。


$ mkdir test-dir
$ echo $?
0
$ mkdir test-dir
mkdir: ディレクトリ `test-dir' を作成できません: File exists
$ echo $?
1

シェルスクリプトの終了ステータスは、最後に実行したコマンドのlsの終了ステータスになる。

#!/bin/bash

mkdir result-test
touch result-test/a.txt
touch result-test/b.txt
touch result-test/c.txt
cp result-test/d.txt result-test/e.txt
ls -l result-test

cpが失敗するのだが、そこで処理が止まるわけではなく、lsが実行される。

eiichi@ubuntu-desktop:~/Programming/shellscripts$ ./result-test.sh 
cp: `result-test/d.txt' を stat できません: No such file or directory
合計 0
-rw-r--r-- 1 eiichi eiichi 0 2011-05-16 00:18 a.txt
-rw-r--r-- 1 eiichi eiichi 0 2011-05-16 00:18 b.txt
-rw-r--r-- 1 eiichi eiichi 0 2011-05-16 00:18 c.txt
eiichi@ubuntu-desktop:~/Programming/shellscripts$ echo $?
0

result-test.shの終了ステータスは、最後に実行したコマンドのlsの終了ステータスになる。

今度は、set -eを指定してみる。

#!/bin/bash

set -e

mkdir result-test
touch result-test/a.txt
touch result-test/b.txt
touch result-test/c.txt
cp result-test/d.txt result-test/e.txt
ls -l result-test

cpが失敗する。そこで処理が止まる。cpより後にある、lsは実行されない。

eiichi@ubuntu-desktop:~/Programming/shellscripts$ ./result-test-b.sh 
cp: `result-test/d.txt' を stat できません: No such file or directory
eiichi@ubuntu-desktop:~/Programming/shellscripts$ echo $?
1

result-test-b.shの終了ステータスは、最後に実行したコマンドのcpの終了ステータスになる。

シェルスクリプトには、たくさんのコマンドをづらづらと書くことになる。場合によっては、その中の一部のコマンドが失敗することもある。シェルスクリプトは、途中で失敗したコマンドがあったとしても、残りのコマンドを実行して終了してしまう。

コマンドが失敗した段階で、シェルスクリプトを異常終了させたい場合は、
set -e
を指定すると良い。

成功したら0、失敗したら1というのが基本みたいなんだが、コマンドによっては、成功、失敗とはちがうんじゃない?というのもある。

test


$ test 1 -gt 2
$ echo $?
1
$ test 1 -lt 2
$ echo $?
0

grep


$ echo "abcd" | grep bc
abcd
$ echo $?
0
$ echo "abcd" | grep bd
$ echo $?
1

トラブルシューティング

サンプルがうごかない

このページで説明しているのは、bashのサンプル。sh, csh, tcshなどではうごかない例もたくさんある。

そして、自分がつかっているシェルがbashでない。それに気づいてない。自分がログイン直後にデフォルトで使用しているシェルはで確認できる。

$ cat /etc/passwd | grep 自分のユーザ名

また、

$ cat /etc/shells

で使用できるシェルを確認できる。

tcshに切り替えて、作業して、元のシェルに切り替えるには、

$ /bin/tcsh
$ 何かの作業をする
$ exit
デバッグ


bash -x ./abc.sh

お手軽だけど、どばっと表示内容が増えて、見づらい。とにかく、1行ずつ丁寧に読んでいくと、どこが期待通りになってないかわかる。

変数

変数に値を設定するとき
A = "abc"
のようにスペースをいれるとだめ。
A="abc"
とする

改行コード

LinuxシェルスクリプトWindows上で作成している場合は、要注意!

たとえば、Windows上で、Eclipseのプロジェクト内で、作成したシェルスクリプトを、Linux上にアップロードして実行すると、エラーがでることがある。改行コードがWindowsは\r\n(CR + LF)で、Linuxは\n(LF)のため。

TeraTermPutty, Poderosaのようなターミナルアプリの設定によっては、改行で、\r(CR)が追加されることもあるので、注意。

bash -x スクリプトファイルパス、または、od -c スクリプトファイルパスなら、\rが混じっていることを確認できる。

ぜんぜんちがう話題だけど、改行を表現したいときは、$'\n'とかく。

文字と数値

シェルスクリプトでは、数字は、基本、文字扱い。数値としてあつかいたいとき、たとえば比較や計算などをしたいときは、注意が必要。

特に、数値としてあつかいたい場合、計算をしたい場合は、

  • $((....))
X=1
Y=$(($X + 2))
echo $Y
  • declare -i
declare -i X=1
declare -i Y
Y=X+2
echo $Y
if

if["abc" = "$1"];then
のように、[と]のそばにスペースがないのはだめ。
if [ "abc" = "$1" ];then
とそばにスペースをいれて書く。

変数

値を設定

値を設定するには=をつかう。$はつけないことに注意
A='abc'

値を参照

値を参照するには、$をつける
$A
または、
${A}

変数に対する処理

変数に対して、ある処理をおこなうことができる。

デフォルト値設定

変数に値を設定されていない場合は、:-の右側に指定した値。
変数に値が設定されている場合は、その値。
ifを使うより簡潔になる。


$ echo $A

$ echo $B

$ echo $C

$ B=${A:-'default-a'}
$ echo $B
default-a

$ C=${B:-'defalult-b'}
$ echo $C
default-a

文字列の長さ
$ A="abcd"
$ echo $A
abcd
$ echo ${#A}
4
文字列の削除

パターンにマッチした文字列を削除する。
前半分とか後半分とか、わりとざっくりした削除。
便利なような、不便なような。

$ A=/abcd/efgh/abcd/efgh
$ echo ${A%/efgh*}
/abcd/efgh/abcd
$ echo ${A%/*}
/abcd/efgh/abcd
$ echo ${A%%/efgh*}
/abcd
$ echo ${A#*abcd/}
efgh/abcd/efgh
$ echo ${A##*abcd/}
efgh
$ echo ${A##*/}
efgh
文字列の置換
$ A="abcd efgh abcd efgh"
$ echo ${A/efgh/ijkl}
abcd ijkl abcd efgh
$ echo ${A//efgh/ijkl}
abcd ijkl abcd ijkl
文字列から部分文字列を切り出す

${A:x:y}
x番目の文字からはじめてy文字を切り出す。

$ A="abcdefg"
$ echo ${A:0:4}
abcd
$ echo ${A:2:3}
cde

コマンド置換

シェルスクリプト中で、コマンドを実行して、その結果?返り値?をシェルスクリプト中で使用することができる。``や$()を使用する。

A=`pwd`
B=$(ls)

変数Cには、testファイルの内容が設定される。
1行のときは良いけど、複数行の時は改行がなくなってしまうようだ。

testfile=test.txt
C=$(cat $testfile)
echo $C

配列

リストから配列をつくる

配列を生成するには、リストを()で囲む。

$ testArray=(a b c d)
$ echo ${testArray[0]}
a
$ echo ${testArray[1]}
b
$ echo ${testArray[2]}
c
$ echo ${testArray[3]}
d

とにかく、()に対して、リストを与えれば、配列にできるようだ。

$ numberArray=($(seq 10 13))
$ echo ${numberArray[0]}
10
$ echo ${numberArray[1]}
11
$ echo ${numberArray[2]}
12
$ echo ${numberArray[3]}
13
配列からリストをつくる

配列のすべての要素にアクセスするには@を使う。

$ testList=${testArray[@]}
$ echo $testList
a b c d
配列でループ
A=(a b c d)
A_LENGTH=${#A[@]}
A_LAST_INDEX=$(($A_LENGTH - 1))

#indexを指定してループ
for index in $(seq 0 $A_LAST_INDEX)
do
    echo ${A[$index]}
done

#配列をリストにしてループする
for e in ${A[@]}
do
    echo $e
done


#文字列でなく数値としてあつかえるように変数を宣言
declare -i index=0

while [ $index -lt $A_LENGTH ]
do  
    echo ${A[$index]}
    index=$index+1
done

if

if [ "$1" ]; then
    echo "in if...."
elif [ "$1" = "b" -a "$2" = "bb" -o "$1" = "c" ]; then
    echo "in elif...."
else
    echo "in else...."
fi
  • [....]これは、test ....とおなじ。
  • = 代入じゃなくて等しいかどうか
  • -a and
  • -o or

[と]はそばにスペースが必要なことに注意
else ifじゃだめで、elifとなるのが、忘れやすい

andとorは、-aと-oより、&&と||で書いたほうが、ほかのプログラム言語でもなじみある書き方だとおもう。シェルでは、&&と||は、コマンドを連続して実行するときに使うわけだけど、[]はtestコマンドとおなじだから、ここでも使うことができる。()でまとめることもできる。

if [ .... ] && ([ .... ] || [ .... ]); then

ためしに、結果をずらずら書くと、

eiichi@ubuntu-desktop:~/Programming/shellscripts$ [ 1 = 1 ] && ([ 1 = 1 ] || [ 1 = 1 ]); echo $?
0
eiichi@ubuntu-desktop:~/Programming/shellscripts$ [ 1 = 1 ] && ([ 1 = 1 ] || [ 1 = 2 ]); echo $?
0
eiichi@ubuntu-desktop:~/Programming/shellscripts$ [ 1 = 1 ] && ([ 1 = 2 ] || [ 1 = 1 ]); echo $?
0
eiichi@ubuntu-desktop:~/Programming/shellscripts$ [ 1 = 1 ] && ([ 1 = 2 ] || [ 1 = 2 ]); echo $?
1
eiichi@ubuntu-desktop:~/Programming/shellscripts$ [ 1 = 2 ] && ([ 1 = 1 ] || [ 1 = 1 ]); echo $?
1
eiichi@ubuntu-desktop:~/Programming/shellscripts$ [ 1 = 2 ] && ([ 1 = 1 ] || [ 1 = 2 ]); echo $?
1
eiichi@ubuntu-desktop:~/Programming/shellscripts$ [ 1 = 2 ] && ([ 1 = 2 ] || [ 1 = 1 ]); echo $?
1
eiichi@ubuntu-desktop:~/Programming/shellscripts$ [ 1 = 2 ] && ([ 1 = 2 ] || [ 1 = 2 ]); echo $?
1
eiichi@ubuntu-desktop:~/Programming/shellscripts$ [ 1 = 1 ] && [ 1 = 1 ] || [ 1 = 1 ]; echo $?
0
eiichi@ubuntu-desktop:~/Programming/shellscripts$ [ 1 = 1 ] && [ 1 = 1 ] || [ 1 = 2 ]; echo $?
0
eiichi@ubuntu-desktop:~/Programming/shellscripts$ [ 1 = 1 ] && [ 1 = 2 ] || [ 1 = 1 ]; echo $?
0
eiichi@ubuntu-desktop:~/Programming/shellscripts$ [ 1 = 1 ] && [ 1 = 2 ] || [ 1 = 2 ]; echo $?
1
eiichi@ubuntu-desktop:~/Programming/shellscripts$ [ 1 = 2 ] && [ 1 = 1 ] || [ 1 = 1 ]; echo $?
0
eiichi@ubuntu-desktop:~/Programming/shellscripts$ [ 1 = 2 ] && [ 1 = 1 ] || [ 1 = 2 ]; echo $?
1
eiichi@ubuntu-desktop:~/Programming/shellscripts$ [ 1 = 2 ] && [ 1 = 2 ] || [ 1 = 1 ]; echo $?
0
eiichi@ubuntu-desktop:~/Programming/shellscripts$ [ 1 = 2 ] && [ 1 = 2 ] || [ 1 = 2 ]; echo $?
1

case

caseも他のプログラミング言語でもおなじみ。

下は、かんたんな、引数の解析をしている。

case-examples.sh

#!/bin/bash

while [ $# -gt 0 ]; do
    echo "current number of arguments is $#"
    case "$1" in
        "--help"|"-h") echo "This is help!"; exit 0;; 
        "-a") echo "a=$2"; shift 2;; 
        "-b") echo "b=$2"; shift 2;; 
        *) echo "the others"; shift 1;; 
    esac 
done

これを実行すると

eiichi@ubuntu-desktop:~/Programming/shellscripts$ ./case-examples.sh -a 1 -b 2 3 4
current number of arguments is 6
a=1
current number of arguments is 4
b=2
current number of arguments is 2
the others
current number of arguments is 1
the others
eiichi@ubuntu-desktop:~/Programming/shellscripts$ ./case-examples.sh --help
current number of arguments is 1
This is help!

forループ

bashのforループは、

Javaのfor文でいうなら

for(int i = 0; i < array.length; i++) {

}

というタイプのfor文ではなくて、

for(A a : list) {

}

のようなタイプのいわゆるforeach文

phpなら、こんなやつ

foreach ( $array as $a ) {
  echo "$a\n";
}

つまり、リストを与えてループ処理を実行する。

forの例

リストをハードコーディングして、ループ処理を実行

for i in a b c d
do
    echo $i
done


eiichi@ubuntu-desktop:~/Programming/shellscripts$ ./for.sh
a
b
c
d

リストをseqコマンドで生成して、ループ処理を実行

for i in $(seq 1 4)
do
    echo $i
done


eiichi@ubuntu-desktop:~/Programming/shellscripts$ ./for.sh
1
2
3
4

とにかくリストを与えることさえできれば、forに適用できる。

1行で書くなら、下のようにする。

for i in $(seq 1 4); do echo $i; done

lsコマンドでファイル名のリストを取得して、ループ処理を実行

for i in $(ls *.txt)
do
    echo $i
done


eiichi@ubuntu-desktop:~/Programming/shellscripts$ ./for.sh
a.txt
b.txt
c.txt

forをつかわなくても、|をつかえば、結果としてループ処理とおなじことはできる場合もある。|をつかうほうがいかにもシェルっぽい。


$ ls *.txt | xargs echo
a.txt b.txt c.txt

whileループ

whileの例

whileは、配列のループのところに書いた例のように

while [....]
do
    ....
done

という書き方ができる。

使いどころがありそうな、例としては、ファイルを1行ずつ読み込むというものがある。

#!/bin/bash

LINE_LIST=""
while read LINE
do  
    echo $LINE
    LINE_LIST="$LINE_LIST $LINE"
done < "list.txt"

echo $LINE_LIST

あとは、特定の条件をみたすまで、sleepしたいときとかかなあ。

関数

他のプログラミング言語のように関数を定義できる。

関数を定義するのは、新しくコマンドやシェルスクリプトをつくったようなもので、呼び出しかたも、コマンドやシェルスクリプトと同様。
関数名 引数 引数 ....

関数の引数は、宣言しない。

  • $1,$2,....のようにすれば、第1引数、第2引数、....を参照できる。

関数の返り値の取得は、けっこう独特。

  • returnの返り値は、終了ステータスとなる。
    • 関数の呼び出し元で$?で取得する。
  • 終了ステータスでない返り値がほしいときは、
    • 関数の書き方としては、returnでなくて、echoで出力する。
    • 返り値を取得するための関数の呼び方としては、FUNC_OUTPUT=$(myfunc)のように実行する。
#関数定義
function mycp {
    local INPUT_FILE_PATH="$1"
    local OUTPUT_DIR="$2"
    cp -p $INPUT_FILE_PATH $OUTPUT_DIR
}

#関数実行
mycp "test.txt" "test_dir"
#終了ステータスは、関数の最後で実行されたコマンドの実行結果になる。
echo $?

#関数定義
mycp2() {
    mycp "$1" "$2"
    if [ $? = 0 ]; then
        return 0;
    else
        return 1;
    fi
}

#関数実行。ふつうのコマンド呼び出しと同じ。
mycp2 "test.txt" "test_dir"
#終了ステータスは、returnで指定された値になる。
echo $?

#結果ステータスではない戻り値を取得したいときは、
#echoで出力して
function mydatetime {
    echo $(date '+%Y%m%d%H%M%S')
}

#$(....)で関数を実行して、結果を取得する。
#mydatetime関数を実行すると、mydatetime内のechoが
#コンソールに出ない代わりに、変数に設定される。
CURRENT_DATE_TIME=$(mydatetime)
echo $CURRENT_DATE_TIME 

その他

javaコマンドの実行結果

javaコマンドの実行結果は、Javaソースコード
System.exit(x);
で指定できる。


System.exit(10);と指定すれば、


java xxxx
echo $?
10

System.exit(x);で明記しなければ、

正常終了した場合は、


java xxxx
echo $?
0

例外終了した場合は、


java xxxx
echo $?
1

参考

bashで始めるシェルスクリプト基礎の基礎 (1/2):Windowsユーザーに教えるLinuxの常識(8) - @IT

Powered by Open Sources

"set -e": 実行コマンドが0以外の終了コードの場合、シェルスクリプトを終了する - 元RX-7乗りの適当な日々
コマンドのエラーを必ず検出するための、set -eか!
なるほど。ああー、もっとはやく知っていれば。でも、エラー時にエラーが発生した箇所でログ出力もしたい。そのためには、やっぱりコマンド実行のたびに$?するしかない。どうしよう。

http://journal.mycom.co.jp/articles/2010/06/10/bash-array/index.html
配列のリファレンス的なサンプル。便利。