※効果には個人差があります。

はじめに

どうも。らりおです。

さてこの度、 rogy Advent Calendar 2015 におきまして、「シェルスクリプトは救い」と題した記事を書くことを予定しておりました。
というのも、なかなか私の要求を満たす静的サイトジェネレータが見付からず、 ならば自力でシェルスクリプトで書こうか、などと考えていたためでございます。
しかしその目論みは崩れ、 nanoc に救われてしまったため、 急遽予定を変更し、

  • シェルスクリプトがあなたを救う、その理由

  • シェルスクリプト成果物 自慢大会 紹介

の二本立てでお送りいたします。

あと、この記事は†強い†人向けのものではありません、念の為。

Rubyに圧倒的感謝。

シェルスクリプトがあなたを救う、その理由

シェルスクリプトは進捗の塊である

シェルスクリプトは、基本的にコマンドを並べていくものです。 そして、ほとんどのコマンドは入出力があり、それ自身がひとつの独立したプログラムでもあります。 要するに、ただでさえ「それひとつだけでも使い道のあるコマンド」を、豪華にもプログラムの一部として並べていくのがシェルスクリプトなのです。

単体では役立たないコードを大量に並べるより、一人前のコマンドたちを濫用する方が、なんか良さそうじゃないですか?

ないですか、そうですか。

意味のわかるコードになる

一つのことを行い、またそれをうまくやるプログラムを書け。
協調して動くプログラムを書け。
標準入出力(テキスト・ストリーム)を扱うプログラムを書け。標準入出力は普遍的インターフェースなのだ。

UNIX哲学と呼ばれていますが、こういう考え方がUNIX(Linuxとかも)では主流です。 たとえばWindowsのフリーソフトなどは「これ一つで何でもできる(ただし連携は無理)」みたいなものが多いですが、 UNIXでは「文字列を検索する」「文字列を置き換える」「行を順に並べ替える」「行をランダムに並べ替える」「先頭の数行だけ表示する」 のような、分割するほどでもない小さな処理それぞれを行うコマンドが沢山存在しています。

シェルスクリプトというのはこれらのコマンドを並べて作るものなわけですが、 このことから「各コマンドの出力は、何かの意味ある処理を行った結果である」と言えるでしょう。

つまり、「 ある行/あるコマンドを抜き出して見たとき、それが何をしようとして書かれた処理なのかはっきりわかる 」ということです。

まあオプションとかは把握していないとわかりづらいところもありますが、そういうときは man を引きましょう。

並列性

簡単に並列化できます。 パイプにデータを流すだけです。

シェルスクリプトではパイプを多用しますが、これはバケツリレーのようなもので 「全てのデータの処理を待つまでもない場合は、部分的な処理が終わり次第どんどん出力する」 という動作をします。

たとえば「検索→圧縮」というような処理をパイプを使って書いた場合、全ての検索が完了しなくても、 データが見付かり次第その箇所がどんどん吐かれていき、それと同時に圧縮も進行します。 というわけで、自然にコードを書くだけで勝手に並列化されるわけです。

無論、明示的に「複数コマンドを同時に実行したい」みたいなこともできて、そういった目的専用の(ひとつのことをうまくやる!) コマンドもいろいろあります。有名かつありふれているのが xargs とかです。

別の言語に逃げられる

なんだかシェルスクリプトで書くのが面倒になってくる、そんなこともあるでしょう。 そういうときは、敢えてシェルスクリプトにこだわらず、部分的に awkpythonruby など、 好きな言語で書いてしまいましょう。

コマンドでさえあれば、シェルスクリプトから使えないものはありません。

どこでも動く (一部地域を除く)

shbash などは多くの環境(というか殆ど全てのLinux環境)に入っています。 コンパイラが無いとかインタプリタが無いとか、そういうことに悩む必要はありません。

まさに Write once, run anywhere です。
(ただし、ここでのanywhereはどちらかというと"almost everywhere"に近いです。 まあWind○ws機は可算個しかないし、無視していいですよね)

シェルスクリプト成果物紹介

dotfiles管理のための簡易パッケージマネージャ擬きを作った

dotfilesというのは、いわゆる .vimrc とか .zshrc とか .xsession とか、 そういう設定ファイルの総称です。 だいたいが隠しファイルとして、「.」で始まるファイル名をつけられているため、 このように呼ばれています。

さて、私はいくつかのマシン(家のデスクトップ、持ち運ぶラップトップ、rogyのサーバ、自分のVPS等)でアカウントを持っていますが、 たとえば家で使っていた快適な設定をサーバで使えないというのは結構に不便なことです。 折角なので †おれの考えた最強の設定† をあらゆる環境で使いたいというのは当然の欲求でありましょう。

というわけで、そのためのスクリプトを書きました。

機能

  • dotfilesをホームディレクトリ以下の適切な場所にインストール

    • アンインストールもできる

    • 更新によって不要なファイルが出た場合、自動で削除してくれる

  • 設定ファイルは用途・対象ソフトウェアごとにパッケージとして分割して管理できる

    • パッケージ間には依存関係を指定できる

    • 複数パッケージが同じ場所にファイルをインストールするよう要求していた場合(衝突)、それを検出する

      • もちろん依存のチェーンも表示する

  • ホスト(マシンやユーザ)ごとに使うパッケージを選択できる

パッケージ自体にはバージョンが存在しないので、厳密にはこれをパッケージマネージャと呼びたくはありませんが、 依存関係の管理と衝突検出ができると、なんとなくソレっぽいと思いませんか?

使用例

今回は対象ディレクトリを /home/larry/dotfiles_test/test/ として実験してみます(普通は対象はホームディレクトリ直下です)。

test/testn (n は0〜9)パッケージは、対象ディレクトリ直下に testn (もちろん n は0〜9)ファイルを導入します。 virtual/test-oddtest/test{1,3,5,7,9} に、 virtual/test-primetest/test{2,3,5,7} に依存しています。 (これらはtestnを楽してまとめて入れるためのもので、それ自体は何のファイルも提供しません。) それから、 virtual/test-odd-indirectvirtual/test-odd に依存しています。

では、 test/test0test/test6virtual/test-primevirtual/test-odd-indirect の4つに依存した状態でインストールすると何が起きるか表示してみましょう。

$ ./install.sh -n

screenshot-2015-11-29-201626+0900

はい。
なんというか説明するまでもありませんが、素数なやつと奇数なやつ、それから個別に指定した0と6もインストールされるようです。

では、衝突も試してみます。 test3 というファイルを提供する別のパッケージ、 test/another-test3 もインストールするよう指定してみます。

$ ./install.sh -n

screenshot-2015-11-29-200630+0900

メッセージの意味はこんな感じです。

  • 対象ディレクトリ直下にインストールされる予定のファイル test3 が衝突

    • test/another-test3test/test3 がこのファイルを入れようとしていた

    • test/another-test3 はユーザが指定したことで要求された

    • test/test3 には virtual/test-oddvirtual/test-prime が依存している

    • virtual/test-odd には virtual/test-odd-indirect が依存している

    • virtual/test-prime はユーザが指定したことで要求された

    • virtual/test-odd-indirect はユーザが指定したことで要求された

こんな感じで、どのような経路でそのファイルが複数パッケージから提供されるに至ったか、 いちいち説明してくれます。

まあその他の機能は ./install.sh -h とかやってヘルプ見てください。

特徴

  • ヤバいファイル名も正しく扱える

    • 空白や改行文字が入ってたり、 * みたいな文字が入っていたりするような場合でも、正しくファイルをリンク/削除できます。

  • 可搬性(portability)がとても高い

    • bashsedfind などありきたりなコマンドだけで動きます。

    • tempfile コマンドはDebian由来らしいので、もしかすると一部distroでは自分で入れないと無いかもしれません。

  • エラーが起きても突っ走らない

    • エラーが出たら、ちゃんとそこで動作を停止します。

作っての感想

  • comm コマンドなんて初めて知りました。まだまだ精進が足りなかったようです

  • 安全性の理由で sed -zfind -print0 等、NUL文字を区切りとする手法はよく使われますが、 /bin/shread コマンドがこれに対応していない(read -d はbashでないとできない)のには驚きました。 なんとかならないものでしょうか。

  • declare -r とか set -eu は初めて使いましたが、これ良いですね。皆さんも是非使いましょう。ケアレスミスを滅せよ!

さいごに

便利! 最高! POSIXに圧倒的感謝!!