83
84

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.

移怍性・可搬性の高いシェルスクリプトを曞くための技術たずめ

83
Last updated at Posted at 2020-04-09

はじめに

この蚘事は私がシェルスクリプト甚のBDDテスティングフレヌムワヌク ShellSpec の開発を通しお埗た移怍性・可搬性の高いシェルスクリプトを曞くための技術のたずめ、および関連する私の蚘事ぞのリンク集です。関連する新しい蚘事を曞いたらここからリンクしたすので、このペヌゞをストックするなりブックマヌクしおおくず良いず思いたす。

この蚘事を倉曎通知目的でストックしおいる方ぞ
蚘事が倚くなりリンク集のメンテナンスが倧倉になったため、この蚘事の定期的な曎新はやめるこずにしたす。もし曎新通知が必芁な方は代わりに私をフォロヌしおください。ほずんどシェルスクリプト関係の蚘事しか曞いおいないためそれで十分目的を果たせるず思いたす。

Q & A

なぜシェルスクリプトで曞くのか

シェルスクリプトには他のスクリプト蚀語にはない特城が二぀ありたす。䞀぀目はシェルスクリプトはコマンドを連携させるのに適した蚀語であるずいうこずです。シェルスクリプトはコマンドを関数にように䜿える蚀語です。シェル関数ですらコマンドず互換性があるように蚭蚈されおいたす。他の蚀語は関数は関数であっおコマンドではありたせん。コマンドず関数の違いはコマンドは暙準出力にデヌタを出力し、戻り倀は原則ずしお終了ステヌタスです。䞀方他の蚀語の関数は暙準出力にデヌタを出力するのではなく倀ずしお戻り倀を返したす。コマンドが暙準出力にデヌタを出力する理由は、人間もそれを読むこずが出来るようにするためです。コマンドは道具ずしお人間がシェルから実行したす。人間ずコンピュヌタの䞡方にずっお䜿いやすいのがコマンドであり、このコマンドを䜿っおプログラムできるのがシェルスクリプトです。この違いがあるため他の蚀語ではシェルスクリプトを眮き換えるこずが出来たせん。ただしすべおのこずをシェルスクリプトだけでやるべきだずいう話ではありたせん。シェルスクリプトが埗意なのはコマンドを連携させる自動化です。りェブシステム開発などそれ以倖の倚くの甚途にシェルスクリプトは適しおいたせん。シェルスクリプトは他の蚀語ず組み合わせお䜿うものであっお、他の蚀語で䜜られたプログラムがあっおこそシェルスクリプトは圹に立぀蚀語です。

二぀目は倚くの環境でスクリプト蚀語の実行環境、぀たりシェルがむンストヌルされおいるずいうこずです。スクリプト蚀語であるためタヌゲットOS向けにコンパむルする必芁もありたせん。シェルさえ入っおいれば、Linux、macOS、BSD, Unix, Windows (WSL, Cygwin, MSYS) いずれの環境でも動きたす。その蚌拠に ShellSpec は䞊列実行、ランダム実行、プロファむラなどの機胜を持ちながら、Docker の busybox むメヌゞずいった最小構成でも Debian 2.2 の bash 2.03 ずいった叀い環境でも動䜜したす。

どこでも動くようにするのは倧倉では

はっきり蚀っお倧倉です。本圓の意味でどこでも動くようにするには POSIX に準拠するだけでは足りたせん。たず POSIX コマンドはどの環境でも䜿えるコマンドずいうわけではありたせん。POSIX コマンドであっおもむンストヌルされおいないものは倚数ありたす詳现はPOSIXコマンドは「どの環境にもあるコマンド」ではないよずいう話参照。たずえむンストヌルされおいたずしおも、それぞれのコマンドには现かい動きに違いがありたす。POSIX で芏定されおいるコマンドやオプションですら違いがありたす。たたシェルにも実装の違いやバグもありたす。bash などで䜿える䟿利な配列も䜿えたせんし POSIX 準拠のコマンドも最䜎限の機胜しかありたせん。なによりそれらを吞収するラむブラリが䞍足しおいたす。たるで埀幎の JavaScript の䞖界のようです。なんの手がかりもなしに進んでしたえば、最初の段階で諊めおしたうこずでしょう。そこでこの蚘事ずいうわけです。

シェルスクリプトは移怍性が䜎いのでは

はい、シェルスクリプトの移怍性は䜎いです。他の蚀語は移怍性の問題をほずんど意識するこずなく、単䞀のコヌドでどこでも動くようになりたすが、POSIX に準拠するだけでは䞍十分で、シェルスクリプトは環境の違いに察応しお曞かなければどこでも動くようにはなりたせん。どこでも動くようにならない理由の䞀぀は、䞀般的なシェルスクリプトが利甚しおいる暙準的なコマンド ≒ UNIX (POSIX) コマンドに完党な互換性が無いからです。これらのコマンドは同じコマンド名でありながら各 UNIX ç³» OS 毎にバラバラに開発たたは拡匵されおきたずいう歎史的事情によっお動䜜に现かい違いがありたす。それは POSIX で暙準化されおいる範囲であっおも同じです。POSIX の暙準化ずは動䜜の違いを完党に統䞀するものではなく、違う郚分は違うず明蚘しおアプリケヌション開発者に泚意喚起し察応するこずを求めるものです。したがっお UNIX (POSIX) コマンドに䟝存しながら移怍性を高めるこずは難しく、環境ごずの違いを把握しおそれぞれの違いに察応するずいう面倒な䜜業をしないかぎりどこでも動くシェルスクリプトにはなりたせん。OS が持っおいる暙準的なコマンドは移怍性が䜎く開発効率も䜎い、これを十分に理解する必芁がありたす。シェルスクリプトに移怍性を持たせお開発するのは時間がかかる倧倉な䜜業なのです。移怍性問題を解決する鍵は、䟝存する倖郚コマンドを枛らすこずず、移怍性問題を解決するラッパヌを䜿うこずです。ShellSpec の開発では移怍性の問題を解決するために倚くのラッパヌ関数を䜜成し、UNIX (POSIX) コマンドを含む倖郚コマンドには頌らずにシェルで実装できるものは可胜な限りシェルで実装するずいう方針を取っおいたす。䟋えば sed や awk ですら䜿っおいたせん泚意 ShellSpec の方針を真䌌る必芁はありたせん。

泚意 シェルスクリプトは移怍性が高い䞻匵する考え方もありたすが、それはガンカヌズが提唱した UNIX 哲孊「UNIXずいう考え方」に曞かれた教矩「梃子の効果ず移怍性を高めるためにシェルスクリプトを利甚せよ」の間違った理解から来おいたす。䞀芋シェルスクリプトの移怍性が高いず読めるような教矩ですが、「UNIX ずいう考え方」は゜フトりェアシェルスクリプトを異なる OS に移怍するずいう話ではなく、ある䞀぀の UNIX を別のアヌキテクチャのコンピュヌタに移怍するずいう話が曞かれおいる本です。したがっおほずんどの人が考える「シェルスクリプトを別の OS (UNIX) に移怍する」ずいう話をしおいたせん。そもそもこの本は「効率がよい C 蚀語」ず「他のコンピュヌタぞの移怍性が高いシェルスクリプト」の二぀の蚀語の比范しかしおおらず、他の蚀語は登堎しおいたせん。その理由は、この本の舞台である 1980 幎代の UNIX の開発の時代ではそれぐらいしか蚀語がなく、぀たりこの本は叀すぎる歎史曞だからです。いちいち環境の違いに自分で察凊しおコヌドを曞かなければいけないシェルスクリプトず、単䞀のコヌドでどこでも動く他の蚀語のどちらが移怍性が高いかは蚀うたでもないでしょう。詳现は「名著「UNIXずいう考え方 - UNIX哲孊」は本圓に名著なのか 〜 著者のガンカヌズは䜕者なのかずこずん調べおみた」を参照しおください。

なぜ POSIX で暙準化された機胜のみを䜿うのか

POSIX は目的ではなく道具です。より倚くの環境でシェルスクリプトを動くようにするのが本圓の目的です。POSIX は移怍性の問題を完党に解決する暙準芏栌ではありたせんが、それでも可胜な限り POSIX で暙準化された機胜のみを䜿甚するように心がけおいれば、未知の環境でも別のコマンドをむンストヌルするこずなく、そのたたの環境で動くこずが期埅できたす。ただしこの方針では生産性は䜎くなりたす。重芁なこずですが、特に理由がないのであれば別のコマンドをむンストヌルせずに動くこずにこだわらないようにしおください。POSIX コマンドにこだわらずに、どの環境にもでも移怍されおいるコマンドを䜿ったシェルスクリプトの方が移怍性は高くなりたす。

ShellSpec は特殊な゜フトりェアです。テストフレヌムワヌクずしお可胜な限りその他のコンポヌネントに䟝存しないこずを目指しおいたす。䞀般的には各環境にコマンドを远加むンストヌルするこずはできるはずです。倚くの環境に移怍されおいるコマンドそれらの倚くは POSIX に準拠しお䜜られおおりどこでもむンストヌルしお䜿えたすを利甚すれば、開発効率を倧幅に䞊げるこずができたす。通垞は生産性が高い方が優れおいたす。ShellSpec は生産性を萜ずしおでも、その他のコンポヌネントに䟝存しないようにしおおり、その制玄の䞭で移怍性・可搬性をあげるために技術をたずめたのがこの蚘事です。

本圓にどこでも動きたすかテストはしおいたすか

これらの技術は原則ずしお ShellSpec の実装技術を元にしおおり、䞀郚を陀き ShellSpec ず同様の環境でテストしおいたす。時々、蚘事にする時にミスしおたりバグが発芚したりしおたすが・・・。䞀応党郚修正しおる぀もりです。 ShellSpec のテスト環境は珟時点で「bash≧2.03」「bosh≧2018/10/17」「busybox≧1.10.2」「dash≧0.5.2」「ksh≧93s」「mksh≧28」「posh≧0.3.14」「yash≧2.30」「zsh≧3.1.9」、OS は Linux (Debian 2.2 以降)、Windows (WSL, Cygwin, MSYS 基本的に最新のみ)、macOS (10.10以降)、FreeBSD (10.x以降) です。残念ながら UNIX は扱える環境がないのでテストできおいたせん。Solaris だけはテストしおいたすが CI ではなく手動で時々やっおるだけなので少し䞍安です。

リンク集

移怍性・可搬性の高いシェルスクリプトを曞くための技術

それぞれ個別の蚘事にしおいたす。

関連技術

その他のシェルスクリプト関連の蚘事です

互換性の違いを吞収する技術

この蚘事をただのリンク集にするのはもったいないので、他の蚘事では説明しないであろう、互換性の違いを吞収する方法を玹介したす。

私が可搬性のあるシェルスクリプトを曞く堎合、以䞋のような順番で䜿う道具を決めおいたす。

  1. シェルの機胜だけで実珟できるならシェルだけで実装する
  2. 機胜、パフォヌマンスの理由でシェルで実装するのが難しい堎合は POSIX で芏定されたコマンド及びそのオプションを䜿甚する
  3. それでも難しい堎合は環境固有のシェル機胜やコマンドをを䜿うが、このずき互換性の違いを吞収する関数を䜜成する

私が曞く蚘事は基本的にシェルだけで実装するか POSIX 準拠のコマンドだけを䜿甚するのが倧半なので 3.の互換性の違いを吞収する方法に぀いお説明する堎所は少ないず思うのです。なのでここで玹介したいず思いたす。

䟋 stat

stat は POSIX で芏定されおいるコマンドではありたせんが、Linux でも macOS (BSD) でも䜿甚できたす。しかしその仕様オプションは倧きく異なりたす。䟋えばファむルサむズを取埗する方法です。

# ファむルサむズの取埗
stat -c %s /etc/hosts # Linux
stat -f %z /etc/hosts # macOS (BSD)

これを䟋に互換性を吞収する方法を玹介したす。たずそれぞれで関数を定矩したす。

filesize() { # Linux
  stat -c %s "$1"
}

filesize() { # macOS (BSD)
  stat -f %z "$1"
}

1. 互換性を吞収する関数

互換性を吞収するには珟圚の環境を刀別し、この2぀の関数のうち適切な方の関数を定矩したす。環境の刀別は uname を䜿甚しお Linux ずいう文字が入っおいるか Darwin (macOS) が入っおいるかで分ける方法を思い぀くかもしれたせんが、これはおすすめしたせん。なぜなら Darwin であっおも Linux版 (GNU 版) の stat が䜿われる堎合があるからです。Homebrew では実際にそのようなこずが出来たす。

ベストな方法は実際にコマンドを実行しおその挙動で刀別するこずです。具䜓的には stat の堎合 stat -f / を䜿っお刀別するこずが出来たす。このオプションは Linux では --file-system オプションず同じ意味で、ルヌトファむルシステムのステヌタスを返したす。macOS (BSD) では -f はフォヌマット文字列なのでそのたたの文字列 / を返したす。

# Linux
$ stat -f /
  File: "/"
    ID: f148a0b23b752507 Namelen: 255     Type: ext2/ext3
Block size: 4096       Fundamental block size: 4096
Blocks: Total: 122661614  Free: 116726932  Available: 110478625
Inodes: Total: 31227904   Free: 30595983

# macOS (BSD)
$ stat -f /
/

このように挙動の違いを利甚しお定矩する関数を分けるこずができたす。長いので関数は䞀行にしたす。

if [ "$(stat -f /)" = / ]; then
  filesize() { stat -f %z "$1"; } # macOS (BSD)
else
  filesize() { stat -c %s "$1"; } # Linux
fi

泚意点ずしおは、どちらでもすでに動䜜が定矩されおいお実行しおも無害なオプションを䜿甚するずいうこずです。「このオプションは Linux でしか䜿えない。」ずいうこずを利甚しお刀別するず、のちに macOS で実装されお動かなくなる可胜性がありたす。無害なオプションを䜿う理由は蚀うたでもないですね。調べれば倧抵䜿えるオプションが芋぀かるず思うのですが、なければロングオプションの --version や --help であれば比范的安心しお䜿えるず思いたす。さすがに --version や --help を他の目的に䜿うこずはないでしょう。

2. 定矩を遅延させる

この関数を「最初に䜿われた時」に定矩する方法です。

filesize() {
  if [ "$(stat -f /)" = / ]; then
    filesize() { stat -f %z "$1"; } # macOS (BSD)
  else
    filesize() { stat -c %s "$1"; } # Linux
  fi

  # 初回はここで定矩した関数を呌び出す。次回からは盎接定矩した関数が䜿甚される。
  filesize "$@" 
}

この方法のメリットは、コマンド刀別のタむミングを実際に䜿甚するタむミングたで遅らせるこずが出来るので、必芁がない堎合に刀別のコストを枛らしたり、たた耇雑な刀別凊理をグロヌバル倉数を䜿わずに䜍眮パラメヌタを䜿っお行うこずができるずいうこずです。あたり意味がない䟋ですが、グロヌバル倉数を䜿わないずいうのはこういうものです。

filesize() {
  # 䜍眮パラメヌタを倉数の代わりずしお䜿うこずで
  # ret=$(stat -f /) のようなグロヌバル倉数(ret)の䜿甚を避けるこずが出来る
  set -- "$(stat -f /)" "$@"

  if [ "$1" = / ]; then
    filesize() { stat -f %z "$1"; } # macOS (BSD)
  else
    filesize() { stat -c %s "$1"; } # Linux
  fi

  shift
  filesize "$@" 
}

ただし、この方法はサブシェルが䜿われるず毎回定矩するこずになっおしたうので泚意しおください。

size1=$(filesize "file1") # サブシェルが䜿われおるので定矩しおも砎棄される
size2=$(filesize "file2") # よっおここでもたた定矩される
filesize "file3" # サブシェルを䜿っおいないので以降は定矩された状態が続く

これを避けるには read -r line ように匕数に指定した名前の倉数に戻り倀が返るように filesize 関数のむンタヌフェヌスを倉曎したす。

size1='' # 参考 shellcheck の未定矩倉数の参照ずいう譊告を隙す方法
filesize size1 "file1" # サブシェルではないので定矩されたたた
echo "$size1" # ファむルサむズは size1 倉数に代入されおいる

実装です。

filesize() {
  if [ "$(stat -f /)" = / ]; then
    filesize() { eval "$1=\$(stat -f %z \"\$2\")"; } # macOS (BSD)
  else
    filesize() { eval "$1=\$(stat -c %s \"\$2\")"; } # Linux
  fi

  filesize "$@" 
}

匕数で指定した倉数に戻り倀を返すずいうテクニックは応甚の幅が広いので芚えおおくず良いです。サブシェルを䜿甚しないのでパフォヌマンスにも優れおいたす。ただし eval を䜿甚するので倉数名等に意図しない文字が入らないように泚意しお䞋さい。脆匱性に぀ながる可胜性がありたす。

3. 関数内での再定矩をさける

「2. 定矩を遅延させる」は通垞は党おのシェルで動くのですが、たれに関数内で同じ関数名で再定矩を行うずシェルが誀動䜜バグする堎合がありたす。その状態になるずシェルが萜ちたり定矩したはずの関数が消えおしたったりずおなしな状況になりたす。ksh や posh でたれに発生するのですが、いく぀かの条件が組み合わさっお発生しおいるようではっきりずした条件は分かっおいたせん。

その堎合は、遅延させるのを諊め「1. 実装」を䜿えばいいのですが、刀定条件が耇雑な堎合、倉数を䜿甚したくなりたす。ただしグロヌバル倉数は䜿いたくないずいう堎合の実装方法です。

filesize() { # 1 段目
  set -- "$(stat -f /)"
  [ "$1" = / ] && return 0
  return 1
}
if filesize; then # 2 段目
  filesize() { eval "$1=\$(stat -f %z \"\$2\")"; } # macOS (BSD)
else
  filesize() { eval "$1=\$(stat -c %s \"\$2\")"; } # Linux
fi

# 参考 環境が耇数ある堎合
filesize &&:
case $? in # 2 段目
  0) filesize() { eval "$1=\$(stat -f %z \"\$2\")"; } ;; # macOS (BSD)
  1) filesize() { eval "$1=\$(stat -c %s \"\$2\")"; } ;; # Linux
esac

同じ関数名で 2 ぀の圹割をもたせおいたす。1 段目は環境刀別凊理を行うために filesize 関数を䜿甚しおいたす。そしお 2 段目で目的の関数を定矩しおいたす。䞀般的には同じ関数名を䜿い回すのはどうかず思いたすが 1 段目はグロヌバル倉数を䜿甚しないために䜿うだけで本来は䞍芁な䞀時的な関数なのでこれぐらいは良しずしおいたす。

さらに泚意ですが、これだけでは誀動䜜が解決しない堎合がありたす。ksh でたれに発生するのですが、「1. トップレベルに関数を定矩しおいる。」「2. トップレベルでその関数を呌び出しおいる。」「3. その関数を再定矩しおいる。」のすべおの条件を満たすず意味䞍明な䞍具合が発生するこずがありたす。必ず発生するわけではありたせん。その堎合のワヌクアラりンドずしお eval を䜿甚しお2.の条件を満たさないようにしたす。具䜓的には次のように曞き換えたす。

# filesize &&:
# ↓
eval "filesize &&:" &&:

# トップレベルで関数を定矩・呌び出しをしなければ良いので
# すべおの凊理を以䞋のように関数内に入れおも良い
initialize() {
  ...
  filesize &&:
  ...
}
initialize

&&: が 2 ぀あるのは set -e が有効な時に mksh が eval の䞭で終了しおしたう問題のワヌクアラりンドです。

さいごに

POSIX で暙準化された機胜だけを䜿っおシェルスクリプトを䜜成するず倚くの堎所で簡単に動かすこずが出来たす。しかしそれはかなり倧倉な䜜業です。䞀番の理由は互換性を吞収するラむブラリがないこずではないでしょうか ブラりザ・JavaScript の䞖界でも今でこそ脱 jQuery などず蚀われおいたすが最初は Prototype.js や jQuery ずいう互換性を吞収するラむブラリの登堎をきっかけに倧きく倉わっおいったず蚘憶しおいたす。開発者各々がシェルやコマンドの互換性や眠やバグに振り回されおいる状態ではシェルスクリプトで快適な開発はできたせん。目指すはバッドノりハりの共有ではなくバッドノりハりが䞍芁な䞖界です。ラむブラリを . で読み蟌んで提䟛されおいる関数を䜿えば现かいこずを考えずに移怍性・可搬性が実珟できる。シェルスクリプトの䞖界にもそういう流れが蚪れれば良いなず私は考えたす。

私芋ですがこれたではシェルスクリプトの"ラむブラリ"のテストは倧倉だったず思いたす。テスティングフレヌムワヌクはありたすが、それらはシェルスクリプトを䜿っお倖郚コマンドのテストを蚘述・実行するためのものであり、シェルスクリプト自身をテストするものになっおいない䞍可胜ではないがやりづらいからです。しかし今は ShellSpec がありたす。これはシェルスクリプト自身ずラむブラリに最適化されたテスティングフレヌムワヌクなのです。ここから倚くのラむブラリが生たれ、シェルスクリプトの有甚性が再認識され、ゆくゆくはシェルそのものが匷化されbash 等の拡匵はほんず暙準化されおほしいです。各コマンドも互換性向䞊ず機胜匷化されればシェルスクリプトでの開発はもっず簡単になるでしょう。他の蚀語で実装すればいいず思うかもしれたせんが、暙準でどこでも䜿える今のシェルがなくなる未来なんお誰も想像できないですよねどうせ䜿うなら快適な方が良いに越したこずはありたせん。

83
84
4

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
83
84

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?