2.7. bash#

bash は、複数のコマンドを組み合わせて処理を自動化するために使われるシェルです。基本的にはコマンドを順番に並べるだけで処理を書けるため、非常に手軽に利用できます。また、他のプログラミング言語と同様に、条件分岐や繰り返し処理を書くことも可能です。一方で、文法が独特なため、複雑な処理を書くのにはあまり向いていません。

bash を実行した瞬間、卒業要件も消えた。伝説を残さないように、ここからは慎重に読み進めてください。引き返すなら、今です。

2.7.1. 特殊変数#

bash を含むシェルには、環境変数environment variable)やシェル変数shell variable)と呼ばれる特殊な変数があらかじめ定義されています。これらの変数は、慣習的に大文字で表記されます。bash で新しい変数を作成する際に、これら環境変数と同じ名前を使ってしまうと、実行環境が破壊され、プログラムが正しく動作しなくなる可能性があります。そのため、変数名の衝突には十分注意する必要があります。

代表的な環境変数には、以下のようなものがあります。

変数

説明

SHELL

現在使用しているシェルの実行ファイルへのパス。

PATH

コマンド検索パス。プログラム実行時に、この変数で指定されたディレクトリの中からコマンドが検索され、最初に見つかったものが実行されます。

HOME

ホームディレクトリのパス。

USER

現在のユーザー名。

PWD

現在の作業ディレクトリのパス。

PS1

プロンプトの表示形式。

LANG

言語などのロケール設定。

実際に、これらの変数の中身を echo コマンドで表示してみましょう。変数を表示させるためには、変数を ${} で囲みます。

echo ${SHELL}
/bin/bash
echo ${HOME}
/home/rispy
echo ${PATH}
/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/snap/bin
cd ~/Desktop
echo ${PWD}
/home/rispy/Desktop

ここで紹介した以外にも、多くの環境変数が定義されています。現在設定されている環境変数を一覧表示するには、printenv コマンドを使用します。

printenv

2.7.2. 変数#

2.7.2.1. 数値#

bash の変数が取り得る値は基本的に文字列です。例えば、変数に 1 を代入しても、hello world を代入しても、どちらも文字列として認識されます。変数に値を代入するには = を使用します。= の左右には空白を入れないでください。他のプログラミング言語では = の左右に空白を入れても問題ありませんが、bash ではエラーになります。

まず、変数に数値を代入する例を示します。次の例では、変数 x に 1 を代入し、変数 y および変数 z に、x に 1 を足したつもりの値を代入しています。

x=1
y=1+x
z=1+${x}

変数に値を代入すると、プログラム(ターミナル)が終了するまでその値が保持されます。次は echo コマンドを使って変数の中身を出力する例です。

echo ${x}
1
echo ${y}
1+x
echo ${z}
1+1

このように、変数 yz では異なる結果が出力されます。変数 y には 1+x という文字列がそのまま代入されています。つまり、2 行目で使用している x は変数 x ではなく、単なる文字 x として扱われています。一方、変数 z では ${x} によって変数 x の中身が展開され、1+1 という文字列になって代入されています。このように、bash で変数であることを明示するには ${x} のように記述します。なお、変数 z が 2 ではなく 1+1 になっているのは、= の右側が計算されず、あくまで文字列として扱われているためです。

2.7.2.2. 数値計算#

bash では、変数の値は基本的に文字列として扱われるため、そのままでは数値計算ができません。数値計算を行うには、expr コマンドを使用します。expr の後ろに書かれた式は、数値や数式として評価されます。足し算と引き算は、そのまま +- を使って記述できますが、掛け算、割り算、剰余算を行う場合は、\*\/\% のように、演算子の前にバックスラッシュ(\)を付ける必要があります。これは、*/% などがコマンドライン上で特別な意味を持つため、そのまま使用することができないためです。

なお、バックスラッシュ(\)は、日本語環境では円マーク(¥)に見える場合がありますが、内部的には同じ文字として扱われます。そのため、表示が異なっていても動作上の違いはありません。

x=5
expr 2 \* ${x} + 1
11
expr ${x} \/ 2
2
expr ${x} \% 2
1

次に、expr の計算結果を他の変数に代入する例を示します。コマンドの実行結果を変数に代入するには、実行文全体をバッククォート ` で囲みます。これにより、コマンドの出力結果が文字列として変数に代入されます。

x=3
z=`expr 2 \* ${x} + 1`

echo ${z}
7

bash を使った計算はやや手間がかかります。数値計算を行う場合は、Python や R を起動して計算した方が効率的です。

2.7.2.3. 文字列#

変数に文字列を代入するには、数値の場合と同様に = を使用します。ただし、文字列に空白が含まれている場合は、ダブルクォート(")で囲む必要があります。

a=hello

b=hello world
Command 'world' not found, but can be installed with:
sudo snap install world

このように、空白を含まない文字列であれば、そのまま変数 a に代入されますが、空白を含む文字列の場合は、空白以降の部分が別のコマンドとして解釈されてしまい、代入に失敗します。

もう 1 つ例を示します。この場合は、空白の後にある ls がコマンドとして実行され、ディレクトリ内のファイルやディレクトリの一覧が表示されます。このとき、エラーは発生しませんが、変数 c には何も代入されません。

c=hello ls
Desktop  Documents  Downloads
echo ${c}

このように、想定外の動作が発生しているにもかかわらず、エラーが出ない場合があります。そのため、文字列を扱うときは特に注意が必要で、必ずダブルクォートで囲む習慣を付けることをおすすめします。

d="hello world"

変数の中身を扱うときにも注意が必要です。単純に画面へ出力するだけであれば、次のいずれの書き方でも問題ありません。

echo ${d}
hello world
echo "${d}"
hello world

しかし、出力ではなく、rm ${d} のように他のコマンドの引数として変数を使う場合には注意が必要です。このとき、ダブルクォートで囲まないと、空白で区切られた複数の引数として解釈されてしまいます。たとえば、次のコマンドでは、ダブルクォートで囲まれている場合は "hello world" という 1 つのファイルあるいはディレクトリを削除しようとしますが、ダブルクォートで囲まれていない場合は "hello""world" という 2 つのファイルあるいはディレクトリを削除しようとします。

rm ${d}
rm: cannot remove 'hello': No such file or directory
rm: cannot remove 'world': No such file or directory

幸いにも、上記の例では削除対象のファイルあるいはディレクトリが存在しないためエラーになっていますが、もし "hello""world" という名前のファイルあるいはディレクトリが存在していた場合は、意図せず両方とも削除されてしまいます。これは非常に危険な動作です。文字列を扱うときは、代入時だけでなく、その変数を利用する際にも常にダブルクォートで囲む習慣を付けると安全です。

bash では変数の値は常に文字列として扱われるため、中身が数値か文字列かを深く考えず、基本的にはダブルクォートで囲んでおきましょう。ヘルメットと同じで、見た目はさておき、安全第一です。

2.7.2.4. 文字列置換#

bash には簡単な文字列操作機能がいくつかようされています。部分文字列の切り出しや検索、置換などが可能です。部分文字列の切り出しには : を使用します。

s="0123456789"
echo "${s:2:5}"
23456
echo "${s:7}"
789

パターンマッチによる削除には #% を使用します。# は先頭から、% は末尾からパターンにマッチした部分を削除します。さらに、##%% を使うと最長マッチになります。

文字列先頭からマッチ

文字列末尾からマッチ

最短マッチ削除

${s#pattern}

${s%pattern}

最長マッチ削除

${s##pattern}

${s%%pattern}

たとえば、次の例では、${s#a*t} によって、文字列 s の中で、a から始まり、任意の文字列が 0 個以上続き(*)、t で終わる部分文字列を先頭から探します。 この場合は、agt とマッチします。そのため、最初の agt が削除され、ccctgg が出力されます。

s="agtccctgg"

echo "${s#a*t}"
ccctgg

一方、${s##a*t} では、最長マッチを行うので、先頭から最後に現れる a から t までの部分文字列が削除されます。そのため、agtccct が削除され、gg が出力されます。

echo "${s##a*t}"
gg

% および %% も基本的に同様に動作しますが、こちらは文字列の末尾からマッチングを行います。この場合は、g*g をマッチするので、g から始まり、任意の文字列が 0 個以上続き、g で終わる部分文字列が削除されます。

s="agtccctgg"

echo "${s%g*g}"
agtccct
echo "${s%%g*g}"
a

これらのマッチ削除機能を利用すると、拡張子の変更などに便利です。

img="input.jpeg"

echo "${img%.jpeg}.jpg"
input.jpg
libname="leaf_1.fq"
fq1="${libname%_1.fq}_1.fq"
fq2="${libname%_1.fq}_2.fq"

echo "${fq1}"
leaf_1.fq
echo "${fq2}"
leaf_2.fq

文字列置換には / を使います。最初の一致だけを置換する場合は /、すべて置換する場合は // を使用します。

s="caagccaagct"

echo "${s/aag/AAG}"
cAAGccaagct
echo "${s//aag/AAG}"
cAAGccAAGct

文字列検索や置換に利用するパターンは、基本的にワイルドカード*?)を使用します。正規表現は使えません。高度な文字列操作が必要な場合は、PythonPerl を使う方が効率的です。

2.7.3. 配列#

配列を利用すると、複数の値を 1 つの変数にまとめて保存できます。配列を作成するには、変数名の前に () を付けて、要素を空白で区切って並べます。

samples=("leaf" "root" "shoot")

要素を取り出すにはインデックスを指定します。bash では 0 から数えます。

echo "${samples[0]}"
leaf
echo "${samples[1]}"
root
echo "${samples[2]}"
shoot

また、次のように値が定義されていないインデックスを指定すると、何も表示されません。

echo "${samples[3]}"

配列全体を取得するには [@] を使います。

echo ${samples}
leaf
echo ${samples[@]}
leaf root shoot

要素数は # を使います。

echo ${#samples[@]}
3

要素を追加するには += を使います。

samples+=(flower seed)

echo ${samples[@]}
leaf root shoot flower seed

ls の結果を配列に保存することもできます。

cd ~/Desktop/takarabako/fastq
ls
flower_1.fastq.gz  leaf_1.fastq.gz  root_1.fastq.gz  seed_1.fastq.gz  shoot_1.fastq.gz
flower_2.fastq.gz  leaf_2.fastq.gz  root_2.fastq.gz  seed_2.fastq.gz  shoot_2.fastq.gz
fq=(`ls *.fastq.gz`)

echo ${#fq[@]}
10
echo ${fq[@]}
flower_1.fastq.gz flower_2.fastq.gz leaf_1.fastq.gz leaf_2.fastq.gz root_1.fastq.gz root_2.fastq.gz seed_1.fastq.gz seed_2.fastq.gz shoot_1.fastq.gz shoot_2.fastq.gz

2.7.4. 分岐処理#

ある条件に応じて処理を切り替えることを分岐処理といいます。コインを投げて表ならコカコーラ、裏ならペプシコーラ、というのが典型例です。ちなみに、社会に出ると、条件は常に曖昧で、結果はだいたい決まっています。分岐しているように見えて、どちらを選んでも修正が入ります。

2.7.4.1. if (文字列)#

bash の分岐処理は、if 文または case 文を使います。if 文は、「もし条件が成り立つなら処理を行う」という、最も基本的な分岐処理を行う制御文です。例えば、変数 coin"head" という文字列が代入されたときに "Coke" を出力し、"tail" という文字列が代入されたときに "Pepsi" を出力する場合は、次のように書けます。

coin="head"

if [ "${coin}" = "head" ]
then
	echo "Coke"
fi

if [ "${coin}" = "tail" ]
then
	echo "Pepsi"
fi
Coke

この例では、coin の中身が "head" であるため、最初の if ブロックが実行されて "Coke" が出力されます。一方、次の if ブロックでは条件が成り立たないため、処理は実行されません。

head/tail のように選択肢が 2 つしかない場合は、if を 2 回書く代わりに、else を使うこともできます。次の例では、coin"head" のときに "Coke" を、それ以外の場合はすべて "Pepsi" を出力します。

coin="head"

if [ "${coin}" = "head" ]
then
	echo "Coke"
else
	echo "Pepsi"
fi
Coke

条件が 3 つ以上ある場合は、if - elif - else を使って分岐します。

mood="happy"

if [ "${mood}" = "happy" ]
then
	echo "Coke (PET) x 2"
elif [ "${mood}" = "excited" ]
then
	echo "Coke (PET) x 5"
elif [ "${mood}" = "normal" ]
then
	echo "Coke (can)"
elif [ "${mood}" = "sad" ]
then
	echo "Coke (bottle)"
else
	echo "Coke"
fi
Coke (PET) x 2

「等しくない」ことを判定したい場合は、= の代わりに != を使います。

mood="sad"

if [ "${mood}" != "sad" ]
then
	echo "Coke (PET)"
else
	echo "Coke (bottle)"
fi
Coke (bottle)

ここまでコカコーラを推しても、研究費の協賛は来ません。マーケティング的には満点、財務的には却下でしょうか。

2.7.4.2. if(数値)#

if 文では数値の比較を行うこともできます。その際には、数値専用の比較演算子を使用します。次の例では、変数 score の値に応じて評価結果を出力します。

  • score が 100 のとき:"excellent"

  • score が 80 以上のとき:"good"

  • score が 60 以上のとき:"pass"

  • score が 60 未満のとき:"failure"

score=86

if [ ${score} -eq 100 ]
then
	echo "excellent"
elif [ ${score} -ge 80 ]
then
	echo "good"
elif [ ${score} -ge 60 ]
then
	echo "pass"
else
	echo "failure"
fi
good

なお、このプログラムでは、score が 100 より大きい値の場合でも "good" と表示されてしまいます。余力があれば、score が 100 を超えた場合に "cheating" を出力するように修正してみてください。

数値の比較には、次のような演算子がよく使われます。

演算子

使用例

意味

-eq

[ ${x} -eq ${y} ]

xy が等しければ真

-ne

[ ${x} -ne ${y} ]

xy が等しくなければ真

-lt

[ ${x} -lt ${y} ]

xy より小さければ真

-le

[ ${x} -le ${y} ]

xy 以下なら真

-gt

[ ${x} -gt ${y} ]

xy より大きければ真

-ge

[ ${x} -ge ${y} ]

xy 以上なら真

if 文では、複数の条件を一つの if 文で判定することができます。必要に応じて、-a および -o の使い方を調べながら活用してみてください。

2.7.4.3. case#

条件が複数あるとき、if 文で複数の elif を並べるとコード全体が見にくくなることがあります。そのような場合には case 文を使うと、コードが読みやすくなります。なお、case 文は文字列の比較にしか使えません。数値の比較には使えないため注意してください。

mood="happy"

case "${mood}" in
	happy)
		echo "Coke (PET) x 2"
		;;
	excited)
		echo "Coke (PET) x 5"
		;;
	normal)
		echo "Coke (can)"
		;;
	sad)
		echo "Coke (bottle)"
		;;
	*)
		echo "Coke"
		;;
esac
Coke (PET) x 2

ここでは文字列が完全一致するかどうかで条件判定を行っています。条件を正規表現で書くこともできますが、正規表現の使い方は難しいため、ここでは説明を省略します。

2.7.5. 繰り返し処理#

繰り返し処理には for 文と while 文があります。for 文は、あらかじめ繰り返し対象や回数が決まっている場合に使います。一方、while 文は、最初に回数を決めず、ある条件を判定し、その条件が真である間処理を繰り返します。どちらも繰り返し処理用の文であり、for 文で書ける処理は while 文でも書けますし、その逆も可能です。

2.7.5.1. for#

for 文では繰り返し対象を指定します。例えば、配列に含まれているすべての要素に対して同じ処理を繰り返すときに使用します。まず、配列の各要素を出力する例を示します。

samples=(leaf root shoot)

for sample in ${samples[@]}
do
    echo "${sample}"
done
leaf
root
shoot

この例では echo コマンドのみを使用していますが、実際には do から done の間にさまざまなコマンドを書いて利用します。例えば、leaf.fastq、root.fastq、shoot.fastq の拡張子をそれぞれ leaf.fq、root.fq、shoot.fq に書き換えたり、あるディレクトリ内に存在するすべての圧縮ファイルを解凍したりする場合などに使用できます。

次に、fastq ディレクトリにある名前が fastq.gz で終わるファイルをすべて fq.gz に書き換える例を示します。

cd ~/Desktop/takarabako/fastq

fastq_files=`ls *.fastq.gz`

for fq in ${fastq_files[@]}
do
    echo ${fq}
    mv ${fq} ${fq%.fastq.gz}.fq.gz
done

for 文のためだけに使う場合は、fastq_files のような一時的な変数を作成しなくても構いません。その場合は、次のように書くことで、ls の実行結果に直接 for 文を適用できます。

for fq in `ls *.fq.gz`
do
    echo "${fq}"
done
flower_1.fq.gz
flower_2.fq.gz
leaf_1.fq.gz
leaf_2.fq.gz
root_1.fq.gz
root_2.fq.gz
seed_1.fq.gz
seed_2.fq.gz
shoot_1.fq.gz
shoot_2.fq.gz

2.7.5.2. while#

while 文は、「条件を判断し、その条件が真である間処理を行う」ことを繰り返す文です。条件の書き方は if 文と同様です。次の例では、変数 i が 5 未満である間、i の値を出力します。

i=0

while [ ${i} -lt 5 ]
do
	echo ${i}
	i=`expr $i + 1`
done
0
1
2
3
4

while 文は、例えば乱数を利用した処理で失敗する可能性があるプログラムを、成功するまで繰り返し試行したい場合などに使えます。また、不安定なネットワーク環境において、正しくダウンロードできるまで再試行したい場合にも便利です。

次の例では、確率 0.8 で裏が出るコインを投げ続け、表が出るまでに投げた回数を計算しています。このコードでは乱数を使用しているため、実行するたびに異なる値が出力されます。何回か実行して結果を比べてみてください。

n=0

while [ `expr $RANDOM \% 10` -ge 2 ]
do
  n=`expr ${n} + 1`
done

echo ${n}
7

while 文を使ってファイルの中身を読み込むこともできます。ファイルの内容を while 文に渡すには、次のようにリダイレクトを使用します。次の例は、acorns.txt ファイルの各行を読み込み、その内容を出力する例です。

cd ~/Desktop/takarabako

while read line
do
	echo "${line}"
done < acorns.txt
tree	weight	height	diameter
kunugi	5.55	2.27	1.89
kunugi	4.62	1.98	1.84
 ...
 ...
matebashii	3.13	2.76	1.40
matebashii	2.20	2.54	1.21

リダイレクトの代わりにパイプを使用することもできます。パイプを使う場合は、cat コマンドや grep コマンドの実行結果を while 文に渡します。次の例は、acorns.txt ファイルのうち "kunugi" を含む行だけを読み込み、出力する例です。

grep "kunugi" acorns.txt | while read line
do
	echo "${line}"
done
kunugi	5.55	2.27	1.89
kunugi	4.62	1.98	1.84
kunugi	5.05	2.08	1.90
kunugi	5.44	2.18	1.91
kunugi	5.60	2.20	1.93
kunugi	4.83	2.19	1.85
kunugi	5.55	2.16	2.00
kunugi	5.13	2.20	1.87
kunugi	6.03	2.26	2.01
kunugi	7.41	2.35	2.12