shell script
プログラミング
shell(シェル)はコマンドラインインターフェースとして都度コマンド要求を処理することもできますが、処理を固めて自動化することもできます。もっとも簡単なプログラム、たとえば所定の文字を出力するだけのたった2行の処理を代表的なshellであるbash(バッシュ)を使って記述してみましょう。処理をファイルに書き出し固めることで、都度コマンドで打っていた仕事が再生産可能な仕事として、自動化されます。
コマンドライン上でechoコマンドを実行した結果は以下のとおりです
$ echo "Hello"
Hello
$ echo "World"
World
上述の処理をスクリプト化すると以下コードの処理のようになります。手動実行していた処理が固められ、まとめて実施が可能になります。それではスクリプトファイルをnanoコマンドで作成してきましょう
# スクリプトの作成
nano echo.sh
nanoで開いた新規ファイルに以下echo.shのコードの内容を貼り付けて保存して下さい
#!/bin/bash
echo "Hello"
echo "World"
Note
冒頭1行目の#!/bin/bashという記載はbashスクリプトだという宣言になりますので必ずコピーして下さい。ちなみに2行目以降に使われる#(シャープ)から始まる行は、該当行がコメント扱いになり処理としては実行されません。#はコードにコメントを追記するために使われますので覚えておいて下さい
bashの引数としてファイルを指定して実行した結果は以下のとおりです、処理がまとまて実行され2行出力されることを確認して下さい。なお、引数については引数と変数を参照して下さい
$ bash echo.sh
Hello
World
条件分岐
shell scriptは上述のとおり単に処理を並べて書いていくだけでも処理を自動化できますが、様々な条件のもと処理を分岐させ、パターンに応じた望ましい処理を展開することができます。以下例ではスクリプトに対してHelloというパラメーターが渡されると、Worldと返す分岐を作成しています。その際渡されるパラメーターはVALUEという変数に格納されています。変数やif文については後述します
#スクリプトファイル作成
nano hello.sh
#!/bin/bash
VALUE=$1
if [ "$VALUE" = "Hello" ];then
echo "World"
fi
実行すると以下のような結果になります。Helloという引数を渡して実行するとWorldという返り値が返ってくることが確認できます
$ bash hello.sh Hello
World
Helloを引数として渡していない場合は、何も返ってきません
$ bash hello.sh
$
引数と返り値
スクリプト実行時に渡されるパラメーターを引数(ひきすう)といいます、また、スクリプトを実行して返される値を返り値(かえりち)といいます。上述のスクリプトではHelloが引数、Worldが返り値となります
変数
上記例でも条件分岐処理に利用されていますが、値を一時的に格納しておく箱として変数(へんすう)を利用すると処理が円滑に記述できます。変数はコマンドライン上でも、script内でもいずれもで使うことができます。例えば、以下にてtestという空の変数を定義しましょう
test=""
変数testの内容はechoコマンドで出力することができます。変数であることは$を頭につけることで宣言できます
$ echo $test
! 空のため何も返ってこない
次に変数testに、値を入れて出力してみましょう。変数に格納する文字列は""(ダブルクオーテーション)で囲んでください
test="123"
変数の中身を見てみましょう
$ echo $test
123
変数にはコマンド実行結果を格納することもできます、その際、コマンドは``(バッククォート)で囲んでください。以下例ではdateコマンドの実行結果を格納しています。shellは、コマンドライン上でもスクリプト内でも同じように変数を扱うことができ、豊富なコマンドの出力結果を変数に直接代入できますが、操作からscript化まで非常に簡単に移行でき、コマンドを用いた様々な分岐を作ることができます。これはshellの非常に有用な特徴であり、利点であるといえます
test=`date`
コマンド結果を格納した変数の中身を表示しましょう
$ echo $test
2024年 11月 18日 月曜日 10:03:23 JST
論理演算と判定
条件分岐のためには論理演算を多用します。[]内で実行される論理演算では、各種判定をおこない、結果に応じてif文やwhile文が分岐を制御します
文字の比較
単純に右側と左側の文字列が同じであるかを判定する場合、単に=を使います。=の場合は同じ値である場合に真となり、!=はその逆の異なる値の場合に真とします
#同じ場合に真
[ "文字列A" = "文字列B" ]
以下はそれぞれの変数に文字列を代入し、同一かどうかを判定した例です。 まずは変数にそれぞれ異なる値を格納しましょう
WORD_A="abc"
WORD_B="def"
論理演算を実行します。$?は特殊な変数で直前の実行コマンドの実行結果が正常(0)であったか異常(1)であったかを表示します
$ [ "$WORD_A" = "$WORD_B" ]
$ echo $?
1
> WORD_AとBは違うため異常(1)が返ってきます
次に、変数にそれぞれ同じ値を格納しましょう
WORD_A="abc"
WORD_B="abc"
論理演算の実行結果が正常(0)になることが確認できます
$ [ "$WORD_A" = "$WORD_B" ]
$ echo $?
0
> WORD_AとBが同じため正常終了しています
Note
本来の論理演算では真は1とされ、偽は0とされるべきです。上述の例が逆になっているのは$?変数が直前の処理結果が正常終了であれば0として、異常終了であれば1とするためです。論理演算が真(1)であった場合は、処理が正常に終了したとみなされ結果は(0)と表現されます。
数値の比較
数値どうしの判定に際してはいくつか使用可能なオプションがあります、以下をご確認ください。ちなみにshellの変数には特に何も意識せず変数には数値も文字も入れられます
# equal, 同じ値の場合に真
[ 数値A -eq 数値B ]
# equalの逆、異なる値の場合に真
[ 数値A -ne 数値B ]
# less than, 左側の数値が右側の数値より小さければ真
[ 数値A -lt 数値B ]
# less than or equal, 左側の数値が右側の数値と同値かそれより小さければ真
[ 数値A -le 数値B ]
# greater than or equal, 左側の数値が右側の数値より大きければ真
[ 数値A -gt 数値B ]
# greater than or equal, 左側の数値が右側の数値と同値かそれより大きければ真
[ 数値A -ge 数値B ]
存在判定
ファイルやディレクトリの存在有無で分岐することも可能です、例えば全行程の処理が終わった場合にフラグファイルを作成し、そのファイルが存在すれば後工程に進むといったプログラムを書くことができます
# ファイルが存在する場合に真
[ -f 対象ファイルPATH ]
# ディレクトリが存在する場合に真
[ -d 対象ディレクトリPATH ]
以下はdoneというファイルがあるかどうかで結果がどう変わるかを示した例です。 まずは、ファイルを作成しましょう
touch done
存在確認を実施すると正常値が返ってきます
$ [ -f ./done ]
$ echo $?
0
> ファイルが存在するため正常(0)が返ってきます
続いてdoneファイルを削除します
rm done
ファイルが存在しないため異常値が返ってきます
$ [ -f ./done ]
$ echo $?
1
> ファイルが存在しないため異常(1)が返ってきます
空判定
変数が空(null)か、ファイルが空(0byte)かといった判定が可能です。空判定はコマンド実行結果を変数に格納して、結果の有無で分岐することができ便利です。
# 変数が空(null)ではない場合に真
[ "変数" ]
# ファイルが空でない場合に真
[ -s 対象ファイルPATH]
以下はos-releaseファイルで任意の文字を検索して結果が空だったかどうかを判定しています。 まずは変数に空の値を格納しましょう
NULL_CHECK=""
続いて論理演算を実行してみましょう
$ [ "$NULL_CHECK" ]
$ echo $?
1
> 変数が空のため異常(1)が返ってきます
続いて変数に文字を格納します
NULL_CHECK="test"
変数が空ではないため正常終了します
$ [ "$NULL_CHECK" ]
$ echo $?
0
> 変数に値が入っているため正常終了(0)が返ってきます
否定
!を使うと、条件式の真と偽りが逆になります
# 文字列が異なる場合に真
[ "文字列A" != "文字列B" ]
# 数値が異なる値の場合に真
[ ! 数値A -eq 数値B ]
# 変数が空(null)
[ ! "変数" ]
# 空ファイルの判定
[ ! -s 対象ファイルPATH ]
高度な分岐
or条件
-oオプションを使うとor条件として機能させることができます、or条件は2つ以上でも記述可能です
#条件式AもしくはBが成立すれば真
[ 条件式A -o 条件式B ]
#条件式AもしkはB,Cが成立すれば真
[ 条件式A -o 条件式B -o 条件式C ]
and条件
-aオプションを使うとand条件として機能させることができます、and条件は2つ以上でも記述可能です
#条件式A及びBが成立すれば真
[ 条件式A -a 条件式B ]
#条件式A,B,Cが成立すれば真
[ 条件式A -a 条件式B -a 条件式C ]
正規表現の利用
分岐に正規表現を利用することもできます、その場合[[ ]]で囲んでください
[[ 文字列A == 文字列B ]]
# 例:aからはじまりdで終わる任意の文字列であれば真
[[ "変数" == a*d ]]
# valueにabcを格納
$ value=abc
$ [[ "$value" == a*d ]]
$ echo $?
1
> abcはaからはじまりcで終わるため異常終了(1)が返ってきます
# valueに別のaから始まりdで終わる値を格納
$ value=aaabbbcccddd
$ [[ "$value" == a*d ]]
$ echo $?
0
> aからはじまりdで終わるため正常終了(0)が返ってきます
if文
それでは早速プログラミングを組み上げていきましょう。if文による条件分岐は通常以下のように記載されます。以下例では文字列Aと文字列Bが同一であった場合、処理Aが実行されます。処理を柔軟にするため文字列Aは変数を使うのが良いでしょう
if [ "文字列A" = "文字列B" ];then
処理A #条件式が真の場合に実行
else
処理B #条件式が偽りの場合に実行
fi
本コースの冒頭で紹介した条件分岐の例にelse処理も加えて実行してみましょう
#スクリプトファイル作成
nano hello.sh
#!/bin/bash
VALUE=$1
if [ "$VALUE" = "Hello" ];then
echo "World"
else
echo "Hello?"
fi
$ bash hello.sh
Hello?
$ bash hello.sh Hello
World
for文
for文を使うとリストを変数に格納しながら1つずつ処理することが可能です
for 変数 in リスト
do
処理 変数
done
for文のリストにはコマンドの出力結果もつかえますので、たとえばディレクトリ内のファイル一覧を出力し、ファイルを1つずつ処理するという場合に便利です。以下例ではfor文を使ってファイルの名前に一律.txtを付与しています
# ディレクトリとファイルを作成
mkdir for_test
touch ./for_test/fileA
touch ./for_test/fileB
touch ./for_test/fileC
touch ./for_test/fileD
#スクリプトファイル作成
nano for_test.sh
以下for_test.shのコードをコピーしてファイルを保存して下さい
#!/bin/bash
DIR=./for_test
for file in `ls $DIR`
do
mv ${DIR}/${file} ${DIR}/${file}.txt
done
Note
Tips : 変数を{}で囲むとどこまでが変数かを示すことができます。lsの出力結果を$file変数に格納しているため、${file}としています
以下が実行結果になります。lsコマンドでファイルに.txt拡張子がつけられたことを確認して下さい
# スクリプト実行前の状態
$ ls for_test
fileA fileB fileC fileD
# スクリプト実行
$ bash for_test.sh
# 実行後のファイルの拡張子を確認
$ ls for_test
fileA.txt fileB.txt fileC.txt fileD.txt
while文
while文は条件式が真の間、処理をループさせます
while [ 条件式 ]
do
処理
done
以下は1から10までカウントしながら出力するwhile文を使った処理の例です
#スクリプトファイル作成
nano while_test.sh
#!/bin/bash
count=0
while [ $count -le 10 ]
do
echo $count
((count++))
done
$ bash while_test.sh
1
2
3
4
5
6
7
8
9
10
コマンドライン読み取り
readはコマンドラインでの入力値を読み取り、変数に格納します。
read 変数
以下は入力をinputという変数に格納して表示した例になります
#スクリプトファイル作成
nano read_test.sh
#!/bin/bash
echo -n "あなたの名前を入力してください: "
read name
echo "こんにちは$nameさん"
$ bash read_test.sh
あなたの名前を入力してください: 田中(入力値)
こんにちは田中さん
引数読み取り
$数字はスクリプトに渡されたパラメータを順番に変数に格納します、以下例では1-3までのパラメーターを読み取っています
#スクリプトファイル作成
nano param_test.sh
#!/bin/bash
param1=$1
param2=$2
param3=$3
echo param1:$param1
echo param2:$param2
echo param3:$param3
$ bash param_test.sh aaa bbb ccc
param1:aaa
param2:bbb
param3:ccc
case
case文は変数の値がマッチした場合の処理をそれぞれ記述できます
case 変数 in
パターン1) 処理;;
パターン2) 処理;;
esac
以下は与えられたパラメーターがAかBかそれ以外かで処理を分岐させた例になります、*は任意の文字列を意味するメタキャラクターですがここではA,Bに該当しなかった場合の処理として使われます
#スクリプトファイル作成
nano case_test.sh
#!/bin/bash
echo -n "AかBを選んでください: "
read input
case $input in
A) echo "正解です";;
B) echo "不正解です";;
*) echo "AかBを入力してください";;
esac
$ bash case_test.sh
AかBを選んでください: A
正解です
ファイルの読み込み処理
While文を使ってファイルを1行ずつ読み込むといった処理ができます
while read 変数
do
処理
done < ファイル名
以下はテキストをHTMLタグを付与した別ファイルにする例です
#テキストファイル作成
nano org.txt
org.txt(テキストファイル)の内容をファイルに転記して保存して下さい
abc
def
efg
#スクリプトファイル作成
nano ch2html.sh
#!/bin/bash
while read line
do
echo "<p>$line</p>" >> ch2html.txt
done < org.txt
以下が実行結果になります、テキストファイルが新たに生成され、<p>というHTMLタグが付与されたことを確認して下さい
$ bash ch2html.sh
$ cat ch2html.txt
<p>abc</p>
<p>def</p>
<p>efg</p>
実行権限の付与
実行権限を付与していないスクリプトは本来はそのままでは実行できません、これまでの例では"bash スクリプト"というかたちで実行してきたことで、ファイルの読み取りさえできれば内容をコマンドのbashに渡して処理してきました。直接ファイルを指定するだけで実行可能な状態にするためにはchmodコマンドで実行権限を付与しましょう。755の権限付与がプログラムとしては最適です。chmodコマンドの詳細はあらためてパーミッションにて確認してください
# PATHを指定してファイルを直接実行 (./は現在のディレクトリを意味する)
$ ./case_test.sh
-bash: ./case_test.sh: Permission denied (実行失敗)
# ファイルの直接実行のため権限を変更
$ chmod 755 case_test.sh
$ ./case_test.sh
AかBを選んでください: A
正解です
デバッグ
bash実行時に-xオプションを付与すると、スクリプトをデバッグモードで実行できます。これによりスクリプトの処理経過が可視化できます
bash -x スクリプト
以下は再びcase_test.shを使った例ですが、コードの各ステップの処理が標準出力にデバッグ出力されるため、コード内で正常に処理できているか確認することができます
$ bash -x case_test.sh
+ echo -n 'AかBを選んでください: '
AかBを選んでください: + read input
C
+ case $input in
+ echo AかBを入力してください
AかBを入力してください
+ exit 1
処理結果が長い場合は、debugファイルに書き出して中身を確認しましょう
bash -x スクリプト > debug 2>&1
関数の利用
functionを用いるとスクリプト内で同じ処理を何度も呼び出すことができます。以下は入力がASCIIテキストかどうか判定する関数を使ったスクリプトの例です、ASCIIテキスト以外ではエラーを返します
#スクリプトファイル作成
nano ascii_chk.sh
#!/bin/bash
# ascii check
function ascii_check(){
input=$1
check=`echo "$input" | file -i - | grep -i ascii`
if [ "$check" ]; then
echo "ASCII"
return 0
else
echo "ERROR"
return 1
fi
}
echo -n "入力をしてください: "
read INPUT
ascii_check $INPUT
exit 0
$ chmod 755 ascii_chk.sh
$ ./ascii_chk.sh
入力をしてください: test
ASCII
$ ./ascii_chk.sh
入力をしてください: テスト
ERROR
Tips
スクリプト作成に際して便利な知識をTipsとして以下にまとめます
awk,sedの多用
while文で処理を行うのに比べ、awkやsedを使ったほうが処理も端的に記述でき高速化することができます、以下はwhile文で記述したファイル読み込み処理を1ラインで記述した例です。awk, sedとメタキャラクターを組み合わせることで様々な処理が記述できます
# sed
$ time cat org.txt | sed "s/^/<p>/g" | sed "s/$/<\/p>/g"
<p>abc</p>
<p>def</p>
<p>efg</p>
# awk
$ time cat org.txt | awk '{print "<p>"$1"</p>"}'
<p>abc</p>
<p>def</p>
<p>efg</p>
終了ステータス
$?は直前のコマンドが正常終了した場合に0を格納し、異常値だった場合に0以外を格納する特殊な変数です。以下例では一般ユーザーが/rootを閲覧してエラーになっていますが、終了ステータスは2が返されています
$ ls /root
ls: cannot open directory '/root': Permission denied
$ echo $?
2
exit
スクリプト自体が正常終了だったかどうかを確認するために最後に終了コードを設定しましょう
0を返す場合は正常終了を表します、scriptの処理の最後に記述しましょう
exit 0
1を返す場合は異常終了であることを通知できます
exit 1
先ほどcaseで作成したスクリプトに終了コードを付与してみましょう
nano case_test.sh
--code--
#!/bin/bash
echo -n "AかBを選んでください: "
read input
case $input in
A) echo "正解です"
exit 0;;
B) echo "不正解です"
exit 0;;
*) echo "AかBを入力してください"
exit 1;;
esac
--------
想定がの入力値のCを入力されたためにエラーステータスを返します
$ bash case_test.sh
AかBを選んでください: C
AかBを入力してください
$ echo $?
1
load
パラメーター一覧が記載されたファイルを直接読み込み一度に変数をロードすることができます。このテクニックをスクリプトの冒頭に入れれば、外部環境ファイルを毎回読み込むことなどができます
. 設定ファイル
以下例ではparams.envという設定ファイルを作成し、読み込みます
nano params.env
param.envの内容を貼り付けてファイルを保存して下さい
param1="abc"
param2="def"
# param.envの内容を読み込みロードします
$ . params.env
# param1変数の内容が格納されていることを確認します
$ echo $param1
abc
隠しファイル
ファイル名やディレクトリ名の先頭に.をつけると隠しファイルとして扱われます。隠しファイルの確認はls -aオプションで確認できます
$ touch .test
$ ls
$ ls -a
. .. .test
sleep
処理を任意の秒数停止したい場合、sleepコマンドを使うと便利です
sleep 秒数
スクリプトの強制終了
実行中のスクリプトを停止したい場合、ctrl + cを押すことで停止することができます。以下は30秒sleepするスクリプトを途中停止する例です
# スクリプトの作成
nano sleep.sh
#!/bin/bash
sleep 30
exit 0
ファイルが作成できたら実行してみましょう、30秒応答がなくなりますので途中でctrl + cを入力して抜けてみましょう
$ bash sleep.sh
ctrl + cで強制停止
バックグラウンドでの実行
上述のsleepを含むようなスクリプトは処理に時間がかかるため、処理を裏で動かして継続したい場合があります。そういった場合は&をつけてバックグラウンドで実行することができます。以下実行例では15293というプロセスIDが付与され、背後で実行されていますがpsコマンドで動いていることを確認できます
$ bash sleep.sh &
[1] 15388
# プロセスの確認
$ ps -ef | grep 15388
naruo 15388 12575 0 10:41 pts/2 00:00:00 bash sleep.sh
プロセスのkill
バックグラウンドで実行しているスクリプトや後述の自動実行しているプロセスを終了したい場合、killコマンドで終了することができます
kill プロセスID(PID)
上述のバックグラウンドで実行しているスクリプト(PID 15388)を終了させたい場合、以下のように終了します
kill 15388
ファイル名のみ抽出
lsコマンドでリスト表示する際にディレクトリ名は無視してファイル名だけを抽出したい場合があります、そういう場合xargsコマンドとbasenameコマンドを組み合わせてください。
xargs basename -a
$ ls /var/log/apache2/access.log
/var/log/apache2/access.log
$ ls /var/log/apache2/access.log | xargs basename -a
access.log
grepコマンドでファイル名だけを表示したい場合は -l オプションを付けてください
grep -l
# オプションなし実行
$ grep ubuntu /etc/os-release
ID=ubuntu
HOME_URL="https://www.ubuntu.com/"
SUPPORT_URL="https://help.ubuntu.com/"
BUG_REPORT_URL="https://bugs.launchpad.net/ubuntu/"
PRIVACY_POLICY_URL="https://www.ubuntu.com/legal/terms-and-policies/privacy-policy"
# ファイル名のみ抽出
$ grep -l ubuntu /etc/os-release
/etc/os-release
実行結果の変数への格納
コマンド実行結果を変数に格納する方法はいくつかありますが、本コースの冒頭でご紹介した``(バッククォート)以外にもいくつかあります
#バッククォート
変数=`実行コマンド`
#$()
変数=$(実行コマンド)
変数をコマンドとして実行
コマンド変数内で組み立てて、実行することもできます
eval 変数
$ ls_exe="ls /var/log"
$ eval $ls_exe
alternatives.log apache2 apt btmp dist-upgrade dpkg.log journal landscape lastlog unattended-upgrades wtmp
後続のsmall shellのmetaコマンドではevalを使って変数内で組み立てたコマンドを実行していますので参考にして下さい
if [ "$type" = "json" ];then
header=`grep "^name=" $ROOT/databox/${databox}/def/col* | sort -V | awk -F"=" '{print $2}' | sed "s/\"//g" \
| sed "s/ //g" | sed -z "s/\n/,/g" | sed "s/,$//"`
JQ="echo \$header | jq -R 'split(\",\") | {"
count=0
for col in `ls $ROOT/databox/${databox}/def/col* | sort -V | xargs basename -a`
do
if [ $count -eq 0 ];then
JQ="${JQ}\"$col\": .[$count]"
else
JQ="${JQ}, \"$col\": .[$count]"
fi
((count += 1))
done
JQ="${JQ}}'"
eval $JQ
fi
tmpファイルの多用
処理を組み立てていく際にevalのように変数の中でコマンドを組み立てながら実行する方法もありますが、処理をテンポラリーファイルを作って過程を書き出しておくことであとからdebugが容易になります
別スクリプト呼び出し
スクリプト内で別スクリプトを呼び出す際は単純にスクリプトをそのまま実行すれば良いです。呼び出すという意味ではコマンドとスクリプトは本質的にあまり変わりがありません
スクリプトの定期実行
cron(クーロン)を利用することでスクリプトを自動で定期実行することができます。small-shellを導入することでcronをより簡単に利用できるようになります
さらなる学習のために
高度なプログラミング言語において、それぞれの処理をモジュール化し疎結合にする手法がとられます。この手法はオブジェクト指向と呼ばれ、プログラムを学ぶ上で必要性を理解できず挫折する契機にもなる手法でもあります。bashでは処理をオブジェクト指向を実現するためのクラスやインスタンスの生成といったことがそもそもできません。代わりにコマンドや外部スクリプトを組み合わせてモジュールとして使います。即席で平易であることがshellスクリプトの特徴ですが、その特性を最大限に活用して作ったフレームワークがsmall shellです。small shellを使うことで非常に簡単にWEBアプリケーションも作ることができますので、ぜひsmall-shell Basicもご確認ください