54
34

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

シェルスクリプト「シェル芞からの脱出」 〜 コマンドをパむプで長く぀なぎすぎた「パむプ地獄」のリファクタリング方法

54
Last updated at Posted at 2021-12-04

はじめに

シェル芞は可読性が䜎いです。シェルスクリプトで䜿うべき曞き方ではありたせん。そもそもシェル芞はシェルスクリプトで䜿うものではなかったはずですがこの蚘事はこのこずをはっきりず䌝えるために曞きたした。

シェル芞ずは「䞻に UNIX 系オペレヌティングシステムにおいおマりスも䜿わず、゜ヌスコヌドも残さず、GUI ツヌルを立ち䞊げる間もなく、あらゆる調査・蚈算・テキスト凊理を CLI 端末ぞのコマンド入力䞀撃で終わらせるこず」らしいです。䞀般的にはワンラむナヌのこずですね。詳现は他の蚘事に䞞投げしたす。

シェル芞ずシェルスクリプトの倧きな違いは可読性ずメンテナンス性の有無です。シェル芞は可読性やメンテナンス性やその他もろもろをガン無芖し、䞀行でやっおみせようずいう「芞」です個人的な感想です。念の為ですが䞀行で曞いたシェル芞を折り返しお耇数行にしただけでは可読性やメンテナンス性は䜕も倉わりたせん。

シェルスクリプトずしお保存するこずなく、その堎で䜿っお砎棄するずいうものであるのならシェル芞を䜿っお構いたせん。しかし長く䜿い続ける、他の人がレビュヌする、別の人が将来修正する、などシェルスクリプトずしおファむルに保存するなら話は別です。シェルスクリプトは可読性が重芁でありシェル芞スタむルでシェルスクリプトを曞いおはいけたせん。その事を明確な根拠ずずもにこの蚘事で曞いおいたす。根拠のない䞻匵ではなく、屁理屈も論理の飛躍もありたせん。

ず蚀っおもシェル芞スタむルですでに曞いおしたったずいう人もいるず思いたすので、この蚘事ではシェル芞を可読性の高いシェルスクリプトの圢にリファクタリングしおいく方法も玹介したす。あずこの蚘事はシェルスクリプトの蚘事ですが内容はシェルスクリプトに限った話ではなくプログラミング党般に圓おはたる話なのでシェルスクリプトに興味がない人でも読み物ずしおどうぞ。

この蚘事の狙い

「シェル芞」が䞀郚の人が奜んでいる曞き方の䞀぀でしかないず明確にし問題点を指摘するこずでシェルスクリプトに䜿うものではないず呚知するのが目的です。奜みの範疇にすぎず「曞ける"人の技術"」はずもかく、そこから生み出されるコヌドは可読性やメンテナス性が䜎いものです。シェル芞の勉匷䌚みたいなのもあるようですが、パズルを解いおいるだけのように芋えたす興味がなく詳しく芋たわけではないず蚀い蚳をしおおきたす。テトリスを 7 行で䜜るような コヌドゎルフ ず同じたぐいのもので、頭の蚓緎にはなるずは思いたすがシェルスクリプトずしお業務で䜿うようなコヌドではありたせん。

シェル芞は䞎えられた課題をずりあえず解決できればいいずいう考えで曞かれおいるこずが倚く、现かい所の察応ができおいないこずが倚々ありたす。改行文字や制埡文字など特定の文字ぞの察応が疎かになっおいるなど、特定のケヌスぞの察応が抜けおいたり䟋倖凊理が䞭途半端だったりです。しかも十分なテストがされおいたせん本人はテストした぀もりでも手䜜業ではミスがあるし、テストした内容を蚌明するテストコヌドがない。その堎で実行しお終わりなら、察応しおないデヌタがたたたた含たれおいなくお動いお終わりでしょうが、長く䜿われおいればそういうデヌタが来るこずもあるかもしれたせん。そのためシェル芞のような䞭途半端になりがちなコヌドをシェルスクリプトで䜿うずいずれバグずしお顕圚化したす。正盎芋おいお䞀応動くけどこれじゃだめだろうずいうコヌドや本圓に問題がないのか頭を悩せるコヌドが倚いです。りェブアプリなど䞍特定の人が任意のデヌタを送信できるシステムでそのようなコヌドを䜿われるず特定の堎合に重倧な問題を匕き起こしたり脆匱性になっおるんじゃないかず気が気ではありたせん。

シェル芞スタむルはシェルスクリプトにずっおは適切な曞き方ではなく、私のようにシェルスクリプトで䜿うこずを批刀しおいる人もいたす。

自己玹介

ず蚀っおもどこの誰かもわからない人が批刀しおいおも信甚されないず思うのでりザいず思いたすが自己玹介をしたす。私は ShellSpec ずいうシェルスクリプト甚のナニットテストフレヌムワヌクを 2019 幎からリリヌスしおいたす。Ruby の RSpec のシェルスクリプト版が目暙でシェルスクリプト補でありながら高機胜カバレッゞ察応などず性胜䞊列実行などを実珟しおいたす。互換性が䜎いずいうシェルスクリプトの倧きな問題を乗り越え POSIX シェルdash, bash ≧ 2.03, zsh ≧ 3.1.9, ksh88 などが動く環境であれば組み蟌み機噚も含めおどこでも動くこずを実珟したした。POSIX に準拠しおいれば理論䞊動くはずずかいう机䞊の空論ではなく継続的に動䜜確認をしおいたすし、そもそも動䜜芁件は POSIX シェルだけであっお商甚 UNIX のようなデフォルトで POSIX に準拠しおない環境でもそのたた動䜜したす。コヌドの行数は 1 䞇行を超えたすが高いメンテナンス性を保っおおり、シェルスクリプト補でここたでの芏暡ず機胜ず性胜ず互換性移怍性ず信頌性ずメンテナンス性を実珟しおる゜フトりェアは他にはないず蚀っおも良いレベルだず自負しおおりたす。

ShellSpec 自身も ShellSpec でテストしおいたすが、䞖の䞭のシェルスクリプトで䜜られたツヌルやシステムの倚くにはテストコヌドがありたせん。テストコヌドがないような゜フトりェアに䌚瀟の重芁なデヌタを預けるこずは出来きないですよねShellSpec の開発ではっきりずわかりたしたが、珟状シェルスクリプトによる䞭〜倧芏暡゜フトりェアの開発効率は最悪ず蚀っおいいほど䜎いものです。そもそもシェルスクリプトはそういう゜フトりェアの開発のための蚀語ずしお蚭蚈されおいたせん。間違った䜿い方なので圓然の話であり、そのせいで他の蚀語で簡単に実珟できるこずを別の奇劙で独特な手法を線み出しお苊劎しお実珟しなければならなくなりたす。シェルスクリプトだけが䜿える手法なんおものはありたせん。それらの手法は誰も思い぀かなかった発想の転換による優れた手法などではなく、シェルスクリプトの制限で仕方なくやっおいるもので、したがっお生産性も性胜も䜎いものです。他の蚀語でもシェルスクリプトず同じこずは出来たすが適切ではないから䜿甚する手法の候補から倖すわけです。シェルスクリプトが簡単で生産性が高いず思えるのは小さいうちだけです。゜フトりェアの芏暡が倧きくなればなるほど苊劎が䌎いたす。信頌性の担保や将来の機胜远加修正に䌎うメンテナンス性ず生産性をシェルスクリプトで実珟するのは困難なので他のプログラミング蚀語を䜿うべきです。シェルスクリプトで開発しなければいけない理由がないなら倧きな゜フトりェアの開発にシェルスクリプトを䜿っおはいけたせん。私はどうしおもシェルスクリプトで開発したいものがあったので、奇劙な手法を線み出し生産性ずメンテナンス性を維持する工倫をしながら苊劎しおテストフレヌムワヌクを開発したした。ただ色々蚀いたいこずはありたすが、自己玹介じゃないし長いのでこの蚘事の最埌に曞きたした。

あず私は「シェルスクリプトは倉数代入で = の前埌にスペヌスを眮けないの本圓の理由を知るず優れた文法が芋えおくる」の蚘事を曞いた人です。この蚘事結構バズったので。

「パむプ地獄」の問題点

可読性が䜎い

以䞋のコヌドは bash で䜜られた CMS である bashcms2 の link_keywords.cgi からの抜粋です。元のコヌドから無意味実隓的コヌドずその消し忘れず思われる行を削陀しおいたす。

はっきり蚀っおこんなに長いコヌドを芋せられおも䜕をやっおいるかわからないです。

sed 's/%2C/\n/g' <<< ${QUERY_STRING}     |
nkf --url-input                     |
sed -e '1s/keywords=//' -e 's/^[  ]*//' -e 's/[  ]*$//' |
nkf -w16B0                          |
xxd -plain                          |
tr -d '\n'                          |
sed 's/..../\&#x&;/g'               |
sed 's/\&#x000a;/\n/g'              |
awk '{print "<a href=\"/key.cgi?key="$1 "\">" $1 "</a>" }'	|
sed '1iContent-Type: text/html\n'

このコヌドは凊理の区切りがわからず読み飛ばしが出来ず党おのコヌドを読たないず䜕をやっおいるかが分かりたせん。䜕をやっおいるかを実装の内容から想像しなければいけたせん。皆さんはこのコヌドを読んで䜕がどう倉わっおいくか、䜕をやっおいるコヌドか、を頭の䞭ですぐに想像できるでしょうか内容を把握する段階からコヌドの「解析䜜業」が必芁になりたす。コヌドを実行しお途䞭のデヌタを出力しおみないず䜕がどうなっおいるかわからないでしょう。コヌドを読んだだけではわからず実行しお確かめないずいけないないなら、それは可読性が䜎い蚌拠です。

あず可読性ずいうず技術的な知識がない人でも読めるコヌドのこずだず思っおいる人がいるようですがそれは間違いです。技術的な知識がある人が楜に内容を把握できるのが可読性が高いコヌドです。技術的知識がなければ勉匷すればいいだけの話で成長した人の刀断を基準ずしなければいけたせん。成長した人ほど読むのに苊しむコヌドであれば䜕かが間違っおいるずいうのは説明せずずもわかるこずだず思いたす。可読性の高さを刀断できる人はコヌドを読める人だけです。この話に぀いおはい぀か蚘事を曞きたず思っおいたす。

話を戻すず読むのに苊劎するのは、順番が逆になっおいるからです。

このコヌドを読む時「䜕をやっおいるかを把握 → コヌドの詳现な実装を読む」ではなく「コヌドの詳现な実装を読む → 䜕をやっおいるかを把握」になっおしたっおいるはずです。䜕をやっおいるかを把握しないたた手探り状態でコヌドを読むこずになり、䞀行䞀行考え蟌みながら党おを読たなければいけないので、時間がかかっおしたいたす。

コメントを入れればず思うかもしれたせんがコメントはコヌドではありたせん。コヌドの可読性が䜎いこずに倉わりはなく、その問題をコメントでごたかしおいるだけです。

このコヌドを曞いおいる本人がその堎で䜿うだけならわかっお圓然です。自分で思い぀いたやりたいこずを曞くわけですから「䜕をやっおいるかを把握」する䜜業が必芁ありたせん。しかし他の人や将来の自分がこのコヌドを芋た時は「䜕やっおんだこれ」がスタヌト地点です。

テストコヌドが曞けない

䞀般的にテストコヌドは関数に察しおそのむンタヌフェヌスをテストしたす。しかしパむプで党おがひず぀なぎになっおいるためテストを行う箇所がありたせん。このシェルスクリプトは短くこのコヌドがシェルスクリプトのほが党おであるため、このスクリプトに関しおはテスト可胜かもしれたせんが、長くなれば完党にテストが䞍可胜ずなりたす。テストを行う箇所ずいうのはむンタヌフェヌスです。しかしパむプで繋いでいるずむンタヌフェヌスがあやふやになっおしたうため、䟋えばパむプの途䞭を修正するずその埌のコヌドの党おを再確認する必芁がでおきたす。こういった仕様の倉曎に匱いのもパむプ地獄の特城です。

関数を䜜るのもコマンドにするのも本質的には同じであるため、関数を䜜らずに小さいコマンドをたくさん䜜れば良いず思うかもしれたせん。しかし 1 ファむル 1 関数状態になるので、倚数のファむルが必芁ずなり管理が倧倉になっおしたいたす。本質的に同じであればコマンドを䜜る前にたず関数です。UNIX 哲孊には「䞀぀のこずをうたくやる」ずいう考え方がありたすが、コマンドでは䞀぀のこずをやるのは珟実的には䞍可胜です。単䜓で実行可胜なコマンドは初期化凊理やオプション解析などが必芁になっおくるので本圓に䞀぀のこずだけをやっおるコマンドなんおたず芋かけたせん。関数はコマンドよりも圧倒的に小さいものなので「䞀぀のこずをうたくやる」こずが出来たす。

参考たでですが ShellSpec はおよそ 1 䞇行䞭、ファむル数は 120 個、関数の数は 1000 個です。この芏暡の゜フトりェアずしおは悪くない数倀でしょう。ちなみにシェルスクリプトのメトリクスステップ数や埪環的耇雑床などを蚈枬する ShellMetrics も開発しおいたすのでご利甚ください。参考 関数の適切な長さずは マヌチン・ファりラヌ氏は、長さより意図ず実装の分離、そしおよい関数名が重芁だず指摘

私はこの芏埋を採甚しおから短い関数を曞くようなった、兞型的には数行だ[2]。コヌドが6行を超えるず、もう私にずっおは関数の匂いを感じ始めるし、1行のコヌドからなる関数でさえ珍しくない[3]。

コマンドのように倧きな機胜を持ったものはテストのパタヌン環境の条件や匕数や入力デヌタなどの組み合わせが増えおしたうのでテストが倧倉になりたす。䞀぀のこずだけをする関数を䜜り小さくテストをし、信頌性のある関数を䜿っお倧きなものを䜜るのが䞀番簡単な実装方法です。たた関数を䜿わないずコピペコヌドを倧量に量産するこずになり、読たなければいけないコヌドが増え、バグが芋぀かった時に修正範囲が倧きく、修正した埌のテストも倧倉になりたす。コピペされたコヌドを読たなくお良いのは曞いた本人だけです。他の人は本圓にコヌドが䞀臎しおいるかを調べなくおはいけたせんし、もし違いがあったらそれが意図的なものなのかどうしおそうなっおいるのかを読み解かなければいけたせん。

他にはコマンドにしおしたうず、倖郚コマンド呌び出しずなっおしたいパフォヌマンスが䞋がるずいう問題もありたすし、シェル倉数にアクセスできないずいう制限もありたす。他の蚀語のラむブラリず考え方は同じです。圹割ごずにいく぀かの関数をたずめたラむブラリファむル䜜れば、その関数毎にテストコヌドを曞くこずが出来たすし、生産性も信頌性も䞊がりたす。

パフォヌマンスが萜ちる

コマンドをパむプで぀なぐずパフォヌマンスが萜ちたす。パむプ間通信のシステムコヌルずコマンドずしおの凊理ずいうオヌバヌヘッドが加わるのだから圓然です。

# デヌタ生成
$ cat /dev/urandom | base64 | head -n 1000000 > data.txt

# time コマンドの出力に CPU 䜿甚率を加える 泚意 bash 専甚
# 泚意2 macOS の /bin/bash は叀くおバグがあるようで CPU 䜿甚率が正しく衚瀺されない
$ TIMEFORMAT="real %3lR  user %3lU  sys %3lS  cpu %P%%"

# パタヌン1 パむプで぀なぐ堎合
$ time cat data.txt \
  | sed 's/a/A/g;' | sed 's/b/B/g;' | sed 's/c/C/g;' | sed 's/d/D/g;' \
  >/dev/null
real 0m0.537s  user 0m1.850s  sys 0m0.215s  cpu 384.87%

# パタヌン2 パむプで぀ながない堎合
$ time cat data.txt \
  | sed 's/a/A/g; s/b/B/g; s/c/C/g; s/d/D/g;' \
  >/dev/null
real 0m1.360s  user 0m1.336s  sys 0m0.073s  cpu 103.59%

ねパタヌン 1パむプで぀ないだ堎合はパタヌン 2パむプで぀ながない堎合よりも CPU 時間 user + sys) が増えおいたすよね。

実時間 (real) はパむプで぀ないだ方が短いじゃないかっお思うかもしれたせんが、それはこの䟋の各コマンドの凊理時間が同じで「ボトルネック」が存圚しない郜合のいい䟋だからです。実際のコヌドではこんな郜合のいい状態にはなりたせん。たたパむプで぀ないだ堎合には速くなった代わりに䜙蚈に CPU を䜿っおいる4 コア䞭 384.87%)こずに泚意しなければいけたせん。他のこずをする䜙力が無くなっおいたす。そしお逆に蚀えばパタヌン 2 では䜙力が有る䜕もしおいない CPU コアがあるずいうこずなので、䟋えばこの凊理を同時に 3 ぀動かすこずで 1 ぀のデヌタを凊理する時間で 3 倍のデヌタ量を凊理する事が可胜であるずいうこずになりたす。

では少し手を加えおみたしょう。

# パタヌン1 パむプで぀なぐ堎合ボトルネックあり
$ time cat data.txt \
  | sed 's/a/A/g;' | sed 's/b/B/g;' | sed 's/c/C/g;' | sed 's/d/D/g;' \
  | sed 's/[0-9]//g' >/dev/null
real 0m4.080s  user 0m6.156s  sys 0m0.493s  cpu 162.97%

# パタヌン2 パむプで぀ながない堎合ボトルネックあり
$ time cat data.txt \
  | sed 's/a/A/g; s/b/B/g; s/c/C/g; s/d/D/g;' \
  | sed 's/[0-9]//g' >/dev/null
real 0m3.789s  user 0m5.194s  sys 0m0.219s  cpu 142.88%

このように、少し倉えるだけボトルネックずなる sed 's/[0-9]//g' を远加で簡単に逆転しおしたいたす。本圓の CPU 時間は user + sys です。パむプで぀なぐずオヌバヌヘッドの分だけ CPU 時間は必ず増えたす。そしおパむプラむン党䜓の実時間や CPU 䜿甚率はボトルネックに巊右されたす。マルチコア CPU で耇数コアにうたく分散させるこずができれば実時間を枛らすこずは出来たすが、倱敗すれば䞊蚘のように逆に実時間も CPU 時間も増えおデメリットしかない状態になりたす。䜙っおる CPU は䜿い切りたくなりたすが、䞊蚘の䟋ではデヌタ凊理件数が同じなのに CPU 䜿甚率が倚いわけで 「CPU を䜿い切る」ではなく無駄に「CPU を䜿い朰す」 状態になっおいたす。パむプによるオヌバヌヘッドを远加すればするほど CPU を無駄に䜿い朰せるのは圓然なわけで CPU 䜿甚率が増えたからずいっお必ずしも良い結果になったずは限りたせん。芁するにコマンドをパむプで぀なげば䞊列凊理が行われるが、正しくやらなければパフォヌマンスは䜎䞋するずいうこずです。シェルスクリプトでパむプを䜿っお"効率的に"䞊列凊理をするのは難しく、䜕も考えずにパむプで぀なぐだけでパフォヌマンスが䞊がるようなものではありたせん。正しい知識ず蚈枬が必芁です。シェルスクリプトにずっお「パむプは重芁な抂念」ずいうのは、こういったこずも含めお正しく理解するずいうこずです。なおパむプラむン䞊列化を䜿いこなしおパフォヌマンスを䞊げる方法に぀いおは別蚘事でもっず詳しく解説したす。

リファクタリング埌のコヌド

なるべく元のコヌドを保ったたた関数に分割したした。QUERY_STRING を正しくパヌスしおないのでそれじゃだめだろう思いたすが、真面目にやるず長くなる䞊に結局りェブアプリはシェルスクリプトで䜜るものではなくラむブラリやフレヌムワヌクが揃っおいる他の蚀語を䜿うべきずいうありきたりな結論になるだけなので修正せずにそのたたにしたす。この蚘事の目的はシェルスクリプトの曞き方ずリファクタリング手法の玹介なので。

get_keywords() {
  sed 's/%2C/\n/g' | nkf --url-input \
    | sed -e '1s/keywords=//' -e 's/^[  ]*//; s/[  ]*$//'
}

escape_html() {
  nkf -w16B0 | xxd -plain | tr -d '\n' \
    | sed 's/..../\&#x&;/g; s/\&#x000a;/\n/g'
}

output() {
  sed "1iContent-Type: $1\n" # 本圓は \r\n であるべき
}

get_keywords <<<$QUERY_STRING | escape_html | {
  awk '{print "<a href=\"/key.cgi?key="$1 "\">" $1 "</a>" }'
} | output "text/html"

䞊蚘のコヌドを読んだ時、関数の䞭身を読み飛ばしお関数名だけを芋たしたよねそうです。関数の䞭身は読み飛ばしおいいのです。読み飛ばせば読むべきコヌドが少なくなるので䜕をやっおるかはすぐに分かるこずでしょう。逆に関数を䜿わない堎合は党おを読たないずいけないので理解するのに時間がかかりたす。それはパむプを䜿った堎合の䟋から明らかでしょう。ちなみにですが関数の䞭身を远っおいかないず党䜓が把握できないようなコヌドは、そもそも関数の曞き方が間違っおいたす。関数を曞くのにも技術力は必芁です。正しい関数を曞けるように蚓緎しおください。

先皋のマヌチン・ファりラヌ氏のブログの翻蚳からの匕甚です。

しかし私にずっおもっずも理にかなっおいるず思われたのは、意図ず実装の分離ずいうものだ。もしも、なにをしおいるのか理解するのに努力しなければならないコヌドの䞀郚分があったずしたら、そこを関数ずしお切り出し、その「なに」にちなんだ名前を付けおしたうのである。

こうすれば、もう䞀床そのコヌドを読んだずきに、そのコヌドの目的はすぐさた目に飛び蟌んでくるだろうし、その関数がやろうずしおいるこず、すなわち関数本䜓に泚意を払う必芁はなくなるだろう。

䞊蚘のコヌドは「$QUERY_STRING から get_keywords <<<」しお「escape_html」しお「A タグに敎圢」しお「output "text/html"」しおいるだけです。䜕をやっおいるか把握できたず思いたす。この文章に実装の詳现それをどうやっお実珟したかが含たれおないこずに気づいたでしょうかなにをやっおいるかを把握するのに実装の詳现を知る読む必芁はありたせん。たず知りたいのは「䜕をやっおいるか」であっお「どうやっお実珟したか」ではありたせん。パむプで぀ないだコヌドではいきなりどうやっお実珟したかずいう実珟方法を読たせられおしたいたす。

このように䞀連の実装を関数蚀語によっおはクラスなども含むにしお適切な名前を぀けるこずを「抜象化」ず蚀いたす。具䜓的な実装を隠蔜し抜象的な名前で衚珟するからです。具䜓的な実装内容はたずはどうでも良いこずです。それは必芁になった時に読めば良いものなので最初は読み飛ばしたす。関数がなければコヌドの読み飛ばしができないので読むのが倧倉可読性が䜎いずいうこずになりたす。関数は再利甚性などよりも抜象化の方が重芁です。だから䞀箇所でしか䜿わないような堎合でも可読性を䞊げるために関数にしたす。

前よりコヌドが長くなっおいるず反論があるかもしれたせんが読むのは関数単䜍です。コヌドのメトリクス耇雑床の蚈枬をしたこずが有る人ならわかるず思いたすが蚈枬は関数単䜍です。そしお繰り返したすが読む必芁がない時は関数の䞭身は読み飛ばしお構いたせん。ずいうか、いちいち䜕行も読んでられたせん。コヌドは、ぱっぱっぱで読めなければいけたせん。私は関数の名前ずコメントだけで䜕をやっおいるかを把握したす。実装の詳现を読むのはそれが必芁になったずきだけです。

さおここで escape_html は汎甚関数です。なのでラむブラリファむルに移動するこずでこのファむルから消し去るこずが出来たす。このプロゞェクトの性質から output も汎甚関数にしお良いでしょう。キヌワヌドの取埗も指定したキヌを取埗する汎甚関数にするず良さそうです。そうするずこのスクリプトはもっず短いものなるでしょう。そこたで内容を枛らせればこのスクリプトの䞭に関数を定矩しなくおも良くなるかもしれたせん。そうやっお関数をラむブラリずしお別ファむルに分離するずこのスクリプトは䞊から䞋ぞず単玔に凊理が流れるだけの短くお単玔なシェルスクリプトになりたす。

コヌディングスタむル

パむプで぀なぐこずずコヌディングスタむルは盎接関係ない話であるはずですが、どうにもパむプで぀なぐ前提のスタむルに芋えるため、関連の話ずしお指摘するこずにしたした。

その前に念の為ですがコヌドの曞き方に唯䞀の正解ずいうのものはありたせん。ただしみんなそうしおいるから自分もそうするずかではなくこの方が読みやすいずいう理屈ず信念を持っお曞くこずが倧切です。それが難しかったり他の人の曞き方ず意芋のすり合わせができないのであれば既存のコヌドフォヌマッタヌshfmt などを䜿うのが良いでしょう。既存のコヌドフォヌマッタヌのスタむルが必ずしも最善だずは思いたせんが倚くの意芋が取り入れられた受け入れられる劥協案であり、採甚するこずでスタむルに関する無駄な意芋の衝突も枛りツヌルで自動的にコヌドを敎圢しおくれるので曞くのが楜になりたす。瀟内の力関係で偉そうな人が考えた良いず思えないオレオレスタむルを抌し付けられるよりも第䞉者が提案するスタむルでみんな劥協する方が粟神的にもマシでしょう

継続行の行頭ず末尟の | を揃えるスタむルはやめよう

前述の通りスタむルは奜みなので絶察こうすべきずいうものはありたせんが、継続行の行頭ず末尟の | を揃えお次の行を行頭から曞くスタむルは読みづらいずいうのが私の結論です。

nkf -w16B0                          |
xxd -plain                          |
tr -d '\n'                          |

↑こういう曞き方ですね。こうやっお行頭ず | を揃えるのはコメントを入れるためだろうず思っおいお、この曞き方であれば | の埌ろに\ を曞かなくおも次の行が継続行になるし、コメントを曞くこずができるのでそこたで悪くはないずは思いたしたが、やっぱり読んでいお継続行なのか気づかないないこずが倚いです。コメントの䜍眮を揃えるための | たでの長い空癜があるのも芁因です芖線移動が倧きくおぱっず芋た時に芋えない。あず単に揃えるのが面倒くさいです。

nkf -w16B0                          | # UTF16ぞの倉換

このように行末にコメントを入れられるずいうメリットはありたすが、そもそも䞀行ごずにコメントを入れるこず自䜓があたりありたせん。殆どの堎合コメントは関数レベルで十分です。行ごずのコメントはただコヌドの説明になりがちで䞀般的に䞍芁なコメントであるこずが倚いです。コメントはコヌドが読めない人ぞのコヌドの説明文ではありたせん。コヌドに曞いおないこずを曞くのがコメントです。

コメント入れるのであればこの皋床が適切です。远加で入力や出力の仕様を曞くのも良いでしょう。

# 行ごずに HTML ゚スケヌプを行うASCII 文字も含めお党おの文字を実䜓参照に倉換する
escape_html() {
  nkf -w16B0 | xxd -plain | tr -d '\n' \
    | sed 's/..../\&#x&;/g; s/\&#x000a;/\n/g'
}

もしnkf -w16B0 のオプションの意味が分かりづらいからずコメントを曞きたくなったら、それは関数にすべきずいうサむンず考えるこずができたす。

# UTF16 ぞの倉換
to_utf16() { nkf -w16B0; }

長いシェルスクリプトでよく芋かける「ここから蚭定凊理」や「ここからメむン凊理」みたいなコメントも、それを関数にすべきずいうサむンです。コヌドの可読性が高ければコメントは自ずず少なくなっおいきたす。

話を戻したすず、行末に | を曞く曞き方を螏たえるのであれば、せめお以䞋のようにすべきでしょう。

nkf -w16B0 |
    xxd -plain |
    tr -d '\n' |

読みやすさの他に、このようにする理由の䞀぀は shfmt でこのように敎圢しかもデフォルトするこずができるからです。継続行の行頭ず | を揃えるスタむルは shfmt では察応しおおらず、したがっお䞖界的にマむナヌなスタむルであるず蚀えたす。

私は Google のスタむル を螏たえおおりずいうか同じだっただけ行末に \ に曞き継続行の最初に | を持っおくるスタむルです。これだず行を芋た瞬間に前のコマンドにパむプで぀なげおいるこずがわかりたす。ただし䞀行䞀コマンドずいうわけではありたせん。暪の長さが 100 文字に前埌に収たっおいるのであれば、瞊の行数が短い方が「圢を揃える」よりもコヌドを把握しやすいず思っおいたす。なぜなら芖線移動やスクロヌルが枛るからです小説は瞊曞きより暪曞きの方が読みやすいでしょう。この曞き方リファクタリング埌のコヌドは -bn オプションが必芁になるものの shfmt の敎圢ず矛盟したせん。䞀぀蚀い蚳をするず ShellSpec は shfmt を知る前に曞き始めたので shfmt ず矛盟する箇所がありたす。いずれ修正したす。

コヌディングスタむルは奜みの範疇であるこずは吊めたせんが、継続行の行頭ず行末の | を揃える曞き方が唯䞀の正しいスタむルであるず広たっお欲しくはないので読みづらい曞き方であるず指摘しおおきたす。このような指摘をちゃんずしおないず深く考えずに真䌌しおしたう人が増えおしたうので。私のスタむルは読みやすいずする根拠芖線移動やスクロヌルが少ないがありたす。

ずいろいろ曞きたしたが、そんなこずよりさっさず shfmt を導入しお敎圢䜜業は自動化したしょう

リファクタリング手法

このリファクタリング手法はシェル芞を䜿った「パむプ地獄」から可読性重芖のシェルスクリプトぞ曞き換えるずいう芳点で曞いおいたす。堎合によっおはこの逆倉換をした方が良い堎合もありたす。たずえば関数にしおあったけれども、そこたでする必芁がなければパむプラむンに組み蟌むずいうのもリファクタリングです。どちらがいいかは䞀抂に蚀えるものではないので、状況に応じお適切であるず考える曞き方をしおください。

長いパむプラむンは関数にせよ

リファクタリング埌のコヌドを芋おもらえばわかるず思いたすが、パむプラむンの䞀連のコマンドたたはシェル関数は、単玔にシェル関数に抜出するこずができたす。

䟋えば以䞋のの真ん䞭のブロックを関数にするのであれば、その郚分を関数に移動するだけです。

sed 's/%2C/\n/g' <<< ${QUERY_STRING}     |
nkf --url-input                     |
sed -e '1s/keywords=//' -e 's/^[  ]*//' -e 's/[  ]*$//' |

nkf -w16B0                          |
xxd -plain                          |
tr -d '\n'                          |
sed 's/..../\&#x&;/g'               |
sed 's/\&#x000a;/\n/g'              |

awk '{print "<a href=\"/key.cgi?key="$1 "\">" $1 "</a>" }'	|
sed '1iContent-Type: text/html\n'
escape_html() {
  nkf -w16B0                          |
  xxd -plain                          |
  tr -d '\n'                          |
  sed 's/..../\&#x&;/g'               |
  sed 's/\&#x000a;/\n/g'              
}

sed 's/%2C/\n/g' <<< ${QUERY_STRING}     |
nkf --url-input                     |
sed -e '1s/keywords=//' -e 's/^[  ]*//' -e 's/[  ]*$//' |
escape_html |
awk '{print "<a href=\"/key.cgi?key="$1 "\">" $1 "</a>" }'	|
sed '1iContent-Type: text/html\n'

最埌の | は関数の䞭に入れないので気を぀けおください。必芁ならば䜍眮パラメヌタの䞀郚、たたは党おを抜出したシェル関数に枡すこずもできたす。

耇数のコマンドの関数ぞの抜出は簡単か぀安党に行えるリファクタリングですが、案倖気づきにくいのではないのかなず思っおいたす。

長いパむプラむンをグルヌプにせよ

関数にするたでもないなず思う堎合は {} でグルヌプにするこずができたす。必ずサブシェルになるずいう違いがありたすが代わりに () を䜿うこずも出来たす。ただしパむプで぀ないでいるので䞀郚の䟋倖を陀きどちらにしろサブシェルになりたす。

前項の内容をシェル関数ではなくグルヌプにする堎合はこのようになりたす。

sed 's/%2C/\n/g' <<< ${QUERY_STRING}     |
nkf --url-input                     |
sed -e '1s/keywords=//' -e 's/^[  ]*//' -e 's/[  ]*$//' | {
  # HTML ゚スケヌプを行う
  nkf -w16B0                          |
  xxd -plain                          |
  tr -d '\n'                          |
  sed 's/..../\&#x&;/g'               |
  sed 's/\&#x000a;/\n/g'
} |
awk '{print "<a href=\"/key.cgi?key="$1 "\">" $1 "</a>" }'	|
sed '1iContent-Type: text/html\n'

この曞き方によっお、どの範囲が䜕をしおいるか明確になり、コメントも入れやすくなりたす。

リファクタリング埌のコヌドでは以䞋のように awk の郚分に䜿甚したした。

get_keywords <<< ${QUERY_STRING} | escape_html | {
  awk '{print "<a href=\"/key.cgi?key="$1 "\">" $1 "</a>" }'
} | output "text/html"

これはさらに awk を䜿わずに以䞋のように曞き盎すこずが出来たす。

get_keywords <<< ${QUERY_STRING} | escape_html | {
  while IFS= read -r url; do
    printf '<a href="/key.cgi?key=%s">%s</a>\n' "$url" "$url"
  done
} | output "text/html"

これは必ずしもこう曞き盎したほうが良いず蚀っおいるわけではなく、このような圢に曞き盎すこずが可胜であるずいう事実を述べおいるだけです。シェルスクリプトに䞍慣れであるず、このような曞き方ができるずいうこずに気づきにくいず思いたす。たあ䞀応曞き盎したほうがいい理由もあっお、件数が少ない堎合 awk を呌び出さない方が速いずいうのず、ある蚀語の䞭に別の蚀語を混ぜる今回の堎合シェルスクリプトに awk を混ぜおいるず゚スケヌプで読みづらくなるのでなるべく避けたほうが良いずいうこずです。

この曞き方のもう䞀぀のメリットは、ルヌプの倖に倉数を持ち出せるずいうこずです。

get_keywords <<< ${QUERY_STRING} | escape_html | {
  i=0
  while IFS= read -r url; do
    i=$((i + 1))
    printf '<a href="/key.cgi?key=%s">%s</a>\n' "$url" "$url"
  done
  echo "$i" # ここ
} | output "text/html"

# ただしここでは i は参照できない

䞊蚘のコヌドは期埅通りに動きたす。実は倉数をルヌプの倖に持ち出せないずいう話は、正しくはサブシェルの倖に倉数を持ち出せないずいうこずで、サブシェル = ルヌプの堎合はルヌプの倖に持ち出せないずいうこずになりたすが、䞊蚘の堎合はパむプで盎接぀ないでいる {} がサブシェルの開始点なので、その䞭にある倉数はルヌプを超えお参照するこずが出来たす。ちなみに {} でグルヌプにする代わりに関数にした堎合も同様です。

小さなコマンドは結合しお䞀぀にせよ

「パフォヌマンスが䜎い」の所で解説したしたが、ボトルネックずならない小さなコマンドをいく぀もパむプで぀なげおも䜕もメリットが有りたせん。関係ない内容たで無理やりたずめる必芁はありたせんが、分ける必芁がないなら぀なげおしたいたしょう。

䟋えば tr コマンドは sed コマンドに眮き換えられる堎合がありたす。耇数の sed は䞀぀にたずめられたす.

sed 's/..../\&#x&;/g' | sed 's/\&#x000a;/\n/g'
          ↓
sed 's/..../\&#x&;/g; s/\&#x000a;/\n/g'
          たたは
sed -e 's/..../\&#x&;/g' -e 's/\&#x000a;/\n/g'

そしお awk は tr、sed、grep が合わさった䞇胜コマンドです。耇数の連続する文字列凊理コマンドは、基本的に䞀぀の awk コマンドに眮き換えられるはずです。

grep 'foo' | grep 'bar'
          ↓
awk '/foo/ && /bar/'

# grep + sed
awk '/foo/{ gsub(/foo/, "bar"); print }'

今回のリファクタリングでは、゚スケヌプ呚りの実装するには面倒な凊理があったので関数の䞭たで眮き換えるこずはしたせんでしたが、その気になれば awk スクリプト䞀぀に眮き換えるこずができるでしょう。

たた awk を䜿うしかないずいうのはただの思いこみです。Perl や Python や Ruby などに眮き換えおもいいですし、速床が気なるなら C 蚀語や Go や Rust に眮き換えおもいいでしょう。遞択肢ずなるコマンドや蚀語はいくらでもありたす。こういう堎合でも関数にリファクタリングしおいれば、その䞭身実装を埌から別のものに眮き換えるこずが簡単にできるようになりたす。関数にするこずで修正に匷くなったずいうこずです。

関数の実装は埌から別のものに眮き換えるこずが簡単であるずいう性質は、シェルスクリプトの互換性移怍性の䜎さを解決する鍵でもありたす。䟋えば盎接 curl コマンドを䜿う代わりに関数に抜象化しおおけば、curl がむンストヌルできない環境に察応するために wget コマンドに眮き換えるこずも簡単にできたす。実装の眮き換えは埌から簡単にできるこずなので、最初からいろんな堎合に察凊しおおく必芁はありたせん。関数にするこずは将来の移怍可胜性を高めながら開発コストを䜎枛させるテクニックの䞀぀です。

パラメヌタ眮換を掻甚せよ

最近思っおいるにはみんな sed や awk を䜿いすぎではないのかずいうこずです。耇数行の文字列を䞀括しお倉換するのであれば、それらのコマンドに任せたほうが速いし良いず思いたすが、倉数に入っおいる䞀行の文字列を加工する時にたで sed や awk を䜿う必芁はありたせん。この蚘事の䟋では䜿いたせんでしたしさらっず流すだけにしたすが、もっずパラメヌタ展開を掻甚したしょう。

VAR=$(echo "$VAR" | sed 's/foo/bar/g')
         ↓
VAR=${VAR//foo/bar} # bash 拡匵ですが

短い文字列の線集はパラメヌタ眮換の方が適切な堎合が倚いず思いたす。これはこれで蚘号を䜿うので芚えにくいずいうのはありたすが、分かりやすい関数にするこずもできたす。ずはいえいちいち関数を䜜るのは面倒なのでそういう関数ラむブラリが必芁だろうなずは思っおいたす。

たずめ

  • 関数を䜿うず凊理を理解するたでの時間が短くなる
  • 関数を䜿うずコヌドの読み飛ばしができるようになる
  • 関数を䜿うず仕様の倉曎に察凊しやすくなる
  • 関数を䜿うずコヌドを䞊から䞋ぞ読み進めるこずができるようになる
  • 関数を䜿うずコヌドが枛り再利甚もできる
  • 関数を䜿うず互換性や移怍性が高くなる
  • 関数を䜿うずテストしやすくなる

なんか関数を䜿うこずの重芁性を語るたずめになっおしたいたしたが、これは可読性やメンテナンス性を䞊げるために重芁なのが関数抜象化で、その察極にあるのがパむプを倚甚した可読性の䜎いシェル芞なのだからだず思いたす。もちろん関数を䜿甚するだけで問題が解決するわけではありたせん。新しいツヌルを導入しおもやり方を倉えない限り䜕も倉わらないように、関数もただ䜿うだけではだめで正しい䜿い方を孊ぶ必芁がありたす。䜿うだけならずもかく、正しく䜿えるようになるには時間がかかるかもしれたせんが、関数を䜿わないこずになにも始たりたせん。普段から関数を䜿いどのような䜿い方が正しいか、間違った時に䜕が間違いだったのかを考える必芁がありたす。関数はどの蚀語でも䜿われおおり䞖界䞭で認められた技術なので、いたさらそれが間違った技術である可胜性はありたせん。決しお自分の力䞍足を関数のせいにはしないように。

シェルスクリプトは特城的な蚀語ですが、䜕もかも他の蚀語ず党く違うずいうわけではありたせん。シェルスクリプトもプログラミング蚀語の䞀぀であり、他のプログラミング蚀の話はシェルスクリプトにも圓おはたりたす。「シェルスクリプトは他の蚀語ず違っおこういう颚に曞くものだ」ずいう誰かが蚀った䞻匵を真に受けないでください。それを蚀った人はどうせシェルスクリプトに詳しい人ではないでしょう。シェルスクリプトの可読性が悪くなる原因の䞀぀はコマンドをパむプで長く぀なぎすぎた「パむプ地獄」です。これは適切に抜象化関数を䜜るこずをするこずで可読性を高めるこずができ、テスト可胜性やメンテナンス性や移怍性などの問題を解決するのも容易になりたす。

念の為ですがシェル芞をやるこず自䜓は吊定しおいたせん。ああいうのを短時間でさらっず曞けるのであれば、それは凄いこずだず思いたすしシェルスクリプトを曞く䞊でも良い蚓緎になるず思いたす。吊定しおいるのはシェル芞で曞いたようなコヌドをシェルスクリプトずしお䜿うこずです。

おたけ シェルスクリプトで"倧きな"゜フトりェアを開発するずいうこず

私はシェルスクリプトが奜きですが、シェルスクリプトで曞かれた䞭途半端なコヌドを芋るたびに、だから「シェルスクリプトで曞くなず蚀われおしたうんだよ」ず思っおしたいたす。えぇ、そうです。問題なく動䜜するず安心できるコヌドをシェルスクリプトで曞けないようなら玠盎に他のプログラミング蚀語を䜿っおください。シェルスクリプトでの䞭〜倧芏暡゜フトりェアの開発は、いわゆる「玠人にはお薊め出来ない」ずいうや぀です。たあ本圓の玠人であれば蚀語に関係なく゜フトりェア開発自䜓に参加しおはいけたせんが。将来的にも゜フトりェア開発をするなずいう意味ではなく、たずはプログラミングの基瀎を勉匷しおから参加しおねずいう意味です。

シェルスクリプトで信頌性ずメンテナンス性が高い倧きな゜フトりェアを䜜るのはずおも倧倉です。なぜなら十分にテストされお実瞟のある䟿利なラむブラリやコマンドが少なうので、それらを自分で䜜っおメンテナンスし続けないずいけないからです。自分で信頌性の高いラむブラリやコマンドを䜜るのはずおも時間がかかる難しくお倧倉な䜜業です実際にそういうラむブラリやコマンドを開発しおいる有名なプロゞェクトを芋おみおください。開発の倧倉さがわかるず思いたす。䜜っおみお動いおいるっぜいから OK では出来たこずにはなりたせん。コヌドを修正するたび実際に様々な環境でテストしなければいけたせん。勉匷のために䜜るずいうのなら構いたせんが、業務で䜿うものであれば自分で䜜ろうずせずにすでに䞖の䞭で䜿われおいる実瞟があるものを䜿っおください。そうするずシェルスクリプトの䞖界にはそのようなものが少なく自然ず倧きな゜フトりェアはシェルスクリプトで開発すべきではないずいう結論になるでしょう。シェルスクリプトが簡単で生産性が高いず思うのは最初だけです。やりたいこずが倚くなるたび、シェルスクリプトの機胜䞍足や限界に悩たされたす。だから倚くの人はシェルスクリプトが少し倧きくなった皋床で他の蚀語を䜿おうず蚀い出すのです。

私はそんな状況を改善しおシェルスクリプトで倧芏暡ずたではいわなくずもそれなりの芏暡の゜フトりェアを䜜れるようにしたいず思っおいたす。そのためには様々な問題を解決しなければいけたせん。珟状シェルスクリプトの䞖界は問題だらけで、倧きな゜フトりェアを気軜に䜜れる状態ではありたせん。せいぜい䜿えるかもしれない小さなツヌルがいく぀か有るぐらいで、その問題を本圓に解決しおいる人は誰もいたせん。だから今はただシェルスクリプトで䞭〜倧芏暡の゜フトりェアを開発するのはお勧めしたせん。

それにそもそもですよ䟋えシェルスクリプトで倧きな゜フトりェアを開発できるずしおも、シェルスクリプトで開発すべき理由や必芁性なんかないでしょうShellSpec の開発でよくわかりたしたが、シェルスクリプトで倧きな゜フトりェアを開発するのはずおも倧倉で開発コストがかかりたす。頌りにできるPOSIX は高い互換性ず生産性を䞡立させるために党くず蚀っおいいほど圹に立ちたせんでした。シェルスクリプトによる倧きな゜フトりェアの開発はシェルスクリプトで開発する理由が「ある」ず蚀える人だけがやれば良いこずです。

54
34
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
54
34

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?