コンテンツにスキップ

shell script

プログラミング

shell(シェル)はコマンドラインインターフェースとして都度コマンド要求を処理することもできますが、処理を固めて自動化することもできます。もっとも簡単なプログラム、たとえば所定の文字を出力するだけのたった2行の処理を代表的なshellであるbash(バッシュ)を使って記述してみましょう。処理をファイルに書き出し固めることで、都度コマンドで打っていた仕事が再生産可能な仕事として、自動化されます。


コマンドライン上でechoコマンドを実行した結果は以下のとおりです

実行例
$ echo "Hello"
Hello

$ echo "World"
World

上述の処理をスクリプト化すると以下コードの処理のようになります。手動実行していた処理が固められ、まとめて実施が可能になります。それではスクリプトファイルをnanoコマンドで作成してきましょう

# スクリプトの作成
nano echo.sh

nanoで開いた新規ファイルに以下echo.shのコードの内容を貼り付けて保存して下さい

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
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

hello.shのコード
#!/bin/bash
VALUE=$1
if [ "$VALUE" = "Hello" ];then
  echo "World"
else
  echo "Hello?"
fi
スクリプトができたら実行してみましょう、Helloという入力を渡さないと聞き返されます

実行例
$ 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のコードをコピーしてファイルを保存して下さい

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

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

read_test.shのコード
#!/bin/bash
echo -n "あなたの名前を入力してください: "
read name
echo "こんにちは$nameさん"
以下が実行結果となります
実行例
$ bash read_test.sh
あなたの名前を入力してください: 田中(入力値)
こんにちは田中さん

引数読み取り

$数字はスクリプトに渡されたパラメータを順番に変数に格納します、以下例では1-3までのパラメーターを読み取っています

#スクリプトファイル作成
nano param_test.sh

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

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(テキストファイル)の内容をファイルに転記して保存して下さい

org.txtファイル
abc
def
efg
次にスクリプトファイルを作成しましょう

#スクリプトファイル作成
nano ch2html.sh
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

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の内容を貼り付けてファイルを保存して下さい

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
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もご確認ください