見出し画像

シェルスクリプトの基本

はじめに

シェルスクリプトの基本(とりあえず簡単なツールが作れるくらいのレベル)や、ハマりやすい罠について、ざっくりとまとめました。
「他のプログラミング言語はわかるけどシェルスクリプトは初めて」という人や「なんとなくは書ける」という人向けの内容になっています。
ちなみにシェルにもいろいろ種類がありますが、本記事で紹介しているシェルはbashです。

シェルスクリプトの使い道

シェルスクリプトは、自分でゴリゴリ処理を書くというよりは、主要な処理はLinuxコマンドに任せて、Linuxコマンドの実行結果や出力結果をもとに他のLinuxコマンドを実行するみたいな感じで使います。
なので、シェルスクリプトを覚える前に、まずは基本的なLinuxコマンドを覚えることをおすすめします。

Linuxの基礎については以下の記事にまとめてますので、こちらもどうぞ。

シェルスクリプトの動作確認時の豆知識

ログインシェルがbashであれば、シェルスクリプトに書いた内容はコンソール上でそのまま実行しても動きます。
長めのシェルスクリプトを書く時に「この部分だけ動作確認してみたい」と思ったら、まずコンソール上にその行を書いて実行してみて、問題なければシェルスクリプトの方に書くようにすると確実です。
ちなみにシェルスクリプトは行末に「;」を書く必要はないですが、行の区切りに「;」をつけると複数行を1行で書けるので、複数行の動作確認もできます。

シェルスクリプトのファイル

文字コードはUTF8、改行コードはLF(\n)とします。
改行コードが違っていると動きません(Windowsのエディタで書いた人がハマりがち)。

あとbashの場合、先頭に「#!/bin/bash」とつけたりしますが、ログインシェルがbashならつけなくてもbashとして動きます。

シェルスクリプトの実行方法

実行方法には以下の2種類あります。
お好みでどうぞ。

①ファイルパスのみを指定して実行

ファイルに実行権限をつけて(755が一般的)ファイルのパスを指定して実行します。ファイルを足元に置いてある場合は「./」をつける必要があります(つけないとコマンドだと思われて、「そんなコマンドはない」と怒られます)。

$ chmod 755 test.sh
$ ./test.sh

②頭に「bash」をつけて実行

bashコマンドに、実行したいファイルのパスを指定して実行します。
この場合はファイルに実行権限がなくても動きます。また、ファイルを足元に置いている場合、「./」なしでも動きます。

$ chmod 644 test.sh
$ bash test.sh

引数

シェルスクリプトに渡した引数は、第一引数から順に「$1」、「$2」…で参照可能です。
ちなみに「$0」は実行されたシェルスクリプト自身です。
引数の数は「$#」で取得可能です。

変数

・変数に値を代入する

代入時は頭に「$」をつけないこと、「=」の前後にスペースを入れないことがポイント。
型はないので数字でも文字列でも好きなものを入れられます。

#!/bin/bash
HOGE="hoge"

・変数に入れた値を参照する

参照時は変数の頭に「$」をつけます。
ちなみに正式には「${変数名}」ですが、{}は省略できます。

#!/bin/bash
HOGE="hoge"
echo $HOGE

<実行結果>
$ ./test.sh
hoge

【補足】変数名を{}で囲む必要がある場面

①変数と文字列を連結したい時
変数と文字列を連結したい場合「"$<変数名><文字列>"」で連結できますが、例えば変数「$HOGE」と文字列「_HUGA」を連結したい場合、「"$HOGE_HUGA"」と書くと「$HOGE_HUGA」という変数だと思われてしまうため、「"${HOGE}_HUGA"」等と変数名のみを{}でくくります。

#!/bin/bash
HOGE="hoge"
echo "{}なし:$HOGE_HUGA"
echo "{}あり:${HOGE}_HUGA"

<実行結果>
$ ./test.sh
{}なし:
{}あり:hoge_HUGA

②引数が10個以上ある場合
前述の通り、引数は第一引数から「$1」、「$2」となりますが、基本的に$の後の数字一桁までで第何引数かを判断しているっぽいです。
なので、第十引数を「$10」と書くと、「$1(第一引数)」と、「0」という文字列の連結だと思われて変な感じになります。
このため、第十引数以降は「${10}」等と{}でくくる必要があります。

#!/bin/bash
echo "{}なし:$10"
echo "{}あり:${10}"

<実行結果>
$ ./test.sh a b c d e f g h i j
{}なし:a0
{}あり:j

・ローカル変数宣言

基本的にシェルスクリプト内で宣言された変数はすべてグローバル変数扱いになりますが(関数内で宣言された変数であっても)、関数内で変数に代入する時に頭に「local」をつけると、その関数内でのみ有効な変数になります。
なお、関数じゃないところで「local」をつけて宣言すると文法エラーになります。

local HOGE="hoge"

【補足】変数を""で括った時と括らない時の違い

シェルスクリプトでは、代入した値に改行が入っている場合、変数を""(ダブルクォート)でくくると改行が保持されて複数行として扱われ、""でくくらないと改行が半角スペースとなって1行として扱われるという特性があります。
例えばファイルの中身を一度変数に入れて、その後でgrepしたい時などは、以下のように""でくくる必要があります。

TEST=$(cat test.csv)
RESULT=$(echo "$TEST" | grep "hoge")

この特性を生かして、敢えて""で括らずに一行にしたりすることもできますので、そこら辺は用途によって使い分けてください。

また、どのスクリプト言語でも大体そうだと思いますが、""でくくると変数が展開され、''(シングルクォート)でくくると変数が展開されません。

配列

・配列に値を代入する

()でくくって半角スペース区切りで入れるか、インデックスを指定して入れます(インデックスは[]でくくる)。

ARRAY=("aaa" "bbb" "ccc")

もしくは

ARRAY[0]="aaa"
ARRAY[1]="bbb"

・配列に入れた値を参照する

普通の変数と同様、頭に「$」をつけますが、「${配列名[0]}」のように、インデックスまで含んで{}でくくる必要があります
他の言語に慣れてると意味不明ですが、{}がないと「$配列名」という変数と「[0]」という文字列の連結だと思われて変な感じになります。
※ちなみにインデックスを指定せずに「$配列名」だけ書くと、配列の1つ目が表示されます。

#!/bin/bash
ARRAY=("aaa" "bbb" "ccc")
echo "{}なし:$ARRAY[1]"
echo "{}あり:${ARRAY[1]}"

<実行例>
$ ./test.sh
{}なし:aaa[0]
{}あり:bbb

・配列の要素数の取得

配列の要素数は「${#配列名[*]}」や「${#配列名[@]}」で取得可能です。
ちなみに「${配列名[*]}」や「${配列名[@]}」は、配列の中身をすべて半角スペース区切りで表示します。

コマンドや関数の実行

・コマンドの実行

コマンドラインをそのまま書くだけで実行可能です。
また、コマンドの戻り値は「$?」で取得可能です。

#!/bin/bash
cat test.txt
RET=$?

・コマンド実行結果の取得等

実行自体は上記でできますが、上記の書き方だと標準出力や標準エラー出力がそのままコンソールに出力されるので(個人的に「垂れ流れる」と表現しています)、出力結果を変数に入れてその後その変数を使って何かしたい場合は、コマンドラインを「$()」でくくった上で、変数に代入します。
なお、変数には「$()」内のコマンドラインの標準出力しか入らないので(標準エラー出力は垂れ流れる)、標準エラー出力の方は標準出力に切り替えるか捨てるかする必要があります。
※ちなみに「$()」は「``(バッククォート)」でも書けますが、最近は「$()」推奨のようです。

・標準出力は変数に入れて、標準エラー出力は捨てる

#!/bin/bash
OUTPUT=$(cat test.csv 2> /dev/null)

・標準出力も標準エラー出力も変数に入れる

#!/bin/bash
OUTPUT=$(cat test.csv 2>&1)

・標準出力も標準エラー出力も捨てる
特に変数に入れる必要がなくて、実行だけして出力を全部捨てたい場合は以下で大丈夫です。

#!/bin/bash
cat test.csv > /dev/null 2>&1

・関数の宣言と実行

こんな感じです。頭の「function」は省略可。

#!/bin/bash
function testfunc () 
{ 
    local VAL1=$1
    local VAL2=$2
    <処理>
    return 0
}

testfunc aaa bbb
RET=$?

・関数の戻り値について

上記の通り、関数の戻り値はreturnで返却し、呼び出し元で「$?」で取得可能です(returnには数字のみ指定可能)。
関数の最後のreturnを省くと、一番最後に実行された行の戻り値を返しますので、意図的に最後の行の戻り値を返したい時以外は、きちんとreturnを記載したほうが良いです。

ちなみに、関数内で文字列を返却したい場合は、以下のようにreturn前にechoで標準出力に出力した上で、呼び出し元で「$()」を使って拾う必要があります。

#!/bin/bash
function testfunc ()
{
    <処理>
    echo <返却したい文字列>
    return 0
}

OUTPUT=$(testfunc)
RET=$?

シェルスクリプトの終了

シェルスクリプトを終了する場合は、exitの後に戻り値を指定します。
処理の最後のexitは省略できますが、この場合はreturnと同様、最後に実行した行の戻り値がシェルスクリプトの戻り値として返却されます。

if文

・if文の書き方

if で始まって fi で閉じます。

#!/bin/bash
if 条件式; then
 :
elif
 :
else
 :
fi

条件式の部分は、testコマンドを使う書き方と、以下のように[]でくくる書き方の2種類ありますが、[]でくくる書き方の方がおすすめです(理由は補足に記載)。
ちなみに、"["の前後と"]"の前に半角スペースを入れないと文法エラーになります("["は実はコマンドなのでそういった縛りがあります)。

#!/bin/bash
if [ "$HOGE" = "hoge" ]; then
 :
fi

・AND条件とOR条件
AND条件は「&&」、OR条件は「||」で繋ぎます。

#!/bin/bash
if [ "$HOGE" = "hoge" ] && [ "$HUGA" = "huga" ]; then
 :
fi
#!/bin/bash
if [ "$HOGE" = "hoge" ] || [ "$HOGE" = "huga" ]; then
 :
fi

条件式が複数ある場合、()でくくると先に評価します。

#!/bin/bash
if [ "$HOGE" = "hoge" ] && ([ "$HUGA" = "huga" ] || [ "$HUGA" = "HUGA" ]); then
 :
fi

・条件式(よく使うものだけ抜粋)

条件式は、文字列比較と数値比較で書き方がちょっと違うので注意。

<文字列の比較>
 = : 等しい(他の言語のように「==」ではなくて、イコール1つでOK)
 != : 等しくない

数値の比較>
 -eq : 等しい
 -ne : 等しくない
 -lt : 左辺が右辺より小さい
 -gt : 左辺が右辺より大きい
 -le : 左辺が右辺以下
 -ge : 左辺が右辺以上
 ※主語が左です。

<ファイルの判定>
 -e <ファイルパス>:指定したファイル・ディレクトリが存在する
 -f <ファイルパス>:指定したパスがファイル
 -d <ファイルパス>:指定したパスがディレクトリ
 -s <ファイルパス>:指定したファイルのサイズが1バイト以上(空ではない)
 ※条件式の頭に「!」をつけると、成り立たない場合にif文を通ります。

・数値の比較時の注意点

数値の比較の時、変数に文字列が入っていたり空だったりすると文法エラーになるので、数値の比較用の条件式は、確実に数値が入っているとわかっている場合のみ使用してください。
空や文字列が入る可能性がある場合は、「-eq」等を使わず、「=」等を使って文字列として比較するのが無難です。

・文字列の比較時の注意点

文字列比較で変数が空になる可能性がある場合や空かどうかを判定する場合、変数名を""でくくっておかないと正しく判定できないので注意。
※常に""でくくっておけば間違いないです。

#!/bin/bash
if [ "$HOGE" = "" ] ;then
 :
fi

ちなみに空の判定で以下のような書き方をする人もいますが、xはなくても大丈夫です。

#!/bin/bash
if [ "x$HOGE" = "x" ]; then
 :
fi

・文字列に特定の文字列を含むかの判定

こんな感じで「=~」を使いつつ、[[]]でくくります。
右辺も変数で大丈夫です。

#!/bin/bash
if [[ "$HOGE" =~ "ho" ]]; then
   :
fi

含まない場合にif文を通したい場合は、条件式の頭に「!」をつけます。

#!/bin/bash
if [[ ! "$HOGE" =~ "ho" ]]; then
   :
fi

【補足】testコマンドを使ったif文の話

シェルスクリプトの本とかLinuxの本とか見ると、[]ではなくtestコマンドを使ったif文の条件式の書き方が紹介されてたりしますが、業務とかで書く時にはあんまりオススメしません。
なんでかというと、「他の言語はわかるけどシェルスクリプトは慣れてない」みたいな人たちがシェルスクリプトを読んだ時、testコマンドで書いてあると可読性が異様に悪くなるからです。
ただ、Linuxに元々入っているシステム用のシェルスクリプト等はtestコマンドで書いてあることがあるので、知識として覚えるだけ覚えておきましょう(ここでは紹介しませんが)。

for文

・for 変数名 in リスト

基本のfor文です。リストは半角スペース区切りで指定し、リストの1つ目から順に変数に格納されてループするイメージです。

#!/bin/bash
for HOGE in "aaa" "bbb" "ccc"
do
  echo $HOGE
done

inの後の書き方は様々で、以下のような感じでもOKです。

#!/bin/bash
STR="aaa bbb ccc"
for HOGE in $STR
do
:
done
#!/bin/bash
STR="aaa
bbb
ccc"

for HOGE in $STR ★""で括っていないため改行が半角スペースに変換され、結果1行ずつループ
do
:
done

【補足】for 変数名 in リスト の罠

前述の通り、リストは半角スペースで区切る必要があります。
上記例で in の後の「$STR」を""でくくっていませんが、これは""でくくると半角スペースまで含んだ「aaa bbb ccc」という1つの文字列と認識してしまい、1周しかしなくなってしまうことへの対策です。

(悪い例)

#!/bin/bash
STR="aaa bbb ccc"
for HOGE in ”$STR” ★これはNG
do
    echo "$HOGE""aaa bbb ccc"が表示される
done

・for 変数名 in 配列

for inを使って配列をループすることもできます。

#!/bin/bash
ARRAY=("aaa" "bbb" "ccc")
for HOGE in ${ARRAY[@]}
do
:
done

【補足】for 変数名 in 配列 の罠

何度も書きますが、inの後は半角スペース区切りのリストを指定します。
配列の場合は上述のように「${配列名[@]}」を渡していますが、これは「${配列名[@]」と記述すると配列の中身が半角スペース区切りで表示されることを利用しているだけで、厳密には配列を回しているわけではなく、やはり半角スペース区切りのリストを渡しているだけです。
そのため、配列の中に半角スペースを含む文字列がある場合、そこで一旦区切れてしまいます。

#!/bin/bash
ARRAY=("aaa" "bb cc" "ddd")
for HOGE in ${ARRAY[@]}
do
    echo $HOGE
done

<実行結果>
aaa
bb
cc
ddd

このような場合どうすればいいかというと、次に紹介する形式のfor文を使うと解決します。

・開始条件、終了条件を指定するfor文

ループ条件を指定する形式の、よくあるタイプのfor文です。
forの後の括弧が二重になることに注意。

#/bin/bash
for ((i=0; i<3; i++))
do
:
done

これを使うと、配列の中の文字列に半角スペースを含んでいても正しくループします。

#/bin/bash
ARRAY=("aaa" "bb cc" "ddd")
for ((i=0; i<${#ARRAY[*]}; i++))
do
    echo ${ARRAY[$i]}
done

<実行結果>
$ ./test.sh
aaa
bb cc
ddd

while文

・基本のwhile文

whileの後に条件文を書きます。条件文が成り立つ間ループします(一般的なwhile文の使い方と大体同じ)。

#!/bin/bash
while [ $HOGE -lt 10 ]
do
:
done

・while read 文(ファイルの中身を1行ずつ処理)

while read文を使って、ファイルを一行ずつ読み込むことができます。

#!/bin/bash
while read LINE
do
    echo "debug:$LINE"
done < ファイルパス

・while read 文(コマンドの標準出力や変数の中身を1行ずつ処理)

コマンドの標準出力や変数の中身を1行ずつ処理する場合もwhile read文が使えますが、この場合はヒアドキュメントを使い、doneの後に「<<END」と「END」で囲ってループするものを渡します。

<コマンドラインの標準出力を渡す場合>

#!/bin/bash
while read LINE
do
    echo "debug:$LINE"
done << END
$(ls -l)
END

<文字列リストを改行区切りで直書きする場合>

while read LINE
do
    echo "debug:$LINE"
done << END
aaa
bbb
ccc
END

<改行区切り変数を渡す場合>

TESTLINE="aaa
bbb
ccc"

while read LINE
do
    echo "debug:$LINE"
done << END
$TESTLINE ★""で括らない(""が文字列として解釈されるため)
END

【補足】コマンドの出力をパイプでwhile readに渡す場合の注意点

コマンド実行結果やファイルの中身を1行ずつ回す方法として、コマンドライン出力をパイプを通してwhile readに渡す方法(「コマンドライン | while read LINE」みたいなやつ)が紹介されたりしますが、以下のような罠があるのでおすすめしません。
・whileの中で宣言・代入した変数が、whileの外に持ち越せない
・whileの中でexitしてもwhile文から抜けるのみで、抜けた後の処理を継続する

(悪い例)

TESTLINE=""
cat test.txt | while read LINE
do
    if 条件式; then
        TESTLINE=$LINE
        break
    fi
done
echo "$TESTLINE" ★while の中の代入が無効になるので空になっている

計算方法

色々あるので好みや用途に合わせてどうぞ。
ちなみによくある「i++」とか「i += 1」とかはありません。
シェルスクリプトは計算が苦手なので、小数を使うような細かい計算はさせない方が無難です。

・exprコマンド(整数計算のみ)

RESULT=$(expr $A + $B)

・bcコマンド(小数計算可能)

小数計算は可能ですが、計算結果が「0.23」等になる時に「.23」のように最初の0が省略されることに注意。

RESULT=$(echo $A * $B | bc)

・letコマンド (整数計算のみ)

以下のようにちょっと不思議な書き方をします。

let RESULT=($A - $B)

※()の中の$は省いてもOK。

・$(( )) 方式 (整数計算のみ)

整数計算であればこれが一番見た目わかりやすくてオススメ。

RESULT=$(($A / $B))

※$(( ))の中の$は省いてもOK。 

シェルスクリプトのデバッグ

以下のようにして実行すると、実行内容がすべてコンソール上に表示されます。

$ bash -x test.sh

部分的にデバッグしたい場合は、デバッグしたい箇所を「set -x」と「set +x」で囲ってください。

#!/bin/bash
<処理1set -x
<処理2> ★ここだけデバッグ
set +x
<処理3>