Bashこんにちは、エンジニアリングGの中村です。以前にこのブログにてエムスリーでの社内研修について紹介しました。今回は、この中でのbashスクリプティング講座の資料を公開します。www.m3tech.blog弊社の中でもいろいろな用途でbashが使われていますが、bashは簡単に利用できるもののプログラミング言語としてはバグを生みやすい、辛い言語だと思います。ここで紹介しているのはいわゆるコーディング規則というよりも、バグ防止と可読性向上のためのルールをTips集的にまとめたものです。bashにおいてまだまだ注意するところはありそうですが、多少なりともわかりにくいスクリプトの削減になればと期待しています。2018-08-22]はてブにて以下のコメントをいただきました。エムスリーテックブログbashで50行以上になったらまずは違う言語使うでいいと思うのだが。20:00b.hatena.ne.jpこれは全くそのとおりだと思います。大事な主張が、研修の中で口頭では伝えていたのですが資料に書き忘れているのに気づきました。複雑な処理はbashではなく他の言語(pythonなど)を使った方がよいです。この資料は、bashという言語がいろいろと注意点が多く、本気で対処するには辛い言語であるということを伝るためのものでもあります。bashで無理にテクニックを駆使してやろうとするのではなく、使いやすい言語を選ぶことを検討した方がよいと思います。-xオプション参考資料エンジニア募集bashスクリプティング講座はじめにbashスクリプトは誰でもとりあえずは作成できるという反面、記述が雑になりやすい傾向があるように思います。一度作成されたものはその後も使い続けられ、後々になって思わぬバグがおきたり、メンテナンスがしづらくて苦労したりということになります。そのため,bashといえども他の言語での開発と同様に読みやすく・わかりやすく書くように心がけてください。言語面での制約が少ない分、わかりにくいコードになりやすい(そういうのを書けてしまう)ので、他よりも一層良いコーディングを意識する必要があります。以下では、bashでバグになりやすい点や、読みやすくなるような書き方の指針、bashのテクニック的なことを上げています。そもそもまた、そもそもbashを使うべきなのどうかを考えてください。この資料を見て、実はbashがいろいろと辛い・大変な言語であることを感じてほしいと思っています。ある程度以上の規模や複雑なことをするのであれば、bashではなく他の言語を使ってください。bashを使うのであれば、ここから示す注意点や記法を守りつつ作成してください。スクリプトの定型テンプレート最初に、シェルスクリプトのおおよその書き方のテンプレートを以下に記載します。説明の詳細文を記述します。0}#test_fugatest_args#test_mainbashの書き方でよくある問題bashは便利な反面、値の解釈などで分かりにくい動きが多くあります。以下の記事にていろいろな落とし穴が紹介されています。Yaksthttps://yakst.com/ja/posts/2929本記事は上記の記事を参考にさせていただきつつ、社内で特にありがちなところを補足したものです。/bin/shではなく/bin/bashを明示するスクリプトの1行目のシバン(shbang)には、shを使うことを明確にしていないならbashを明示してください。shとbashは正確には別物です。shはPOSIX仕様にそった機能のみが使えるものですが、bashはそれよりも機能が豊富です。OSによっては/bin/shが別のシェルのシンボリックリンクである場合があります(dashだったり)。/bin/bashのシンボリックリンクの場合もあったりしますが、実体はbashでもsh実行モードで動いていて実質POSIXしか使えないときもあります。同じスクリプトをいくつものUnix,Linux系OSをまたいで流用するのであればshを意識して実装する必要があります。社内のスクリプト(特に分析用・調査用のスクリプト)は、特定の環境でのみ動けばいいようなことがほとんどなのでbashを使っておいてください。変数を展開するときは必ず「"」で囲うbashでの変数は普通のプログラミング言語とはかなり違うため、色々と注意する必要があります。その中で大きなポイントとしては以下の2点です。半角スペースやタブを含む値を扱う時これらの直感的にはわかりにくい問題を回避するためにも、変数を利用するときは必ず「""」で囲うようにしてください。bar上記の2つのコマンド実行は同じ意味になります。some_funcの第1引数は$VAL1ではなくfooとなります。fooを第2引数として指定したいのであれば、$VAL1をダブルクオートで囲んで第1引数が空文字となるようにする必要があります。その2以下は、if文の構文エラーになってしまいます。「-eq」になるため左辺がない)変数をダブルクオートで囲うと回避できます。変数の値に半角スペースがあると、コマンド引数やループなどのときにスペース区切りで値が分解されてしまいます。if文は[〜]ではなく[[〜]]を使うbashでは、条件判定部分を[とは別に[[を使えます。[[の方が安全で機能が豊富なのでこちらを使うことを推奨します。エラーにならず、"OK"と表示される詳しくは以下参照してください。配列変数を展開するときは「"」で必ず囲うコマンドやfunctionに渡された引数の全てを保持する「$@」という特殊変数を使う時は、必ず「"」で囲ってください。値に半角スペースを含む場合に囲うかどうかで挙動が変わってしまいます。300「"」で囲わない場合は、値に半角スペースを含む場合にそこで分解されてしまいます。スペースを含むファイル名を受け取るような場合に正しいファイル名を利用できないといったことが起こります。この「"$@"」はこの4文字で固有の動作をする表記になっています。$@"といった表記では成立しません。引数の一覧を取得する別の方法に「$*」というものがありますが、こちらでは「"$*"」と書いても必ず半角スペースが分割されてしまいます。基本的に「$*」は使わず「"$@"」を使うようにしてください。bashスクリプトのいい感じの書き方スクリプトを書くときの全体的な記述の方針を以下にまとめます。functionの記述方法functionを定義するときは以下のフォーマットで書いてください。例としては以下のような書き方になります。grepなどでfunctionの定義を探しやすくするために明記することをお願いします。functionなどのブロック内はインデントするbashも普通のプログラミング言語と同様にコードブロックの中ではインデントして記述してください。例外としてヒアドキュメントの場合などで行頭に空白を入れられない場合は除きます。インデントはスペースのみファイル中のインデントはスペースで2文字、もしくは4文字で記述してください。スペース・タブ混在は不可です。タブ文字はgitlabなどでの表示の文字幅などが指定できないため、見た目を統一するためにスペースとしてください。1ファイル中の記述の配置シェルスクリプトに記述することになる要素は以下のようなものがあります。関数定義スクリプトのメイン処理のエントリー部分これらの要素は、要素ごとに一塊になるように記述してください。定数や関数の定義がファイル中に散在すると、見落としの原因になってスクリプトの全体を読みづらくなります。各要素の記述順序は基本的に前述のテンプレートのような順序で記述するようにしてください。main関数を作成してエントリーポイントとする引数のパース処理やグローバルの定数・変数の定義を除いて何らかの処理を記述するときは、必ず関数として定義してください。スクリプトのエントリーポイントとなるメイン処理も、main関数を定義してそれをスクリプトの最後で呼び出すように記述してください。thenでブロックすることで、スクリプトを直接実行したときのみ処理を行い、sourceで読み込まれたときは処理を行わないようにできます。これには以下の利点があります。関数、変数の命名については以下の表記としてください。ようにしてもよい時間を表す数値などは、変数名で単位がわかるようにする。変数名は長くなってもわかりやすく付けるbashはグローバル変数の利用が多くなりがちで、その性質から定義場所と利用場所が離れるなどして読解が難しくなることがあります。そのため、グローバル変数については変に短くするよりも長いほうがいいと考えます。コマンド、functionを作成するときはしっかりとコメントを記述するコマンドやfunctionの使い方や作成された意図などについてコメントを残すようにしてください。sourceして使う前提のファイルでも、そのファイル自体にも用途やそのファイルの意図を記載してください。以下の点を明確になるように記載することを心がけてください。コマンド実行結果の標準出力を変数で取得するときは「$(cmd)」表記を使うコマンド実行の標準出力を取得するには、「`command`」と「$(command)」の2通り表記がありますが、必ず「$(command)」で記述してください。「`」の表記は記号が小さく視認しづらいのと、「$()」であれば入れ子で記述することができるためです。localやreadonlyを付けて変数を定義するbashではfunction内で定義した変数もグローバル変数になります。それを避けるためにfunction内で変数を定義するときはlocalを付けてください。それを付けずに意図的にグローバル変数を定義しようとしてるときは、何か無茶をしようとしていると思って再考してください。localの変数のスコープは普通のプログラミング言語とは異なるので過信しすぎないように注意してください。localはそれが宣言されたブロック内の呼び出し全てに影響します。値を変更しない定数を定義するときはreadonlyを付けて定義する値を変更しない定数を定義するときは、readonlyを付けてください。これをつけた定数は上書きで代入しようとするとエラーになります。グローバル変数をできるかぎり使わないbashは変数のスコープを制限しづらくグローバル変数を多用しがちです。グローバル変数を使うと実装を読み解きづらくなるため基本的にはグローバル変数を使わないように実装してください。functionを読み解くときに、そのfunctionだけを見れば処理が理解できるようになることを心がけてください。グローバル変数を直接参照できるとしてもその変数を直接つかうのではなく、引数から渡すようにしてください。何もかもの変数を渡して回ると逆に読みにくくなる場合もありますので、グローバルな定数やほぼ定数的に使うようなものは直接使ってもよいです。複雑な処理が必要になったらbash以外を使うことを考えるbashは簡易なことを行うなら便利ですが、複雑なデータ構造のものを扱う場合などには向きません。そのため、bashでやるのが難しいことがでてきたらbashを使うのを諦めて別の言語やコマンドを使うことを考えてください。-euCを付けるbashの実行時のオプションにスクリプトを作成する上で有用なものがあります。オプションはbashコマンドの引数としてしてするか、スクリプト中にsetコマンドで指定できます。-euCを付けてください。"myscript"シバン(#!/bin/bash)のところに記述することもできますが、それだとデバッグ時などでスクリプトをbashコマンドで直接実行するようなときにオプションが機能しなくなるのでsetで指定する方がよいと考えます。0以外での終了)した場合にスクリプトを停止します。逆に言えば、このオプションなしではコマンドがエラー終了してもそのまま処理が継続してしまいます。そのため、想定外のバグや突発的なエラー発生時に、誤って後続の処理をしてしまうようなことが無いように、このオプションは必ずつけるべきです。いう場合は一時的に-eオプションを解除するようにする必要があります。true-uオプション-uオプションを指定すると未定義変数を使おうとするとエラー終了するようになります。これにより、変数名のタイプミスや初期化わすれでの変数の利用などを防ぐことができます。この場合はOK-Cオプション-Cオプションを付けるとリダイレクトでのファイル上書き時にエラー終了するようになります。リダイレクトでファイルを生成するときに想定外に上書きしてしまうことを防ぐことができます。スクリプト終了時に必ず処理をさせたいときはtrapを利用するtrapコマンドを使うことで、スクリプトが終了したときに必ず実行する処理を指定することができます。それをつかって、スクリプトの最後に掃除処理などを行うことができます。関数から複数の値を返したい場合は以下のようにprintコマンドを使うと対応できます。おすすめするわけではないですが、使う方法のテクニックとして紹介します。temp_table_name}ログの出し方ログを出力するときに、処理中のファイルや関数、実行行数などを出力することができます。message途中終了でも安全なファイル生成方法ファイルを生成するときは、一旦別名のファイルに出力した上で最後にリネームするようにするとよいです。ファイルの生成途中でエラーになったり、手動停止したりした場合に、作成途中のファイルを間違って最終データとして使ってしまわないようにすることができます。再実行時に既にファイルがあるときはスキップするような処理にするときにも有功です。target_file.csvfi一時的に環境変数を設定・上書きした状態でコマンドを実行するシェルでコマンドを実行するときに、一時的に環境変数を上書きしてコマンド実行することができます。その内部での変数への変更は「()」の外側に影響しません。これは、cdでのフォルダ移動も同じ効果があります。~/コマンドやfunctionに長い文字列を渡すときは標準入力を使う表示用の文言やSQL文などの長い文字列を渡すような場合は、引数で渡すよりもパイプを使って標準入力をつかった方が良いです。"pause"で一時停止readコマンドをつかえばEnterキーを入力するまで停止できます。処理の途中状態を確認するときや、trapなどで掃除処理を仕込んだ場合でそれが実行されるまえの状態を確認するようなときなどに便利です。シグナルとしてDEBUGを指定するとスクリプト中の全行の実行時に処理を割り込めます。シグナルとしてERRを指定するとコマンドのエラー終了時に処理を割り込めます。--に続けて文字列を記述すると、それらを実行中のスクリプトの引数として上書きできます。スクリプトのテストを記述するようなときに有用です。"$@"上記を実行すると以下のようになります。-xオプションbashの-xオプションをはデバッグをするときに有用です。これを指定すると、スクリプトが処理したコマンドが表示されます。変数も実際の値に展開されて表示されるので、実際に処理された内容を把握することができます。100注意パスワードを変数に保持して使うような処理で、-x付きのログをログファイルに保存したりしないこと。作成にあたって以下の記事などを参考にさせていただきました。オープンソースこねこねhttp://kohkimakimoto.hatenablog.com/entry/2016/03/14/044924エンジニア募集エムスリーでは自身で手を動かし、技術で医療の課題を解決するエンジニアを募集しています。13:00bashスクリプティング研修の資料を公開しますTweet関連記事2019-12-21機械学習パイプラインライブラリluigiのshell補完ツールを作ったこんにちは。Python謹製のshellを使って早3年、エムスリーエン...2019-12-20Scalaで参照透過に作用を扱うエムスリーエンジニアリンググループの冨岡です。AI・機械学習チームの笹川です。月別アーカイブ注目記事はてなブログをはじめよう!m3techさんは、はてなブログを使っています。ブログを報告するスターをつけました引用をストックしましたストック一覧を見る閉じる引用するにはまずログインしてくださいログイン閉じる引用をストックできませんでした。再度お試しください閉じる限定公開記事のため引用できません。