scriptシェルスクリプトで複数コマンド群に名前を付けてグループ化して可読性を向上させる方法について検討した。命名グループ化の検討の概要コマンドのグループ化には変数,関数,aliasの3種類の方法が存在。aliasは関数と複合文での利用に注意が必要で,過去のPOSIXでオプションのため利用は非推奨。実行速度の計測結果から,命名グループ化には変数(単独)と関数(波括弧)の利用を推奨。今回の記事で利用したコード類は以下に格納している。expand_aliasesaliasが使えない場所ResultSummaryIntroductionシェルスクリプトでは,パイプを使うことで複数のコマンドを繋いで処理できる。パイプをつないで記述されたコードは,全体として何をやっているのか意味がわかりにくくなる。複数のオプションを組み合わせたコマンドの記述や,自分がよく知らないコマンドが使われた場合,どういう処理をやっているのかわかりにくくなる。処理内容をわかりやすくするために,複数の処理を変数や関数にまとめ,適切な名前を与える方法がある。例として,以下にpwdコマンドが存在するかどうかを判定するコードを掲載する。-vの処理内容を理解していないと,見ただけでは意味がわからない。-vの意味を知らなくても全体の処理内容を理解しやすくなる。foundまた,関数にまとめるだけでなく,変数にまとめることでも可読性の向上を実現できる。以下のコードはbashのバージョンが4.3以上であればbindコマンドを実行している。menu-complete'fiこのコードは以下のように,IS_BASH_VER_GE_43変数に一度代入すれば,どのバージョンを対象としているのかがよりわかりやすくなる。menu-complete'fiこのように,関数や変数などシェルスクリプトには複数の処理に対して別の名前を付ける方法がいくつかある。わかりやすくなったのはいいが,それで実行速度が遅くなっていないだろうか?複数の実現方法があるのならば,最も簡単で最も速度の速い方法を採用したい。そこで,シェルスクリプトで処理をグループ化して可読性をあげるための方法を紹介し,実行速度を計測することで,どの方法がベストであるかを検討した。コードの可読性の向上に関しては,コメントを書くことで補うこともできる。コメントで説明した場合,実際のコードとコメントの内容を合わせて読む必要があり,読み手に負担がかかる。コメントを書く前に,コメントが不要になるようにコード自体をわかりやすくすべきだろう。Method処理のグループ化には以下の3種類の方法がある。Variable(変数)Function(関数)aliasVariable(変数)1点目は変数に処理を代入する方法だ。以下のように処理内容を(マクロ)変数に代入し,変数展開により代入された内容を実行する。menu-complete'fiシンプルで記述量が少ないが,いくつか注意点がある。&&,||,;,&など)利用時は先頭にevalが必要。evalを使わなければ複数コマンドの実行はできない。引数は末尾にしか使えない。evalを使わなければ,変数展開は一度しか起きないため,再帰処理は不可能。変数に代入して展開する場合,通常のコマンドラインの解釈が異なり,予約語が通常の文字列とみなされてしまう。これを回避する場合,先頭にevalを記述することで残りの部分をevalで解釈してやる。evalを先頭に使う場合,本来よりもコマンドを余計に1個実行するため,速度が遅くなる懸念がある。>使い方としては,条件判定結果を格納して,条件判定に使うのが良い例だろう。]のように記述する方法もある。個人的にはこの方法は好ましくないと考えている。人によって条件判定値とその意味が異なる(0と1のどちらが真か不明)。testコマンドによる記述が冗長になる。変数に直接true,falseを代入して使ったほうが,意図がわかりやすく,また共に組み込みコマンドとしてPOSIXで定義されているので(速度も)安定している。Function(関数)2点目は関数を使う方法だ。以下のように複数のコマンド群をグループ化できる。"$OPT"'[[:blank:],=[]')関数を記述する場合,本体部分に波括弧{}か丸括弧()を使うことができ,それぞれで挙動が異なる。現在シェルで実行する。関数内で定義した変数の値,関数定義,set,exportなどの特殊組み込みコマンドの実行結果が,現在シェルにも作用する。サブシェルで実行する。関数内で定義した変数の値,関数定義,set,exportなどの特殊組み込みコマンドの実行結果が,現在シェルには作用しない。丸括弧()で定義した場合,その関数は別プロセスとなり現在シェルへの影響を抑制できる。そのため,丸括弧で関数を定義すれば,意図しない変数の変更などを防ぐことができ,保守性が上がる。代わりに,新しくプロセスを作成するので,実行速度が遅くなる懸念がある。関数を使えば,引数の扱いも柔軟にでき,複雑な処理も可能なので,汎用性が高いと考えられる。alias3点目はaliasを使う方法だ。enabled"fialiasには以下の特徴がある。変数を使うより柔軟な対応ができ,aliasの定義は一度だけでよいので,alias実行はeval変数を使う場合よりも速度が速い可能性がある。ここでいうaliasの定義とは以下のようにaliasコマンドでaliasを作成することを指している。-l'変数や関数と異なり,名前に!%,@を使えるので,工夫すればオブジェクト指向型言語のような,メンバー関数(メソッド)やメンバー変数(プロパティ)を実現できる可能性がある。引数は末尾にしか取れないbashではexpand_aliasesオプションを有効にしないと使えない関数や複合文など定義しても使えない場所がある1点目の引数については,変数と同じであり,エイリアス名の後に渡した内容をそのまま引数と解釈する。expand_aliasesbashにはexpand_aliasesというシェルのオプションが存在する。このオプションはbashでalias展開を有効化するためのオプションとなっており,以下のどちらかのコマンドでalias展開を有効化できる。startupbashは標準では対話シェル以外では,expand_aliasesがオフになっており,aliasが展開されない。Manual)で起動された場合はaliasが展開される。bashがPOSIXモードで起動される場合は以下となる。posix)シェルスクリプトでbashのaliasが展開されないパターンとしては,以下2通りが存在する。foundここでは詳細を述べないが,シバンがない方がシェルスクリプトの互換性が高まることが調査の結果わかっている。そのため,aliasを使う場合は,bashの対話シェルからでも起動できるように以下のコードで冒頭に記述する必要がある。expand_aliasesaliasが使えない場所aliasはコマンドを実行しても,aliasが定義できなかったり展開されない記述箇所が存在する。関数内で定義した場合は,(たとえ他の関数内でも)関数内では展開できない(関数外なら展開できる)。&)で定義した場合は,その内部では展開できない(外部では展開できる)。foundこのことについて,理由を考察する。1点目の関数内で定義した場合に関数内で使えない理由だ。これは関数は一つの独立した環境であるためだ。コマンドの実行環境は以下の通りに定義されている。environment)ではaliasが定義されているが,コマンド(utilityl)に関してはaliasについて定義されていない。上記引用部分の末尾で強調した通り,コマンド内で明確に変更されることが記述されていれば,変更は可能である。aliasの説明を確認しても,関数内でaliasを定義できるとの文言はない。そのため,関数内でaliasの定義と実行を同時にはできないのだと考えられる。2016なお,aliasの説明で書かれている通り,現在シェルでaliasを定義した場合は,現在の実行環境とサブシェルに影響を与えるため,関数内でもaliasを展開できる。2点目の複合文内でのaliasの定義と展開の同時実行について。これは,複合文内はUtilityと同じように別の環境であるためだと考えられる。utilityを除く名前が付いているものと定義されている。2016複合文は名前は付いていないので,Utilityではないと思うのだが,シェル実行環境とは異なるためこのような挙動になっているのだと思われる。実行後に現在シェルでaliasが有効になっている件については,関数も複合文も同一プロセスであるため,実行後の現在シェルにはaliasの定義が残るのだと考えられる。複合文でのaliasの定義・展開については一つ懸念がある。それは,複合文内でaliasの定義・展開が行われている外部のシェルスクリプトをdot.コマンドで読み込んだ場合,どうなるかだ。シェルスクリプトが外部でどのように利用されるかは利用者次第だ。コマンドとして実行される他に,dotコマンドでライブラリーとして読み込んで実行されてもおかしくはない。その場合,複合文内でdotコマンドで読み込んだ場合,この挙動だと確実にalias展開が失敗することになる。Utilityであるため,また別の挙動を示す可能性がある。そこで,これらのaliasがどのような挙動を示すか調査した。実際にいろんなシェルでaliasの定義と実行結果について調査し,表にまとめた。検証コードは以下となる。aliasが使えないことがあるのでその検証。複合コマンド内でaliasを定義と実行を同時にできない。c=:}func2(){c}func1func2表の見方は,縦軸に実行したコマンドがあり,横軸に実行したシェルが並んでいる。○が付いた欄はalias展開に成功したことを示し,空欄は失敗したことを示す。調査結果についての考察を以下にまとめた。aliasの挙動の結果考察poshはaliasコマンドが存在しないため,全ての欄が空欄となった。予想通り,複合文とリストではaliasは展開できなかった。ksh93だけ複合文内でdot.コマンドでaliasを展開できず,他のシェルでは展開できた。dot.コマンドで読み込む場合は特殊なようで,多くのシェルでは問題なく複合文内でもaliasを展開できた。ksh93だけはできなかった。aliasは名前に使える文字が多く,再帰など興味深く応用が効きそうではある。関数内や複合文内での定義・実行に難があることがわかった。Option)扱いであり,必須ではなかった。実際に,poshのようにaliasコマンドに対応していないシェルが存在する。これらのことから,シェルスクリプト内でのaliasの利用は控えたほうが無難かもしれない。Resultここまでで処理に名前を付けてグループ化する3種類の方法について紹介した。実際にこれらの方法でコマンドを実行して,実行速度を計測した。実行は以下の8パターンを試した。)関数の実行方法が変わった際にどれくらい実行速度が変わるかを見たいだけなので,試験用のコマンドは何もしない組み込みコマンドの:を採用した。上記の7と8は処理の命名グループ化とは直接は関係ないが,コマンド実行時間がどれくらいかかるかの参考にする良い機会と判断したので,追加した。16.04で測定した。binary.検証には以下の2個のコードを利用し,./run-time-macro.shを実行した。*//g'}main実行結果を整理したものを以下の表に示した。28.39まず,表の見方について説明する。この表は10万回実行した際の経過時間[s]を示している。そのため,100倍して,単位にm(ミリ)をつければ1回あたりにの行速度となる。bodyはコマンドの定義を含めずに,本体部分を実行した際の速度となっている。definitionはコマンドの定義を含めた計測時間となっている。aliasは複合文内で定義をすると実行できないため,計測のforループ外で定義する必要がある。処理の負担を平等にするため全てのケースでevalにより定義を実行している。s/10万回の時間が余計にかかっている。最後の複合文2個は定義と本体が同一であるため,単純に2回実行することになり,時間がかかっている。2列目の:の列が命名によるグループ化を行っていないケースなので,これが実行速度の基準となる。調査結果の考察を以下に掲載する。ms程度遅れる程度であり,十分速度が速い。これらはシェルのコマンドライン解釈だけで展開されるため,プロセスの発生がないため速度が速いのだと思われる。関数(丸括弧)と複合(丸括弧)は,サブシェルで実行するため,基準ケースの60-70倍も実行時間を要しており,ループでの利用は極力控えたほうがよい。変数(eval)は,基準ケースの2-3倍程度の実行時間を要しており,関数(波括弧)よりも遅いことが分かった。そのため,変数(eval)よりも,関数(波括弧)を利用したほうがよいといえる。busyboxは全体として組み込みコマンドの実行速度が他のシェルの約2倍かかっており,遅いことが分かった。それ以外のシェルは概ね似たような実行速度だった。Summaryシェルスクリプトの可読性を向上させるために,複数のコマンドに名前を付けてグループ化する方法を検討した。その概要を以下に示す。命名グループ化の検討の概要コマンドのグループ化には変数,関数,aliasの3種類の方法が存在。aliasは関数と複合文での利用に注意が必要で,過去のPOSIXでオプションのため利用は非推奨。実行速度の計測結果から,命名グループ化には変数(単独)と関数(波括弧)の利用を推奨。コマンドの実行というシェルスクリプトを書くうえでかなり基本的な部分に注目した検討を行った。実行速度を計測することで,今までなんとなく把握していた,丸括弧によるサブシェルでの実行が遅いということや,変数やalias展開は実行速度が速いということが定量的に分かった。今後は今回の調査結果を利用して,可読性と実行速度に注意を払ったシェルスクリプトの作成を心がける。コメントをキャンセルメールアドレスが公開されることはありません。「ScanSnapを使用することができませんでした。