機械学習屋は通常のシスアドほどはシェルに詳しい必要はありません。

ですがコンテナとインスタンスの設定は凄くしょっちゅうやる事になるので、 簡単なトラブルシューティングは自分で出来る方が良いです。 また、大きな学習データを扱うのにシェルを使うケースもちょこちょこ出てきます。

ここではそうした事を踏まえて、機械学習で必要な程度のシェルの入門をしたいと思います。

良くある設定として、以下が理解出来るくらいを目指します。(これは私のインスタンスの設定用スクリプトです)

幾つかの例は実際の自分のスクリプトから持ってきた物ですが、説明してないコマンドなども多く出てきます。 コマンドが何をしているのかが分からなくても説明している項目は理解出来るはずです。

実務でも全てのコマンドを知っているという事は稀です。例えばDockerの公式サイトにあったインストール手順のスクリプトをそのままコピペする時など、知らないコマンドを実行するケースは良くあると思います。 そういう時に、何かトラブルが起きたら、まず分かる部分にだけ着目する、というのはトラブルシュートの第一歩となります。

いろいろなクオートと変数の展開

シェルの文字列のクオートには、シングルクオートとダブルクオートとバッククオートの三種類があり、それぞれ意味が違います。

echoに見る、クオートが無い場合とある場合の違い

echoで、「Hello World」と、間に空白を二つ入れたいとします。 以下のようにすると、

$ echo Hello  World
Hello World

空白が一つになってしまいます。

echoコマンドからは、echo Hello Worldというのは、

  • 第一引数としてHello
  • 第二引数としてWorld

が渡されている、と見えて、間の空白というのはシェルが処理する事になります。

シェルは空白で区切って最初の文字列をコマンド、それ以降を引数として配列に詰めて渡します。 この時、間の空白が幾つかとかはシェルは気にしません。そしてechoまで辿り着いた時には、間の空白が二つあったという情報は失われます。

では空白を二つ挟んだ結果を出したい場合はどうしたらいいでしょう? ダブルクオートで囲むのが普通の解決策です。

$ echo "Hello  World"

こうすると、echoからは第一引数に「Hello World」が渡された、と見えます。 引数は一つと解釈されます。これはechoというよりはシェルの振る舞いなのでそのほかのコマンドでも同じ事が言えます。(あとでシェルスクリプトをやった時にさらに確認します)

シェルの変数

シェルには変数という物があります。 変数はイコールで代入する事で作られます。 例えば以下でHOGEという変数が出来ます。

$ HOGE=abc

これでHOGEという変数にabcという文字列が入ります。

変数は参照する時はドル記号をつけます。例えばechoを使うと、以下のようになります。

$ echo $HOGE
abc

シェルは、$HOGEとあったら変数の中身に置き換えてechoに渡します。 echoコマンドからはHOGEという変数だったという事実は分からず、abcという文字列が最初から渡されたのと区別出来ません。

シングルクオートとダブルクオートと変数展開

文字列をダブルクオートで囲むと、中に変数があった場合、それが展開されます。

$ echo "ika$HOGE"
ikaabc

ですが、以下のようにすると、何も表示されません。

$ echo "$HOGEika"

これは、$HOGEという変数のあとにikaがあるのではなく、$HOGEikaという別の変数があると勘違いする為です。

HOGEで切りたい場合は中括弧でくくります。

$ echo "${HOGE}ika"
abcika

シェルでは、文字列をシングルクオートでくくる事も出来ます。 シングルクオートの場合、中の変数は展開されません。

$ echo '${HOGE}ika'
${HOGE}ika

バッククオートと$()

シェルスクリプトにはさらに、バッククオート、つまり\`でくくる、というのがあります。 バッククオートはプログラム以外ではあまり使わないのでキーボード上で探さないといけない人もいるかもしれませんね。(なお私の手元のキーボードではシフトを押しながらアットマークのキーでした)

バッククオートで囲まれた部分は、「その中をシェルスクリプトとして、子プロセスで実行し、そこで得られた標準出力で置き換える」という機能になります。

言葉にするとややこしいので例をみましょう。

まず一番簡単なのがechoを使う例です。

$ echo hoge_`echo ika`_fuga
hoge_ika_fuga

バッククオートの中はecho ikaなので、これを実行した結果の標準出力、つまりikaで展開されます。

もう少し他の例も見て見ましょう。例えば以下みたいなスクリプトを実行してみましょう。

~$ echo hoge_`cd /etc; pwd`_fuga
hoge_/etc_fuga
~$

バッククオートの中はcd /etc; pwdなので標準出力には/etcでpwdした結果、つまり/etcが得られるでしょう。

ここで着目して欲しいのは、呼び出し元のシェルでは作業ディレクトリが/etcになってない、という所です。 バッククオートの中は子プロセスとして実行されるので、呼び出し元のシェルには影響がありません。 だからこの中でcdしても問題が無い。

なお、バッククオートの他に$(シェルのコマンド)という記法もあって、効果は一緒です。 つまり上記のechoはecho hoge_$(cd /etc; pwd)_fugaと書いても良い。 複数行に渡る場合はこちらが多く使われるようです。

機能だけ説明してもいまいちどう使われるか分からないかもしれないので、以下に幾つか実例を載せてみます。 コマンド自体は分からない物も多いと思いますが展開の使われ方の雰囲気だけ感じ取ってください。

実際の例、dockerのURL生成

dockerのサイトからインストール方法を調べると、以下のようなスクリプトを実行せよ、と書かれています。(GCP Setup, debian, non gpuにも入っています)

curl -fsSL https://download.docker.com/linux/$(. /etc/os-release; echo "$ID")/gpg | sudo apt-key add -

curlやapt-keyなどはおいといて、URLの部分はここで学んだ知識が使われていますね。 curlはやめてechoにしてみましょう。

$ echo https://download.docker.com/linux/$(. /etc/os-release; echo "$ID")
https://download.docker.com/linux/ubuntu

私の環境ではubuntuと展開されるようです。 $()の中を見るとドットで始まっています。これは解説してないですが、引数のファイルを現在のシェルで実行する、という機能です。実行されるシェルスクリプトを見てみましょう。

$ cat /etc/os-release
NAME="Ubuntu"
VERSION="18.04.3 LTS (Bionic Beaver)"
ID=ubuntu
ID_LIKE=debian
PRETTY_NAME="Ubuntu 18.04.3 LTS"
VERSION_ID="18.04"
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"
VERSION_CODENAME=bionic
UBUNTU_CODENAME=bionic

どうやらこのLinuxがどういうディストリビューションか、とか、バージョンとかが書かれているようですね。これらは変数として代入されていきます。そしてこのスクリプトを実行したあとにecho $IDを実行しているので、このファイルに書かれているIDが展開された結果が標準出力に出るのでしょう。

せっかくなので試しにこの仮説があっているかを確認してみましょう。

$ . /etc/os-release
$ echo $ID
ubuntu

あってそうですね。

なお、$()の中は子プロセスで実行されるので、元のスクリプトではechoしたあとにはこの変数は消えているはずです。

このようにURLやファイルのパスの一部を作ったり、コマンドラインの引数の一部を使うのにバッククオートや$()は良く使われます。

実例その2、dockerのコマンドライン引数生成

以下のgistの、dockerを実行している所、 For execution from android phone.

pushd ~/ClassSim;
docker run -it --rm --publish 52688:8888 --publish 6006:6006 -v $(pwd):/work arino/tfcpu2 /work/notebook.sh

この-vの所で$()を使っています。-v $(pwd):/workの所ですね。 これはpwdを実行した結果に展開されます。

dockerのコマンド自体の意味はのちほどやりますが、こんな風にバッククオートや$()が使われるんだなぁ、と思っておいてください。

実例その3、jupyterの接続元ip指定

これは手元のスクリプトになりますが、以下みたいなスクリプトがありました。

#!/bin/sh
# used from docker image

jupyter-notebook --ip=`ip route | awk 'NR==2 {print $9}'`

awkはテキスト処理の所で扱います。ipというコマンドとawkというコマンドを使ってdockerを実行しているホストのipアドレスを取っているようです。

確かコンテナの中から呼び出し元のIPアドレスを取る方法、とかでググったらip routeで取れる、と出てきて、それを適当に整形してバッククォートで使う、みたいな風に書いた記憶があります。

環境変数入門

環境変数とは、シェルの設定を行う特殊な変数です。 環境変数回りのトラブルはちょくちょくあるので、簡単に基本を説明しておきます。

envで環境変数を確認する

envというコマンドを実行すると、現在の環境変数の一覧が見れます。やってみましょう。

$ env
PWD=/home/karino2/Documents/linux_intro_ml
USER=karino2
HOME=/home/karino2
...その他一杯出力される...

これらは通常の変数として見る事も出来ます。例えばUSERを表示するなら、 これまで通りechoを使えば見る事が出来ます。

$ echo $USER
karino2

良く使う環境変数

機械学習をする人がお目にかかる環境変数はそんなに多くありません。 ざっと説明しておきます。

PATHとwhichコマンド

シェルがコマンドを探すディレクトリの一覧が入っています。ディレクトリはコロンで区切られています。

$ echo $PATH
/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin

人によってはもっといっぱいずらずら出てくるでしょう。 上記の例だと、/bin, /sbin, /usr/bin, /usr/bin, /usr/local/bin, /usr/local/sbinから実行コマンドを探します。

例えばpythonと打った場合、上記のディレクトリを順番に探していって、最初に見つかったpythonが実行されます。

なお、最初に見つかるpythonがどこにあるかはwhichコマンドで探せます。

$ which python
/usr/bin/python

/usr/binにあるらしいです。 なお、この場合は、以下のように実行するのと

$ python

以下のように実行するのは同じ事になります。

$ /usr/bin/python

このPATHにホームディレクトリのディレクトリを追加したり、新しくインストールしたアプリのディレクトリを追加したりする事があります。

特に良くあるのがaptやpipなどでインストールしたのに実行出来ない、という時に、このPATHが適切に更新されていない、というパターンです。 そういう場合は手で追加したあとにあとで説明するexportを使って動くか確認したりしていきます。

USER

現在のユーザー名が入ります。 ユーザー名はインスタンスによって自分のローカルのマシンの名前だったりubuntuだったりdockerだったりgmailのアカウント名だったりするので、スクリプトを共有するのに$USERを使うのが良い事があります。

前述のセットアップのスクリプトでは、以下のように usermodでdockerのグループに自身を加えていますね。

sudo usermod -a -G docker $USER

詳細はインスタンスのセットアップの所で説明しますが、これはdockerを使う為に必要な設定です。

LD_LIBRARY_PATH

ロードする共有ファイルを検索するディレクトリの一覧が入っています。 共有ファイルというのはlibXXXX.soみたいな名前のファイルです。 数値計算ライブラリとかCUDAとかpythonの外の何かを必要とする時にインストールした物がうまく動かない時にこの辺を調べる事になります。

LD_LIBRARY_PATH周辺のトラブルはちょっと難度が高いので、少しチャレンジして無理だったらチーム内の詳しい人に聞いても良いと思います。 ただ一応名前くらい知っておくと、見よう見まねでトラブルシュートしないといけない時に役に立ちます。

LANG

日本語がおかしい、みたいな時にたまにいじる必要があります。 en_USとかCとかja_JP.UTF8とかをごちゃごちゃやる必要があるかもしれません。

これもチームの詳しい人に聞いて良い奴ですが、名前くらい知っておくとピンチの時に役に立つかもしれません。

環境変数の更新とexport

環境変数を変更する時は、ただ新しい値を代入するだけじゃダメで、何故かexportという事をやらないといけません。

たとえばLANGをja_JP.UTF8に変更したい場合、以下のようにします。

$ LANG=ja_JP.UTF8
$ export LANG

また、以下のように書いても同じ意味になります。

$ export LANG=ja_JP.UTF8

この辺は「何故?」とか考えずに、こういう物だと飲み込んでください。

子プロセスと環境変数の継承と設定

機械学習やってて環境変数が出てくる一番重要なシチュエーションは「なんか環境変数が設定されない!」というトラブルの時です。

具体的にはJupyterからシェルコマンドを実行してaptやpipでインストールしたりした時にPython側に反映されない、とか、dockerコンテナ内で設定したかったのに間違ってホストに設定していたり、とか、tmux上で設定してログインシェルに反映されてないとかそういうのです。

そうしたトラブルを理解する為に、基本的な事を簡単に解説しておきます。

シェルの起動時に.bashrcが実行される

環境変数の設定などは、ホームディレクトリにある.bashrcで行います。

機械学習では以前よりもnon login shellを使う事が増えたので.profileよりも.bashrcに設定は書きましょう。

気を付ける事としては、この設定はシェルの起動時に読まれる、という事です。 ですからシェルが起動している状態でこのファイルを変更しても、シェルを立ち上げなおすまでは更新されません。

環境変数は親から継承される

あるプロセスを作ると、環境変数は基本的には親から継承されます。 継承というのはコピーされる、という事です。 だから親のプロセスに環境変数を設定して新しい子プロセスを作ると、基本的には親の環境変数が子に引き継がれます。

なお、明示的に引き継がせずに子プロセスを作る事は出来ますが、機械学習には必要無い知識です。

一度子プロセスが作られたあとにどちらかを変更しても反映されない

作成される時に引き継がれるのはコピーです。 ですから子プロセスが作られた後に親のプロセスの環境変数を変更しても子プロセスには反映されませんし、子プロセスの環境変数を設定しても親プロセスの環境変数は更新されません。

これは当たり前の事なのですが、意外と困るシチュエーションがあります。 少し具体例を見てみましょう。

具体例1: シェルスクリプトで環境変数の更新が出来ない

シェルスクリプトに関してはこのあと解説するのですが、 良く実行するコマンドをシェルスクリプトでまとめる、というのは良くやる事です。

ですが、このシェルスクリプトの実行方法を気を付けないと、 子プロセスで実行されてしまって親のシェルの設定は変更出来ない、というのは良くあります。

基本的にはbash ファイル名で実行すると子プロセスが実行され、source ファイル名. ファイル名で実行すると現在のシェルで実行されるのですが、 Dockerfileやその他のプログラムから実行される時には解決策が無い場合もあります。

具体例2: Jupyterのシェルコマンドで環境変数の更新をしてもPythonに反映されない

Jupyterのエクスクラメーションマークによるシェルコマンドは(たぶん)Pythonの子プロセスとして実行されます。 だからこの中で例えばPythonのライブラリの検索に関する環境やLD_LIBRARY_PATHなどを変更しても、Pythonには反映させられません。 Pythonのos.systemモジュールを使って実行しても同様です。

こういう時は「Jupyterを実行しているシェル」の環境変数をどうにか更新したあとに、Jupyterを再起動する必要があります。

シェルスクリプト入門

シェルスクリプトは非常に高機能なのですが、機械学習では作業の再現性の都合でなるべくJupyterで作業する方が望ましいため、普通のシステムアドミニストレータほどは詳しい必要はありません。

ですが環境のセットアップやちょっとした自動化、そして他の研究者が書いたシェルスクリプトを読むなど、ちょっとした事で必要になる事はあるので、基本くらいは知っておく価値があります。

そこでここでは、そういう用途で必要になる程度の最低限のシェルスクリプトの話をします。

シェルスクリプトの作り方

  1. ファイルを#!/bin/shという行で始める
  2. シェルのコマンドをつらつら書く
  3. ファイルに実行属性をつける

これで作る事が出来ます。 試しにhello.shというファイルでシェルスクリプトを作ってみましょう。

まずファイルを作ります。(上記1と2)

$ echo '#!/bin/sh' > hello.sh
$ echo  >> hello.sh
$ echo 'echo "Hello World"' >> hello.sh
$ cat hello.sh
#!/bin/sh

echo "Hello World"

次に実行属性をつけます。 実行属性はコマンド入門のUNIX Tutorial - 4. Managing files and folders - UC Berkeley School of Informationで少し出てきたchmodを使います。

この時点では、hello.shの属性は以下のようになっています。

$ ls -l hello.sh
-rw-rw-rw- 1 karino2 karino2 30 Jan 28 19:47 hello.sh

オーナーとグループとその他に、それぞれreadとwriteの権限がついています。実行属性(xで表される)がついていません。

実行属性をつけるには+xを使います。

$ chmod +x hello.sh
$ ls -l hello.sh
-rwxrwxrwx 1 karino2 karino2 30 Jan 28 19:47 hello.sh

xがついたのが分かると思います。 これでシェルスクリプトは完成です。

実行はフルパスを指定すれば普通のコマンドのように実行出来ます。 また、./をつけて相対指定でも実行出来ます。

$ ./hello.sh
Hello World

コメントはシャープ

コメントはシャープ記号で行います。シャープ記号があるとそのシャープより後ろがコメントになります。 ただし先頭の#!/bin/shだけは特別でコメントではありません。(厳密にはシェルからはコメントだがOSに特別な意味がある)

例えば以下のようになっていると、最後の行は無視されます。

#!/bin/sh

echo "Hello World"
# This line is comment

複数行に分割したい場合は改行の直前にバックスラッシュ(環境によっては円記号)

複数の行にまたがる事をやりたい場合は改行の直前にバックスラッシュでエスケープします。

#!/bin/sh

#これはls -lと同じ
ls \
-l

外部のシェルスクリプトの実行

シェルスクリプトから別のシェルスクリプトを実行したい時には、実行方法は

  1. source ファイル名 (. ファイル名 でも同じ)
  2. bash ファイル名

の二つの方法があります。1は現在のシェルでファイルの中身を実行します。 2は新しい子プロセスのシェルを作ってそこでファイルの中身を実行します。

これはシェルスクリプトの中でもコマンドラインからでも使えます。 たとえば以下。

$ bash hello.sh
Hello World
$ source hello.sh
Hello World
$ . hello.sh
Hello World

ドットによる実行はバッククオートの解説で出てきてしましたね。再掲しましょう。

$ echo https://download.docker.com/linux/$(. /etc/os-release; echo "$ID")

$()の中で、/etc/os-releaseをシェルスクリプトとして実行している訳ですね。

.bashrcを変更した時に、コマンドラインでその場で変更を反映したい時にも使います。

$ source .bashrc

これは以下と同じです。

$ . .bashrc

.bashrc内で副作用がある事をやっているとそれも実行されてしまうので注意。

最初の行の意味

最初の行は#!/bin/shで始める、といいました。 これはシェルから実行されるファイルは、先頭が#!で始まっている時はスクリプトとして解釈し、 そのあとに書かれているコマンドにファイルを渡して実行する、という決まりになっているからです。

だから/bin/sh以外でも良い訳です。 良く使うのは我らがpythonです。

#!/usr/bin/python

# pytnon2系列なら
# print sum([2*i for i in range(5)])

print(sum([2*i for i in range(5)]))

ただ#!/bin/sh以外はほとんど使いません。

あんまり使い過ぎないように気を付けよう

シェルスクリプトは高機能で、if文やfor文などもあったり、ファイルの操作回りの豊富な機能があったりしてちゃんと勉強すればいろんな事が出来ます。

ただシェルスクリプトの文法は古い時代に作られたせいでいろいろと落とし穴も多く、 経験を積まないと生産性はあまり高くありません。

また、シェルスクリプトをどう使うかはコマンドラインの操作に埋もれてしまって、 別途ドキュメントを書かないと第三者に伝わりにくくなり、実験の再現性という観点からはあまり良くありません。

機械学習の分析を本業とするならあまりシェルスクリプトで複雑な事はせず、 なるべくipynbファイルを見れば何をやったかが全て分かるようになるべくJupyter上で作業しましょう。