シェルは、人がコンピュータに命令を出すときに、手軽にできる命令集の一つである。コマンド 1 つだけで実行されることもあるが、複数のコマンドを組み合わせて使うこともある。複数のコマンドを組み合わせて使うとき、複数のコマンドの組み合わせ順序や繰り返し回数などを指定することもできる。シェルを使用して、複数のコマンドを適切に制御すること複雑な処理を手軽に行える。
シェルには、sh、bash、ksh、tcsh、zsh、fish などの種類がある。とりわけよく使われているシェルは bash と zsh である。ほとんどの Linux では bash を採用している。また、macOS ではライセンスの関係上、2003 年ごろまでは tcsh を採用し、その後 2019 頃までは bash を採用し、現在は zsh を採用している。Linux で使われている bash と macOS で使われている zsh は似ているものが多く、初めてシェルを勉強するならば bash がおすすめである。
bash には、プログラミング言語でいう分岐制御(if
や case
)、繰り返し制御(for
や while
)などのフロー制御ができる。関数を作成する機能も備えてある。bash のフロー制御用の構文は非常にシンプルである。その反面、記述が雑になりやすく、思わないところでバグになっていたりする。例えば、あるファイルだけを削除するように制御しているにもかかわらず、そのディレクトリにあるファイル全体を削除してしまうようなことも起こりうる。そのため、手軽に済ませたい簡単な処理ならば bash を使い、少し複雑な処理、例えば 1 処理を書くのに 100 行以上にも及ぶ見込みがあるならば Python や Perl を使う方が効率的である。
このページでは、bash の基本的な使い方を紹介する。bash 実現できる機能は、ほとんどの場合、Python や Perl などのプログラミング言語でも実現可能である。bash を使うことで便利な場面もあるが、無理して bash を使用することはない。
シェルには bash や zsh などがある。Ubuntu ではデフォルトで bash が使えるため、特別な変更作業はいらない。そのまま次項に進んでください。macOS の場合は、zsh となっている可能性があるため、以下の操作にしたがって zsh を bash に切り替えてください。
まず、ターミナルを起動して次のコマンドを実行する。表示された結果が "/bin/bash" となっている場合は、その macOS では bash を使っていることになる。変更作業を行わずに、そのまま次項に進んでください。表示された結果が "/bin/zsh" となっている場合は、その macOS はデフォルトで zsh を使用していることになるので、これを bash に変換する必要がある。
echo $SHELL
## /bin/zsh
シェルを bash に変更するには、続けて以下のコマンドを実行する。このコマンドを実行すると、パスワードの入力が求められる。macOS にログインするときのパスワードを入力し、Return キーを押してください。なお、パスワードを入力しても、入力カーソル部分が移動しないようになっている。これはパスワードの文字数が見られないようにするための保護措置である。
xxxxxxxxxx
chsh -s /bin/bash
シェルを変更した後に、ターミナルを再起動する。その後、もう一度次のコマンドを実行すると、"/bin/bash" が表示されるようになる。
xxxxxxxxxx
echo $SHELL
## /bin/bash
なお、bash の練習を行った後に、bash を zsh に戻す忘れないようにしてください。シェルを zsh に変更する場合は次のコマンドを実行すればよい。
xxxxxxxxxx
chsh -s /bin/zsh
bash の変数がとりうる値は基本的に文字列だけである。例えば、変数に 1 を代入しても、"hello world" を代入しても、どちらも文字として認識される。変数に値を代入するには =
を使用する。=
の左右には空白を入れない。他のプログラミング言語では =
の左右に空白を入れても差し支えないが、bash の場合は不可能であることに注意。
まず、変数に数値を代入する例を示す。次の例は、変数 x
に 1 を代入し、変数 y
および変数 z
に、x
に 1 を足したつもりで得られた値を代入している。
xxxxxxxxxx
x=1
y=1+x
z=1+${x}
その変数に値の代入すると、その変数はプログラムが終了するまで(ターミナルを終了するまで)その値を保持し続ける。次は、echo
コマンドを使って、変数の中身を出力している例である。echo
コマンドは他のプログラミング言語の print
関数に相当するものである。
xxxxxxxxxx
echo ${x}
## 1
echo ${y}
## 1+x
echo ${z}
## 1+1
このように変数 y
および変数 z
は異なる結果が出力された。変数 y
は 1+x
であり、=
の右側に入力した文字列がそのまま代入されれたことになる。つまり、変数 x
が 1 行目で用意されても、2 行目で x を使うときは、その x は変数 x
のことではなく、普通の文字 x になる。
一方で、変数 z
を見ると、その値は 1+1 となっている。つまり、1+${x}
の中の ${x}
によって、変数 x
の中身が展開されて、1+1 という文字列になって変数 z
に代入されている。このように、bash の中で、変数であることを明示するためには ${x}
のように記述する必要がある。また、変数 z
が 2 ではなく、1+1 となっているのは、=
の右側があくまで文字列とみなされて、変数 z
に代入されたためである。
bash の変数に対して数値計算を行う例を示す。bash の変数の中身は基本的に文字列として認識される。文字列に対して、数値計算を行うためには、計算を行うためのコマンド expr
を使用する。expr
コマンドを使用すると、そのコマンドの後ろに書かれた文字が数字および数式として認識されて実行される。たたし、掛け算および割り算で使用する *
および /
などは、\*
および \/
のようにエスケープして使用する必要がある。(なお、/
はバックスラッシュだが、日本語版 Windows を使用している場合、円マークに見える。円マークとバックスラッシュを同じ文字として扱う。令和の令が、明朝体とゴシック体とで見え方が異なると同じ現象。)
xxxxxxxxxx
x=5
expr 2 \* ${x} + 1
## 11
expr ${x} \/ 2
## 2
expr ${x} \% 2
## 1
次に、expr
コマンドを利用した計算結果を他の変数に代入する例を示す。あるコマンドを実行して、その出力結果を変数に代入したい場合は、そのコマンドを ` で囲む必要がある。この ` は、シェルを書く上で非常によく使うものである。` は、` によって囲まれたコマンドを実行し、その実行結果を文字列としてまとめるという機能を果たす。そのため、下記の例では、計算結果を文字列としてまとめて、変数 z
に代入している。
xx=3
z=`expr 2 \* ${x} + 1`
echo ${z}
## 7
シェルを使った計算がやや手間がかかる。手軽に計算を行いたいだけであれば、Python あるいは R を起動して、計算することをおすすめする。
次に変数に文字列を代入する例を示す。文字列を代入するとき、空白を含む場合は "
で囲む必要がある。"
で囲まないと、空白の次にある単語がコマンドとして識別されて実行されるため、想定外のことが生じる。
xxxxxxxxxx
a=hello
b=hello world
## bash: world: command not found
c=hello ls
## Desktop Documents Downloads unix4bi
d="hello world"
b=hello world
を実行したとき、空白の後の単語 "world" がコマンドとして実行される。しかし、UNIX には world
というコマンドがないため、command not found
エラーが起こる。次に、c=hello ls
を実行したときに、空白の後の単語 "ls" がコマンド ls
として実行され、ディレクトリの一覧が出力される。このように、空白を含む文字列を変数に代入する場合は、想定がのことが起こる可能性がある。その対策として、文字列全体を "
で囲む必要がある。
次に文字列を代入した変数を echo
コマンドで表示させてみる。変数 b
および変数 c
を含む行は、world
および ls
コマンドとして実行されたため、変数の代入が行われていない。そのため、変数 b
および変数 c
の中は何もない状態となっている。
xxxxxxxxxx
echo ${a}
## hello
echo ${b}
##
echo ${c}
##
echo ${d}
## hello world
echo "${d}"
## hello world
変数 d
には空白付きの文字列が代入されている。echo
を利用して出力する場合は、そのまま echo ${d}
を実行して構わないが、echo
のあとにパイプを利用して後続処理を行ったときに想定外のエラーが起きる可能性がある。これは echo ${d}
と実行したときは、確かに画面上には "hello world" が出力されるが、"hello" と "world" の間に空白があるため、後続処理ではこれを "hello" と "world" の 2 つの要素として扱ってしまうからである。これを防ぐためには、「hello、空白、world」が 1 つの要素(文字列)であることを明示して扱うために、echo "${d}"
のように変数をダブルクォートで囲む必要がある。実際に、文字列の中に空白含むかどうかにかかわらず、ダブルクォートで囲む習慣を付けるとよい。この資料では、これ以降、変数の中身が文字列の場合は、すべてダブルクォートを付けて使うことにする。
以上で見たように、bash の変数に値を代入するときは、直感と異なる動作を行うことが多い。大規模処理を bash で書くときは、こまめに動作確認しておくとよい。
bash には強力な文字列操作の機能が備えられている。部分文字列の切り出し、検索や置換などが行える。部分文字列の切り出しは、:
を使う。変数名の後ろに :
をつけ、切り出し開始位置および切り出しの長さを指定する。なお、切り出しの長さを省略することができ、省略する場合は、文字列の最後まで切り出すことになる。
xxxxxxxxxx
s="0123456789"
echo "${s:2:5}"
## 23456
echo "${s:7}"
## 789
パターンマッチを行い、マッチ部分を削除するには #
や %
を利用する。#
は文字列の先頭から最短マッチを行うのに対して、%
は文字列の末尾から最短マッチを行う。また、##
は文字列の先頭から最長マッチで、%%
は文字列の後尾から最長マッチを行う。
文字列先頭からマッチ | 文字列末尾からマッチ | |
---|---|---|
最短マッチ | ${s#pattern} | ${s%pattern} |
最長マッチ | ${s##pattern} | ${s%%patter} |
最短マッチは、1 つの文字列の中で、パターンにマッチする部分が複数あったときに、一番短いマッチ部分を選ぶことである。逆に、最長マッチは、一番長いマッチ部分を選ぶことである。例えば、文字列 "agtccctgg" に対して、a で始まり t で終わるパターン "a*t" を、先頭から最短マッチする場合は "agt" がマッチされる。また、最長マッチする場合は、"agtccct" がマッチされる。
xxxxxxxxxx
s="agtccctgg"
echo "${s#a*t}"
## ccctgg
echo "${s##a*t}"
## gg
文字列末尾からマッチも同様に操作できる。
xxxxxxxxxx
s="agtccctgg"
echo "${s%g*g}"
## agtccct
echo "${s%%g*g}"
## a
文字列の後尾からのパターンマッチは、ファイルの拡張子を変更したり、規則性のあるサフィックスを持つファイルに対して何らかの処理を行いたいときに有効である。
xxxxxxxxxx
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
文字列の置換は /
を使う。置換の場合は、置換元パターンと置換先の文字列の両方の情報を必要とする。また、置換を行うときに、置換元のパターンに複数の部分がマッチしたとき、最初のマッチ部分だけを置換する場合は /
を使い、すべてのマッチ部分を置換する場合は //
を使う。
xxxxxxxxxx
s="caagccaagct"
echo "${s/aag/AAG}"
## cAAGccaagct
echo "${s//aag/AAG}"
## cAAGccAAGct
bash には多くの文字列操作機能が用意されているが、初めからこれらをすべて覚えるのは大変である。使いたいときに、その都度調べることとよい。ただし、処理が複雑になったとき、Python や Perl で書いた方が効率がいいということも忘れずに。
変数 s に対して、前方最短一致(文字列の先頭から最短マッチ)、前方最長一致、後方最短一致(文字列の末尾から最短マッチ)、後方最長一致で、パターン "a*g" を削除せよ。また、出力結果を見て、なぜその結果になったのかを説明せよ。
xxxxxxxxxx
s="accttgggaaact"
変数 s に含まれているすべての "tga" を "***" に置換せよ。
xxxxxxxxxx
s="aatgactattgatgc"
1 つの変数に複数の値を代入したいとき、それらの値を括弧で束ねて代入する。このとき、要素と要素の間を空白で区切る必要がある。例えば、"leaf"、"root"、"shoot" という 3 つの文字列を変数 samples
に代入する場合は次のようにする。
xxxxxxxxxx
samples=("leaf" "root" "shoot")
配列の中から要素を取り出すときは、角括弧 []
を使う。角括弧の中に要素の位置番号(インデックス)を指定する。なお、bash ではゼロから数え始めるため、配列の最初の要素を取り出すときは [0]
と指定する必要がある。
xxxxxxxxxx
echo "${samples[0]}"
## leaf
echo "${samples[1]}"
## root
echo "${samples[2]}"
## shood
echo "${samples[3]}"
##
上で見たように、${samples[3]}
に値が保存されていない。これは 3 個の要素しか持たない変数 samples
の中から4 番目(インデックスは 3)の要素を取得しようとしている。そのような値は存在しないために、何も表示されていない。
配列の内容全体を取得するためには [@]
を使用する。配列の変数名をそのまま使用すると、配列の最初の要素が出力されるが、変数名に [@]
を付けると、配列の全要素を取得できるようになる。
xxxxxxxxxx
echo ${samples}
## leaf
echo ${samples[@]}
## leaf root shoot
配列の要素数を取得するには、変数名の前に #
を付ける。
xxxxxxxxxx
echo ${#samples[@]}
## 3
既存の配列に要素を追加するには、+=
で新しい要素を代入する。
xxxxxxxxxx
samples+=(flower seed)
echo ${samples[@]}
## leaf root shoot flower seed
ls
コマンドを適切に使うことで、あるディレクトリ内に存在する特定の拡張子を持ったファイルの名前を配列に保存できるようになる。例えば、次は、data/fastq ディレクトリの中にある FASTQ ファイルを変数 fq
に保存する例を示している。
xxxxxxxxxx
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
data/fasta ファイルの中にある FASTA ファイルのうち、ファイル名に "_1" を含むものだけを変数(配列) に保存せよ。
xxxxxxxxxx
cd
cd unix4bi/data/fastq
ある条件を見て、その条件を満たしたときにある処理を、満たさないときに他の処理を行うような処理を分岐処理という。例えば、フードコートで買い物をするときに、その場で食べるならば消費税 10%、そうでなければ消費税 8% になる。また、出勤するときに、晴れならば自転車、そうでなければ電車を使う。このように、現実の世界では、人は多くの分岐処理を無意識に行っている。
bash の分岐処理は if
または case
構文を使う。if
構文は、「もし条件が成り立つならば、処理を行う」という基本的な分岐処理を行う構文できある。例えば、変数 mode
に "eatin" の文字列が代入されたときに "10%" を出力し、"takeout" の文字列が代入されたときに "8%" を出力する場合は、次のようにする。
xxxxxxxxxx
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
の中身が "take out" でも、"eat-in" でも 10% が出力される。
xxxxxxxxxx
mode="takeout"
if [ "${mode}" = "takeout" ]
then
echo "8%"
else
echo "10%"
fi
## 8%
条件が複数ある場合は、if-then-elif-then-else
を使う。次の例では、mode
が "takeout" ならば 10%、"eatin" ならば 8% を出力し、それ以外の文字列であれば "unknown" を出力している。コードの mode
の中身を "eatin" や "eat-in" などに書き換えて、実行結果を比べてみてください。
xxxxxxxxxx
mode="takeout"
if [ "${mode}" = "takeout" ]
then
echo "8%"
elif [ "${mode}" = "eatin" ]
then
echo "10%"
else
echo "unknown"
fi
## 8%
「等しくない」の判断を行うときは =
を使う代わりに !=
を使う。
xxxxxxxxxx
mode="takeout"
if [ "${mode}" != "takeout" ]
then
echo "10%"
else
echo "8%"
fi
## 8%
if
構文で数値の比較もできる。この場合 =
を利用する代わりに、数値比較用の演算子を使用する。次のコードでは、変数 score
が 60 以上ならば "pass" を出力し、80 以上ならば "good" を出力し、100 ならば "excellent" を出力するようにしている。score
が 60 未満の場合は "failure" としている。
xxxxxxxxxx
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
などの演算子を使う。よく使う比較演算子には以下のようなものがある。
演算子 | 使用例 | 意味 |
---|---|---|
-eq | [ ${x} -eq ${y} ] | 変数 x が変数 y と等しければ真である。 |
-ne | [ ${x} -ne ${y} ] | 変数 x が変数 y と等しくなければ真である。 |
-lt | [ ${x} -lt ${y} ] | 変数 x が変数 y よりも小さければ真である。 |
-le | [ ${x} -le ${y} ] | 変数 x が変数 y 以下ならば真である。 |
-gt | [ ${x} -gt ${y} ] | 変数 x が変数 y よりも大きければ真である。 |
-ge | [ ${x} -ge ${y} ] | 変数 x が変数 y 以上ならば真である。 |
if
構文を使って 2 つの条件を同時に判断することができる。AND 演算する場合は、2 つの条件を -a
で結ぶ。OR 演算する場合は、2 つの条件を -o
で結ぶ。必要なとき、調べて使ってみてください。
条件が複数あるとき、if
構文で複数の elif
をつければよいが、コード全体が見にくくなることが多い。このとき、case
構文を使用すると、コードが見やすくなったりする。次の例は mode
が "takeout" ならば 10%、"eatin" ならば 8% を出力し、それ以外の文字列であれば "unknown" を出力しているコードを、case
構文で書いた例である。
xxxxxxxxxx
mode="takeout"
case "${mode}" in
"takeout" ) echo "8%" ;;
"eatin" ) echo "10%";;
* ) echo "unknown";;
esac
## 8%
上では、文字列が完全一致するかどうかで条件判定を行っている。条件を正規表現を使って書くこともできるが、正規表現の使い方は難しいので、ここでは説明を割愛する。
繰り返し処理を行う構文には for
構文と while
構文がある。for
構文は、あらかじめ繰り返し対象(繰り返し回数)が決まっているものに対して処理を行う。while
構文は、はじめに繰り返し回数を決めないで、ある条件を判定し、その条件が真ならば繰り返す。両者どちらも繰り返し処理用の構文で、for
構文で書いた処理を while
構文でも書けて、逆も可能である。そのため、はじめは、for
あるいは while
のどちらか理解しやすい方の使い方だけを理解すればよい。
for
構文は、繰り返し対象を指定する必要がある。例えば、配列に含まれている要素すべてに対して、同じ処理を繰り返すときに使用する。まず、配列の各要素を出力してみる例を示す。
xxxxxxxxxx
samples=(leaf root shoot)
for sample in ${samples[@]}
do
echo "${sample}"
done
## leaf
## root
## shoot
上の例では echo
コマンドだけを使いましたが、実際に for
構文を使用するときは、do-done
の間に様々なコマンドを書いて使用する。例えば、leaf.fastq、root.fastq、shoot.fastq の拡張子を leaf.fq、root.fq、shoot.fq に書き換えたり、あるディレクトリの中にあるすべての圧縮ファイルを解凍したりするときに使う。
data/fastq ディレクトリにある gzip で圧縮されたデータ(拡張子 .gz を持つファイル)をすべて解凍してみる。なお、gzip 圧縮形式のファイルを解凍するには、 gzip
コマンドに -d
オプションを付けて使う。
xxxxxxxxxx
cd
cd unix4bi/data/fastq
fastq_files=`ls *.gz`
for fq in ${fastq_files[@]}
do
gzip -d "${fq}"
done
なお、for
構文を利用するためだけしか使わないのであれば、fastq_files
のような一時変数を使わなくても大丈夫。この場合、例えば、次のように書くことで ls
の実行結果に対して、直接 for
構文を適用できるようになる。
xxxxxxxxxx
for fq in `ls *.fastq`
do
echo "${fq}"
done
while
構文では、「条件を判断し、その条件が真ならば処理を行う」ということを繰り返す構文となっている。条件は、if
構文のときに書いたものと同様な書き方で書く。次の例は、i
が 5 未満ならば、i
を出力するという簡単な例を示している。
xxxxxxxxxx
i=0
while [ ${i} -lt 5 ]
do
echo ${i}
i=`expr $i + 1`
done
## 0
## 1
## 2
## 3
## 4
while
構文は、例えば乱数を利用した処理で、乱数の値によって実行に失敗したりするようなプログラムを、成功するまで続けてトライしたい場合に使う。また、不安定なネットワークで、正しくダウンロードされるまで、ダウンロードを挑戦し続けるといった場合に使うこともある。
次の例では、確率 0.8 で裏になるコインを投げ続けて、表が出るまでに投げた回数を計算している。以下のコードは乱数を使っているため、実行する度に異なる値が出力される。このコードを、何回か実行して、出力結果を見比べてみてください。
xxxxxxxxxx
n=0
while [ `expr $RANDOM \% 10` -ge 2 ]
do
n=`expr ${n} + 1`
done
echo ${n}
## 7
while
構文を使って、ファイルの中身を読み込むこともできる。ファイルの内容を while
構文に流すには、次のようにリダイレクトを使用する。
xxxxxxxxxx
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" を含む行だけを読み込んで、出力している例を表している。
xxxxxxxxxx
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
fastq ディレクトリを fastq2 の名前でコピーせよ。続けて fastq2 の中にある拡張子が .fastq のファイルを、すべて .fq に書き換えよ。
xxxxxxxxxx
pdb ディレクトリ中にあるすべての FASTA ファイルの行数を計算して、ファイル名と行数の情報を fasta_len.txt ファイルに保存せよ。
xxxxxxxxxx
yield_data ディレクトリの中に複数年度の収穫量データファイルが含まれている。これらのファイルの中から、"Akita" を含む行だけを抽出し、akita.tsv に保存せよ。
xxxxxxxxxx
yield_data ディレクトリの中に複数年度の収穫量データファイルが含まれている。これらのファイルの中から、"Akita" かつ "akitakomachi" を含む行だけを抽出し、akita_akitakomachi.tsv に保存せよ。
xxxxxxxxxx