警告

本資料は参考情報として提供されています。内容についてのご質問には必ずしもお答えできない可能性がございます。

9. シェルスクリプト入門

ここではシェルスクリプトについて簡単に紹介します。

シェルスクリプトはシェルで実行する一連のコマンドを書き連ねたプログラムです。特定の処理をファイルにまとめておくことで、解析の管理と実行、そして再現性の確保が容易になります。また、ジョブスケジューラのように他のサーバーで同じ処理を簡単に実行できるようになります。

9.1. シェルスクリプトの基本

これは環境変数HOMEを表示するシェルスクリプトの例です。

1#!/bin/bash
2
3set -eu -o pipefail
4
5echo $HOME

これを例えばscript.shというファイルに書き込んだときに、シェルスクリプトとして実行するには次のような操作をします。

$ chmod +x script.sh
$ ./script.sh
/home/project/personal-id

以下、順番に解説します。

9.1.1. シバン(shebang)

script.shの1行目には#!に続けてbashへのパスが記述されています。これはシバン (shebang)と呼ばれていて、スクリプトをどのプログラムで実行すべきかを指示するための表記です。例ではbashが指定されていますが、Bourne Shellを使う場合は#!/bin/shであったり、pythonのようなシェル以外のスクリプトであれば、envコマンドを用いて#!/usr/bin/env python3のように書かれます。

シバンは1行目に書く必要があります。また、#!とパスの間にスペースを入れることはできません。

多くの場合、スクリプトの種類は拡張子で判別可能ですが、例えばシェルスクリプトの拡張子は.shが使われますが、それがbashやzshなど様々なシェルのうちどれで実行すべきスクリプトかは分かりません。シェルの機能や仕様はその種類によって微妙に異なるため、あるシェルを想定して書かれたシェルスクリプトを別のシェルで実行すると不具合が起きることもあります。シバンには、このような場合にも実行に使うプログラムを明示し、判別を容易にする効果もあります。

9.1.2. シェルスクリプトにおけるエラーの扱い

他のプログラミング言語と違い、何も設定していないシェルスクリプトは、途中でコマンドが失敗しても処理が中断されません。これは多くの場合、誤った状態のまま続きのコマンドを実行してしまうことになります。また、使われていない変数を参照したときも、空文字が返されるもののエラーにはなりません。そのため、変数名のタイプミスに中々気づかないこともシェルスクリプトではありがちなことです。

このような仕様は一連の処理を行う時に都合が悪いため、シェルにオプションを設定することで挙動を変えることができるようになっています。script.shの3行目では、set コマンドでその設定を行っています。

-e

実行したコマンドがエラーになった時点で実行を中断する

-u

未定義の変数を使おうとした時にエラーとする

-v

コマンド実行前にスクリプトの対応する行を表示する

-x

シェルが実際に実行するコマンドを出力する-vと異なり変数等も展開された状態で表示される

-o pipefail

-eだけだとパイプの最後のコマンドしかエラーになったか確認されないので、パイプの全てのコマンドを確認し、一つでもエラーとなった場合に終了するようにします。これはbashzshなど一部のシェルで利用できます。これを利用するときにはshebangを#/bin/bashとしておくことをお勧めします。

特別な理由がなければ、set -eu -o pipefailをシェルスクリプトの冒頭で宣言することをおすすめします。

9.1.3. 実行権限

例ではシェルスクリプトを実行する前に、chmod +x script.shというコマンド(→chmodコマンド)でファイルに実行権限を与えています。Linuxにはファイルの権限に、読み込み・書き込みの他に、実行権限があります(→アクセス権の管理)。実行権限は、あるファイルがプログラムとして解釈できる場合に、それを実行するのに必要な権限です。例えば、シェル上で実行するコマンドの実態を確認すると、どれも実行権限が付与されていることがわかります。

$ which ls
/bin/ls
$ ls -l /bin/ls
-rwxr-xr-x  1 root  wheel  187040  4  1 12:00 /bin/ls

シバンを書いたスクリプトに実行権限を付与し、スクリプトへのパスをコマンドのように書くことで、それをシバンで指定したプログラムで実行できるようになります。

9.2. コマンドの改行

シェルでは1行が1コマンドとして扱われますが、これは長いコマンドを記述する際に不便です。そのような場合は、バックスラッシュを行末に置くことで改行を無視させることができます。次に示すのは、長いコマンドを複数行にわたって記述した例です。

some-complex-command \
    --long-option "here comes a lot of parameter" \
    --another-long-option or/here/you/have/to/write/a/long/file/path \
    --yet-another-long-option

9.3. 終了ステータス

終了ステータスはプログラム(プロセス)が終了した時に、呼び出し元に返す数値のことです。数値はプログラムの処理が成功・失敗したか(あるいは失敗の種類)を示しており、これを元に処理の中断といった条件分岐を行うことができます。

シェルでは、直前に実行したコマンドの終了ステータスが、$?という特別な変数に格納されます。

$ ls exists.txt  # 正常終了するコマンド
exists.txt
$ echo $?
0
$ ls nosuchfile.txt  # エラーになるコマンド
ls: nosuchfile.txt: No such file or directory
$ echo $?
1

慣例として、正常終了したときの終了ステータスは0で、それ以外(1〜255)の終了ステータスは異常終了を示します。異常終了時のステータスの値はエラーの種類に対応しますが、その意味はプログラムごとに異なります。

9.4. AND演算子とOR演算子

AND演算子とOR演算子を使うことで、終了ステータスに応じてコマンドの実行を中断させたり、エラー時に処理を追加することが簡易に記述できます。

9.4.1. AND演算子

AND演算子&&パイプのように2つのコマンドを繋げるように記述して使います。

$ mkdir log && cp batch.log log/

このとき、&&の左側にあるコマンドは普通に実行されますが、右側にあるコマンドは、左側のコマンドが終了ステータス0で終了したとき、すなわち処理が成功した時だけ実行されます。

$ ls
a.txt b.txt
$ mkdir data && mv a.txt data/
$ mkdir data && mv b.txt data/
mkdir: data: File exists
$ ls
b.txt data

3つ以上のコマンドを繋げることも可能です。

$ mkdir log && cp batch.log log/ && cd log/

9.4.2. OR演算子

OR演算子||も書き方はAND演算子と同様です。OR演算子の場合は、右側にあるコマンドが終了ステータス0以外で終了したとき、右側のコマンドが実行されます。

$ ls
a.txt
$ rm a.txt || echo "rm command failed"
$ rm b.txt || echo "rm command failed"
rm: b.txt: No such file or directory
rm command failed

9.5. 制御文とtestコマンド

シェルスクリプトも他のプログラミング言語と同じように、ifによる条件分岐やforによるループ処理を記述することができます。

9.5.1. if文

シェルスクリプトにおけるif文は次のように書きます。

if command-list
then
    command
    ...
elif command-list
then
    command
    ...
else
    command
    ...
fi

command-listには、1つのコマンドや、&&||で繋げたコマンドが入ります。シェルでは、条件判定の真偽値として終了ステータスを使い、0であれば真、それ以外は偽となります。

thencommand-listの終わりを表すためのキーワードで、次のように書いても構いません。

if command-list; then
    command
    ...
fi

また、コマンドライン上などで、if文全体を1行で書くこともできます。

$ if command-list; then command; elif command-list; then command; else command; fi

elifelse節は省略可能です。また、elif複数回記述可能です。

9.5.2. testコマンド

if文の条件を記述するのに便利なのがtestコマンドです。文字列・数値比較やファイルの有無を判定して、結果に合った終了ステータスを返します。

$ STEP=1
$ test "$STEP" -eq 1
$ echo $?
0
$ CHAR=a
$ test "$CHAR" = b
$ echo $?
1

testコマンドはif文などで多用されるため、[という名前でも定義されています。次のコマンドは、先程の例と等価です。

$ STEP=1
$ [ "$STEP" -eq 1 ]
$ CHAR=a
$ [ "$CHAR" = b ]

[の後の条件はコマンドの引数として書く必要があるため、1つ以上のスペースで離す必要があることに注意が必要です。

ヒント

testコマンド中で変数を使う場合で、変数が空になる可能性がある場合は、ダブルクオーテーションでクォートする必要があります。変数が空の場合、testコマンドが引数が足らないと判断しエラーになってしまうためです。

$ echo $TASKID  # 未定義の変数

$ [ $TASKID -eq 1 ]  # `[  -eq 1 ]` と解釈される
[: unknown condition: -eq
$ [ "$TASKID" -eq 1 ]  # `[ "" -eq 1 ]` 空文字と比較と解釈される

後述の[[ ... ]]を使用する場合、クォートは不要です。

test コマンドの主なオプション

書式

真(終了ステータス0)になる条件

[ text1 = text2 ]

text1とtext2が同じ

[ text1 != text2 ]

text1とtext2が違う

[ -z text ]

textの長さが0

[ -n text ],[ text ]

textの長さが1以上

[ n1 -eq n2 ]

n1とn2が同じ整数

[ n1 -ne n2 ]

n1とn2が異なる整数

[ n1 -gt n2 ]

n1 > n2

[ n1 -ge n2 ]

n1 >= n2

[ n1 -lt n2 ]

n1 < n2

[ n1 -le n2 ]

n1 <= n2

[ -e path ]

pathが存在するとき

[ -f path ]

pathがファイルのとき

[ -d path ]

pathがディレクトリのとき

[ -s path ]

pathのファイルが空でないとき

[ ! cond ]

条件式condの反転

[ cond1 -a cond2 ]

and

[ cond1 -o cond2 ]

or

なお、bashには[[ ... ]]という文法もあり、testコマンドとほぼ同じ使い方ができます。こちらはクォートが不要・追加のオプションや文字列のパターンマッチングが行えます。

9.5.3. for文

for文は複数の値に対して同じ処理を繰り返し実行することができます。

for name in words ...
do
    command
    ...
done

if文と同様、fordoを同じ行に書く形式もよく使われます。

for name in words ...; do
    command
    ...
 done

nameには変数名を指定します。words ...にスペース区切りで値を並べると、for文は指定された値を順次ループ変数に代入しながら、for文内のコマンドを繰り返し実行します。

$ for char in a b c; do echo $char; done
a
b
c

また、ループ制御としてbreakcontinueがサポートされています。

#!/bin/bash

for char in a b c; do
    if [ "$char" = b ]; then
        break
    fi
    echo $char
done
$ ./for.sh
a

9.5.4. while文

while文は指定した条件が成立する間、処理を繰り返すことができます。

while command-list
do
    command
    ...
done

例えば、readコマンドと組み合わせることで、ファイルの行ごとにループ処理させることができます。

#!/bin/bash

# `line`は変数名の指定
while read line; do
    echo $line
done
$ ./while.sh < no.txt
1
2
3

9.5.5. case文

case文では、複数の条件に対して処理を用意し、条件に合った処理のみを実行させることができます。

case text in
    pattern1) command... ;;
    pattern2) command... ;;
    ...
esac

textに指定した値(普通は変数)があるパターンにマッチした時、それに対応するコマンドが実行されます。ただし、複数のパターンにマッチする場合は、最初にマッチした処理のみが実行されます。

パターンと;;の間は複数のコマンドを記述でき、改行も可能です。

case text in
    pattern1)
        command
        ...
        ;;
    pattern2)
        command
        ...
        ;;
esac

パターンにはワイルドカードとして*,?,[...],[!...]が使用できます。

case "$filepath" in
    *.gz) zcat "$filepath" ;;
    *.bz2) bzcat "$filepath" ;;
    *) cat "$filepath" ;;
esac

9.6. シェルスクリプトにおける変数

ヒント

変数についてはシェルにおける変数もご覧ください。

9.6.1. 配列

bashでは、通常の変数の他に1次元配列を変数として使うことができます。配列は()を使うことで定義できます。

ARRAY=()  # 空の配列を定義
ARRAY=(1 2 3)  # 初期値を指定して配列を作成

配列の操作は次のようにします。

$ ARRAY=(1 2 3)
$ echo $ARRAY  # 通常の変数のように参照すると、最初の要素だけ返す
1
$ echo ${ARRAY[0]}  # 1番目の要素を参照。インデックスは0から
1
$ echo ${ARRAY[1]}
2
$ echo ${ARRAY[@]}  # 配列全体を表示
1 2 3
$ echo ${ARRAY[*]}  # 配列全体を表示(`@`と同じ)
1 2 3
$ ARRAY+=(4 5)  # 末尾に配列を結合
$ ARRAY=(0 ${ARRAY[@]} 6)  # 配列の再定義による要素の追加
$ echo ${ARRAY[@]}
0 1 2 3 4 5 6
$ echo ${#ARRAY[@]}  # 要素数を取得
7

スペースを含む要素を配列で扱う場合は注意が必要です。

$ TEXT="with space"
$ ARRAY=(text $TEXT)  # クオートしないとスペースで要素が分割される
$ echo ${ARRAY[1]}
with
$ ARRAY=(text "$TEXT")
$ echo ${ARRAY[1]}
with space
$ for text in ${ARRAY[@]}; do echo $text; done  # 配列内では1要素でも、クオートがないと要素が分割されて解釈される
text
with
space
$ for text in "${ARRAY[@]}"; do echo $text; done  # `${ARRAY[@]}`をクオートすると、各要素をクオートした状態で展開してくれる
text
with space

注釈

${ARRAY[@]}${ARRAY[*]}はクオートをした時にのみ異なる結果を返します。"${ARRAY[@]}"は各要素をクオートした状態で展開しますが、"${ARRAY[*]}"は配列全体を1つの値として返します。

$ for text in "${ARRAY[*]}"; do echo $text; done
text with space

9.6.2. 連想配列

bashでは配列の他に連想配列も定義できます。連想配列は、要素へのアクセスに、インデックスの代わりに任意の文字列を使用できる配列です。

$ declare -A HASH  # 連想配列を宣言
$ HASH=([gz]="GNU zip" [bz2]=bzip2)  # 連想配列を定義
$ echo ${HASH[gz]}  # 要素へのアクセス
GNU zip
$ HASH[xz]=XZ  # 要素の追加
$ echo ${HASH[@]}  # バリュー一覧
GNU zip bzip2 XZ
$ echo ${!HASH[@]}  # キー一覧
gz bz2 xz

9.6.3. 特殊な変数

9.6.3.1. 位置パラメーター

シェルスクリプトでも、一般的なコマンドと同じように引数を受け取ることができます。指定された引数は$1,$2, ...,$9という特殊な変数に格納されます。これは位置パラメーターと呼ばれています。

#!/bin/bash

echo $1
echo $2
echo $@
$ ./positional-parameter.sh param1 pram2
param1
param2
param1 pram2

$@$*という変数からすべての引数を参照できます。また、$#は引数の数を、$0はコマンドそのもの(実行時のシェルスクリプトのパス)を表します。

$10以降は定義されていません。10個目以降の引数を参照するには、$@から配列を作成したり、shiftコマンドを使用します。

9.6.3.2. $?(終了ステータス)

終了ステータス

9.6.4. パラメーター展開

パラメーター展開は、${...}によって変数を参照(展開)するときに、変数の値に応じて既定値を設定したり、値の一部を切り取り・置換する機能です。

9.6.4.1. 変数の切り取りと置換

変数の前方・後方の特定の文字列を削除したり、一部の文字を置換することができます。例えばパスを格納した変数からファイル名のみを取り出したり、拡張子を削除するのに便利です。

変数の値を加工して出力するパラメーター展開

表記

効果

${parameter#word}
${parameter##word}
先頭から word に一致するパターンがある場合、一致した箇所を削除して出力する。
# は最短一致、## は最長一致する。
${parameter%word}
${parameter%%word}
後方から word に一致する場合、一致した箇所を削除して出力する。
% は最短一致、%% は最長一致する。
${parameter/pattern/string}
${parameter//pattern/string}
pattern に一致する箇所を string で置換する。
最初のマッチのみ置換されるが、// の場合はパターンに合うすべての箇所を置換する。
${parameter:offset}
${parameter:offset:length}
offset には整数を指定する。変数に格納されている値の offset 文字目以降を返す。
length も指定した場合は、そこから length 文字だけ返す。また負の値を指定する と、末尾から -length 文字目までを返す。

wordpaternにはワイルドカードが使用できます。

$ FILEPATH=analysis/input/input-01.fastq.gz
$ echo ${FILEPATH#analysis/}
input/input-01.fastq.gz
$ echo ${FILEPATH#*/}  # ワイルドカードの使用
input/input-01.fastq.gz
$ echo ${FILEPATH##*/}  # 最長一致(最後の`/`までを削除)
input-01.fastq.gz
$ echo ${FILEPATH%.gz}
analysis/input/input-01.fastq
$ PREFIX=${FILEPATH%%.*}
$ echo $PREFIX
analysis/input/input-01
$ echo ${PREFIX/input/output}
analysis/output/input-01
$ echo ${PREFIX//input/output}
analysis/output/output-01
$ ACCESSION=DRA000583
$ echo ${ACCESSION:0:6}
DRA000

9.6.4.2. 変数が空のときの処理

ある変数$VARが未定義かを確かめたり、未定義かどうかである値(デフォルト値)を使いたいという場合があります。その場合は次のような表記が便利です。

デフォルト値を使うためのパラメーター展開

表記

返す値

元の変数に対する処理

${VAR:=value}

$VARが空のとき、valueを返す。

$VARが空のとき、valueが代入される。

${VAR:-value}

$VARが空のとき、valueを返す。

何もしない($VARは変更されない)

${VAR:?message}

$VARが空のとき、messageを表示する。

$VARが空の場合、シェルスクリプトを中断する。

${VAR:+value}

$VARが空でない場合、valueを返す。

何もしない

これらの処理では、コロン(:)を省略することができます。省略された場合は、変数の中身が空(空文字)の場合も、未定義とみなして処理します。

9.7. シェル関数

シェルスクリプト内で使いまわしたい処理は、関数を定義すると便利です。関数は次のように定義します。

name() {
    command
    ...
}

例におけるnameには任意の関数名を記述します。定義された関数は他のコマンドと同じように使用できます。

#!/bin/bash

sorthead() {
   path=$1  # 位置パラメーターが使える
   sort $1 | head
}

sorthead list.txt

特定の変数を関数内でのみ使うには、localを使って変数を宣言します。

#!/bin/bash

path1=some.txt
path2=another.txt

assign() {
   local path1  # 外側の`some.txt`が入っている`path1`とは別の変数とみなされる
   path1=$1
   path2=$1  # `path2`は上書きされる
}

assign data.txt
echo $path1 $path2  # `some.txt data.txt`

関数は標準入出力もサポートしています。

$!/bin/bash

recenterrors() {
    grep '^Error' | head  # `grep`にファイルが指定されていないが標準入力から読む
}

zcat step1.log.gz | recenterrors | sort  # 関数の出力を受けることもできる

returnを使うと、特定の終了ステータスで関数を終了します。returnがない場合は、関数内で最後に実行したコマンドの終了ステータスになります。

fileisgz() {
    if [ "${1##*.}" = gz ]; then
        return 0
    else
        echo $1
        return 1
    fi
}

if fileisgz "$1"; then
    ...

9.8. 別のシェルスクリプトを読み込む

特定の設定やプログラム・ファイルへのパスを格納した変数や、自作した関数を複数のシェルスクリプトで使いまわしたい場合は、設定を記述したシェルスクリプトを用意しておくと便利です。

シェル上であるスクリプトを実行するには、.(ドット)コマンドもしくはsourceコマンドを使用します。例えば次のようなconfig.shというファイルがある場合、

INPUTDIR=./data/input
OUTDIR=./data/outdir

.もしくはsourceコマンドで、conifg.shで定義した変数がシェル変数として参照できるようになります。

$ ls
config.sh
$ echo $INPUTDIR

$ . ./config.sh
$ echo $INPUTDIR
./data/input

注釈

.コマンドで指定するファイルは、相対・絶対パス(/を含む形 )で記述する必要があります。/が含まれない場合は、環境変数PATHに基づいてファイルを探します。

シェルスクリプトの実行はbashコマンドでも行えますが、bashコマンドの場合は別のシェルが起動してコマンドを実行するのに対して、.は現在のシェルでスクリプトを実行します。そのため、読み込んだスクリプトで定義された変数や関数がすべて現在の環境に引き継がれます。