2.7. bash#
bash は、複数のコマンドを組み合わせて処理を自動化するために使われるシェルです。基本的にはコマンドを順番に並べるだけで処理を書けるため、非常に手軽に利用できます。また、他のプログラミング言語と同様に、条件分岐や繰り返し処理を書くことも可能です。一方で、文法が独特なため、複雑な処理を書くのにはあまり向いていません。
bash を実行した瞬間、卒業要件も消えた。伝説を残さないように、ここからは慎重に読み進めてください。引き返すなら、今です。
2.7.1. 特殊変数#
bash を含むシェルには、環境変数(environment variable)やシェル変数(shell variable)と呼ばれる特殊な変数があらかじめ定義されています。これらの変数は、慣習的に大文字で表記されます。bash で新しい変数を作成する際に、これら環境変数と同じ名前を使ってしまうと、実行環境が破壊され、プログラムが正しく動作しなくなる可能性があります。そのため、変数名の衝突には十分注意する必要があります。
代表的な環境変数には、以下のようなものがあります。
変数 |
説明 |
|---|---|
|
現在使用しているシェルの実行ファイルへのパス。 |
|
コマンド検索パス。プログラム実行時に、この変数で指定されたディレクトリの中からコマンドが検索され、最初に見つかったものが実行されます。 |
|
ホームディレクトリのパス。 |
|
現在のユーザー名。 |
|
現在の作業ディレクトリのパス。 |
|
プロンプトの表示形式。 |
|
言語などのロケール設定。 |
実際に、これらの変数の中身を 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 コマンドは、他のプログラミング言語における print 関数に相当します。
echo ${x}
1
echo ${y}
1+x
echo ${z}
1+1
このように、変数 y と z では異なる結果が出力されます。変数 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="agtccctgg"
echo "${s#a*t}"
ccctgg
echo "${s##a*t}"
gg
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
すべてを暗記する必要はありません。必要になったときに調べて使いましょう。処理が複雑になった場合は Python や Perl を使う方が効率的な場合もあります。
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]}"
${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
cd unix4bi/data/fastq
ls *.fastq.gz
## 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. 分岐処理#
ある条件を見て、その条件を満たしたときにある処理を、満たさないときに別の処理を行うような処理を分岐処理といいます。例えば、フードコートで買い物をするときに、その場で食べるならば消費税 10%、そうでなければ消費税 8% になります。また、出勤するときに、晴れならば自転車、そうでなければ電車を使うといった判断も分岐処理の一例です。このように、現実の世界では人は多くの分岐処理を無意識に行っています。
2.7.4.1. if (文字列)#
bash の分岐処理は if 構文または case 構文を使います。if 構文は、「もし条件が成り立つならば処理を行う」という基本的な分岐処理を行う構文です。例えば、変数 mode に “eatin” という文字列が代入されたときに “10%” を出力し、”takeout” という文字列が代入されたときに “8%” を出力する場合は、次のようにします。
mode="takeout"
if [ "${mode}" = "takeout" ]
then
echo "8%"
fi
## 8%
if [ "${mode}" = "eatin" ]
then
echo "10%"
fi
上の例では mode の中身が “takeout” となっているため、最初の if 構文のブロックで “8%” が出力されます。一方、次のブロックでは [ "${mode}" = "eatin" ] が成り立たないため、そのブロックの中(then 〜 fi)は実行されません。
eatin/takeout のように yes/no しかない選択の場合は、if を 2 回使って判断する方法のほかに、次のように else を使う方法があります。次の例では、mode が “takeout” のときに “8%” を出力し、それ以外のときに “10%” を出力します。つまり、mode の中身が “takeout” 以外であれば、すべて “10%” が出力されます。
mode="takeout"
if [ "${mode}" = "takeout" ]
then
echo "8%"
else
echo "10%"
fi
## 8%
条件が複数ある場合は、if - then - elif - then - else を使います。次の例では、mode が “takeout” ならば “8%”、”eatin” ならば “10%” を出力し、それ以外の文字列であれば “unknown” を出力します。コード中の mode の中身を “eatin” や “eat-in” などに書き換えて、実行結果を比べてみてください。
mode="takeout"
if [ "${mode}" = "takeout" ]
then
echo "8%"
elif [ "${mode}" = "eatin" ]
then
echo "10%"
else
echo "unknown"
fi
## 8%
「等しくない」という判断を行うときは、= の代わりに != を使います。
mode="takeout"
if [ "${mode}" != "takeout" ]
then
echo "10%"
else
echo "8%"
fi
## 8%
2.7.4.2. if (数値)#
if 構文では数値の比較もできます。この場合、文字列比較で使用した = の代わりに、数値比較用の演算子を使用します。次のコードでは、変数 score が 60 以上ならば “pass”、80 以上ならば “good”、100 ならば “excellent” を出力するようにしています。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” と出力されてしまいます。余力があれば、100 を超える値に対して “cheating” を出力させるように修正してみてください。
文字列の比較では = および != を使ったのに対して、数値比較の場合は -eq、-le などの演算子を使います。よく使う比較演算子には次のようなものがあります。
演算子 |
使用例 |
意味 |
|---|---|---|
|
|
変数 |
|
|
変数 |
|
|
変数 |
|
|
変数 |
|
|
変数 |
|
|
変数 |
if 構文を使って 2 つの条件を同時に判断することもできます。AND 演算を行う場合は 2 つの条件を -a で結び、OR 演算を行う場合は -o で結びます。必要なときに調べて使ってみてください。
2.7.4.3. case#
条件が複数あるとき、if 構文で複数の elif を並べるとコード全体が見にくくなることがあります。そのような場合には case 構文を使うと、コードが読みやすくなります。次の例は、mode が “takeout” ならば “8%”、”eatin” ならば “10%” を出力し、それ以外の文字列であれば “unknown” を出力する処理を case 構文で書いた例です。
mode="takeout"
case "${mode}" in
"takeout" ) echo "8%" ;;
"eatin" ) echo "10%";;
* ) echo "unknown";;
esac
## 8%
ここでは文字列が完全一致するかどうかで条件判定を行っています。条件を正規表現で書くこともできますが、正規表現の使い方は難しいため、ここでは説明を省略します。
2.7.5. 繰り返し処理#
繰り返し処理を行う構文には for 構文と while 構文があります。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 に書き換えたり、あるディレクトリの中にあるすべての圧縮ファイルを解凍したりする場合に使えます。
次に、data/fastq ディレクトリにある gzip で圧縮されたデータ(拡張子 .gz を持つファイル)をすべて解凍してみます。gzip 圧縮形式のファイルを解凍するには gzip コマンドに -d オプションを付けて使います。
cd
cd unix4bi/data/fastq
fastq_files=`ls *.gz`
for fq in ${fastq_files[@]}
do
gzip -d "${fq}"
done
for 構文のためだけに使う場合は、fastq_files のような一時変数を作らなくても構いません。その場合、次のように書くことで ls の実行結果に直接 for 構文を適用できます。
for fq in `ls *.fastq`
do
echo "${fq}"
done
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 構文に渡すには、次のようにリダイレクトを使用します。
cd
cd unix4bi/data/field_data
while read line
do
echo "${line}"
done < iris.txt
## 1 5.1 3.5 1.4 0.2 setosa
## 2 4.9 3 1.4 0.2 setosa
## 3 4.7 3.2 1.3 0.2 setosa
## ...
## ...
## 149 6.2 3.4 5.4 2.3 virginica
## 150 5.9 3 5.1 1.8 virginica
リダイレクトの代わりにパイプを使うこともできます。パイプを使う場合は cat コマンドや grep コマンドの実行結果を while 構文に渡します。次の例は、iris.txt ファイルのうち “versicolor” を含む行だけを読み込み、出力する例です。
grep "versicolor" iris.txt | while read line
do
echo "${line}"
done
## 51 7 3.2 4.7 1.4 versicolor
## 52 6.4 3.2 4.5 1.5 versicolor
## 53 6.9 3.1 4.9 1.5 versicolor
## ...
## ...
## 99 5.1 2.5 3 1.1 versicolor
## 100 5.7 2.8 4.1 1.3 versicolor