weblog of key_amb

主にIT関連の技術メモ

シェルスクリプト用テストツール "shove" v0.8 までの更新のお知らせ

シェルスクリプト用のテストツール "shove" を作って、初めて上の記事で紹介したのは約5ヶ月前になります。

今回は、上記事の時点からこれまでの主な差分をお知らせします。
言うなれば CHANGELOG + αな記事となります。

shove は GitHub で公開しています。URL は下になります:

https://github.com/key-amb/shove

CONTENTS:

グルーピングの新しい記法を追加 (v0.8.1)

今まで T_SUB "..." (( ... )) という記法でテストコードをグループ化できるようにしていました。
が、なんか微妙だなと思って t::group "..." ({ ... }) という新しい記法を追加しました。

なんか微妙だな、と思った以上に深い理由はあまりなかったりもします^^;
が、2つの記法をサポートするのはコード上、厳しいことになっていたりもするので、古い記法はその内消そうと思ってます。
ので、もし使っている人がいたら新記法に移行をお願いします。(すいませんすいません)

サンプルコードは README を更新していますので、そちらをご覧くださいませ。

テスト用の関数を12個追加 (v0.8.0)

コードをリファクタしていじりやすくなったので、調子に乗って色んな test 関数のオプションでテストできるように t_xxx な関数を追加しました。

README そのままですが、追加した関数とその使い方は下のようになります:

t_present   $str  "str is present"    # [ -n "$str" ]
t_blank     $str  "str is blank"      # [ -z "$str" ]
t_exist     $path "path exists"       # [ -e "$path" ]
t_file      $path "path is file"      # [ -f "$path" ]
t_directory $path "path is directory" # [ -d "$path" ]
t_symlink   $path "path is symlink"   # [ -L "$path" ]
t_eq        $x $y "x == y"            # [ $x -eq $y ]
t_ne        $x $y "x != y"            # [ $x -ne $y ]
t_gt        $x $y "x >  y"            # [ $x -gt $y ]
t_ge        $x $y "x >= y"            # [ $x -ge $y ]
t_lt        $x $y "x <  y"            # [ $x -lt $y ]
t_le        $x $y "x <= y"            # [ $x -le $y ]

右部のコメントの通りに test 関数が実行され、結果が成功ならテストが通ります。

shpec のテストを追加 (v0.7.2)

テストツールのテストを自分自身でやるのは微妙だな、という気はします。

shpec *1 は上の記事でもちょこっと紹介していますが、RSpec ライクな記法でシェルスクリプトのテストを可能にしてくれるツールです。

CI には入れてませんが、テストコードを少し書いて、ときどき手元で実行しています。

shpec では describe ... endit ... end でブロックを作ることができます。が、 shove のグルーピングのようにサブシェルは使っていないようでした。
単に RSpec っぽく書けて、結果もきれいに出せるもの、という理解でいいのかな、と思っています。

コードがミニマルな感じですごくきれいなので、 shove のリファクタで参考にさせてもらいました。

その他にも、少し内部構造をいじったり、ちょっとした改善がいくつか入っていたりします。

シェルスクリプトのテストをしたいシーンがありましたら、shove のことを思い出していただければ幸いです。

Enjoy!

"clenv" というシェルスクリプトのモジュール管理ツールを引き続き作っている

keyamb.hatenablog.com

上の記事を書いたのが3ヶ月前ですね。
趣味で作っているのでだいぶ波があるのですが、初コミットからは5ヶ月ほど経ちました。

"clenv" って何?

https://github.com/key-amb/clenv です。

説明は上の記事に書きましたが、一応こちらでもかんたんに。

私が趣味で作っているツールで、シェルスクリプトの実行ファイルやライブラリをモジュール化して管理できるようにしてくれるものです。
名前はお察しの通り、rbenv など 〜env が由来です。

"version" という語ではなく "environment" という語を使っています。
"version" だと異なるバージョンのシェルをビルドしてインストールして…のようなことを連想されそうだな、と思いまして。
そういうのをできるツールがあってもいいとは思いますが、それをやる気はない。

"clam" モジュール

これも冒頭の記事に書いたのですが、clenv で使うモジュールを "clam" モジュールと呼ぶことにしました。
git の URL か、ローカルのファイルシステムからインストールできます。

モジュール側では clam.spec というファイルを用意しておく必要があります。
これのファイル形式が v0.3 で少し変わりました*1

まあ、詳しくは README や脚注の PR をご覧くださいということで。

NEW - "cload" コマンドと "cllib" 関数

v0.2 で導入しました。
. or source 相当ですが、 CLOAD_PATH という環境変数の path を見て、そこからの相対パスで読み込めるものです。
これで rubyperl の require 相当のことができる、と考えました。

面倒なのは、これをシェルの関数で提供しなければならないことです。
スクリプティングにおいては、シェル関数を読み込むコードを一行足すか、 eval 実行する必要があります。

使い方としては、下のようになります。

# 例1
eval $(cload "mylib")
eval $(cload "mylib/foo")

# 例2
eval $(cload -)
cllib "mylib"
cllib "mylib/foo"

ここで、 cllib が "cload" のシェル関数版です。

eval $(cload -) をやってくれるラッパーコマンドを作ればいいじゃないか、と思う人もいるかもしれませんが、そうすると実行スクリプト. または source して実行せざるを得なくなる気がします。
ので、厳しそう。

NEW - shims/ に shim を置くことにした

v0.2 までは、 shims => environments/$CLENV_ENVIRONMENT/bin と symlink していただけですが、 rbenv を参考にランチャーとなるシェルスクリプトの実体を置くことにしました。
clam install するときに、下のようなファイルを実行ファイルと同じ名前で shims/ 以下にコピーしています。

#!/usr/bin/env bash

set -euo pipefail
[[ ${CLENV_DEBUG:-} ]] && set -x

program="${0##*/}"

exec "${CLENV_ROOT}/bin/clenv" exec "$program" "$@"

だいたい rbenv や plenv と同様で、clenv exec で対象のコマンドを探して exec します。

Travis CI で継続的にテストできるようにした

シェルスクリプトでどうやるのかな、と思っていたところ、 b4b4r07/enhancdTravis が設定されていたので、参考になりました。

手前味噌ですが、 shove でテストを書いています。*2

実行ファイル系は Bash 前提で書いていますが、シェル関数で機能提供するものは POSIX シェルで動くように書いています。
ので、 make test タスクもそんな感じで書いています。

いま CI 環境 だと、Ubuntu v12.04 のコンテナで sh, bash, dash で CI が走っています。

あらゆる環境で動作保証するものではないけど、まあ、だいたい動くんじゃないかな、というところ。

※テスト足りてないところは色々あります。

clenv 環境で使える Bash 用の Logger モジュールを書いてみた

https://github.com/key-amb/bash-logger

これです。これ自体は bash スクリプト1枚なので、これだけダウンロードして単独で使うこともできます。

clenv 環境にインストールするには clam https://github.com/key-amb/bash-logger.git で。

clenv 環境でこれを使うサンプルコードは下のようになります。

# myapp.bash
eval $(cload logger.bashrc)

log.info "foo"
log.warn "warn"

実行すると下のようになります。

% ./myapp.bash
2016-09-19 08:47:35 [INFO] foo
2016-09-19 08:47:35 [WARN] warn

(本当はログレベルに応じて色が変わります。)

まあ、こういうことがやりたいわけです。
つまり、再利用性のあるライブラリを書いて、再利用して楽にスクリプティングしたい。

今後

shims/ 以下は実体を置くことができたのですが、 environments/$env/{bin,lib} 以下がまだインストールした module への symlink になっているので、それも実体にしないと、ライブラリ同士のディレクトリ構成に依存関係があるときに上手く行きません。

解決する方法は2通りあります。

  1. bin/, lib/ 以下にも中身をコピーする
    • uninstall できるように、何をコピーしたか記録しておかなければなりません
  2. bin/, lib/ 以下に実体は置かない。 cload 時に任意のモジュールについて environments/$env/$module/ 以下の path を CLOAD_PATH に追加して解決する

後者は RubyGems が行っている戦略ですね。*3

前者は clam install/uninstall で頑張る。後者は cload で頑張る感じ。
後者にしても、 bin/ 以下には RubyGems で言うところの binstub 相当を置かないといけないですが、それでも uninstall はだいぶ楽になりそうです。
また、後者にすると同一モジュールでも複数バージョンインストールできるようになったりと、(ニーズはさておき)夢が広がりそうです。

趣味でやっているので、いつになるかわかりませんが、ご興味ありましたら、完成までは気長にお待ちください。

現時点でも上に書いたようなことや、冒頭の記事で書いたような実行ファイルの管理に使えると思います。というか、使っています。

…が、今後、互換性のない変更が入る可能性はありますので、ご注意ください。

脚注

enhancd を改修して引数なし cd コマンドの挙動を変えずに使えるようにした

先日、上の記事を書きました。

記事末尾に「余談」として、次のように書きました。

もう1つ enhancd による cd の挙動変更でときどき戸惑うのは、 cd 単体で実行したときです。 enhancd ではこのときもディレクトリ履歴検索による選択画面になります。

これはとりあえず諦めて受け入れることにしたのですが、今回紹介した変更のように、デフォルトの挙動を変更しないオプションがあってもいいかもしれません。

すると、意外と反響が有って、ニーズがあることがわかりました。

というわけで、オプションを追加して PR してみました。

で、それが本日、めでたくマージされましたので、お知らせします。

~/.bashrc などで以下のオプションを指定して、enhancd をご利用くださいませ。

ENHANCD_DISABLE_HOME=1

これで、 cd コマンドを引数なしで実行したとき、peco などのインタラクティブフィルタを起動せずに、ふつうに $HOME に cd できます。

Enjoy!

余談

前回の cd -, cd .. の拡張の時、ドキュメントの修正までやってなかったので、別 PR でやっておきました。

これも既に master に取り込んでもらっています。

もう一つ余談ですが、enhancd のテストで拙作の shove を使っていただいています。ありがたや。

RSpec で example の外で定義したローカル変数を使うのはアリか?

※9/3 @jnchito さんのコメントを受けて追記しました。

RSpec で example の外で定義したローカル変数を使う

テストコードの例

こういうの:

outside_var = :outside_var # (1)

describe :top_scope do
  top_scope_var = :top_scope_var # (2)

  it :example_in_top_scope do
    expect(outside_var).to be_truthy
    expect(top_scope_var).to be_truthy
  end

  context :second_scope do
    second_scope_var = :second_scope_var # (3)

    it :example_in_second_scope do
      expect(outside_var).to be_truthy
      expect(top_scope_var).to be_truthy
      expect(second_scope_var).to be_truthy
    end
  end
end

これまでのところ、他人が書いたコードでこういう書き方を見たことはなかった。
外側で定義した変数を使う場合、 let を使うか before ブロックでインスタンス変数に代入するのがふつうだろう。

が、上のような書き方でも、ふつうに期待通りに動く。

特徴・用途

ローカル変数を使う場合には、let を使って宣言する場合や beforeインスタンス変数を使う場合と比較すると、以下のような特徴がある。

  • 変数定義は1回だけ実行される 〜 before :context 相当
    • letbefore :each では、example ごとに実行される
    • describe ブロックが評価され、RSpec の ExampleGroup が作られる時点で実行されているようだ
  • スコープが有効
    • 上の例で、 :top_scope から second_scope_var は見えない
    • let で宣言する変数や before で定義するインスタンス変数と同様
  • let 同様、typo に気づける。インスタンス変数だとエラーにならないので、気づかない恐れが有る(参考記事を参照)

RSpec の内部構造に踏み込んで深く追ったことはないのだけど、挙動を確認した限り、上のようだった。

ローカル変数のユースケースとしては、一々毎回実行・評価しなくていいような性質のもので、テスト中に状態が変わらないものに使えそうだと考えている。リファクタ用途としても使えそうだ。

「アリ」なのか、「ナシ」なのか?

自分自身としては、何ヶ月か前 fireap *1などを作っていた頃は、RSpec の作法をよく知らなかったので、ローカル変数を多用していた。

しかし、後になって RSpec について色々調べている内に、このような書き方を全く見かけないことに気がついた。
とはいえ、逆に「こういうのは駄目だ」とはっきり書いてある記事やドキュメントもなかった。

…ので、こういう書き方がアリなのかナシなのかというのが、数カ月ぐらい気になっていた。

で、最近初めて自分以外の人でそういう書き方をしている人を見かけたので、気になり度がしきい値を突破して、とある Rubyist が集うチャンネルで聞いてみた。


私「RSpec 詳しい人いますかね? example の外でローカル変数定義して使うのってアリなのかなぁというのが気になってます。こんなの↑(上のようなコード)」

S氏「1. rspecdsl で書くと、ほらわかりやすいでしょ?というのが走りなので、dsl を使わなくなったらなぜ rspec を使うのか、という話になりそう。
 2. let はオプションで thread-safe にしたり thread non-safe にしたり切り替えられるはず。
 3. let は それぞれの it の中で、それぞれ呼ばれるのでちょっと動きが違う。
 ぱっと思いつくやつ↑」

O氏「

describe "parameterized test" do
  [ 5, 10, 15 ].each do |i|
   it { expect(fizzbuzz(i)).to eq("buzz") }
  end
end

 みたいなパラメタライズドテストを rspec-parameterized みたいなのをかかなかったら普通にそうなってるので、特に気にしないでやればいいんじゃないかな?」


確かに、そういう書き方をしたこともあった。
ってか、 rspec-parameterized 知らなかった。

2016/9/2 現時点の結論

…というわけで、今のところ自分の中では「アリ」ということになっている。

ただ、テスト実行中に変数の状態が変わったりしたら、なんかまずそうなので、定数にしたり freeze した方が安全かも、と思ったり思わなかったり。

どうなんでしょうね。

(9/3 追記)

@jnchito さんが、本件についての見解・回答を示すブログを書かれています。

結論だけ書くと「興味深いけど積極的に使うのは NG」という感じです。
興味のある方は是非、上の記事もご覧ください。

9/3 追記:ローカル変数が使えそうな例

なんと、参考記事等でおなじみの @jnchito さんからコメントをいただきました。
正直、Qiita のコメントで聞いてみようかと思ったぐらいなので、僥倖でした。ブログ書いてよかった。

「絶対ローカル変数が良い、というサンプルコードがあれば教えてください」

とのこと。

「絶対」ということはないかな、と思いますが、「まあ、こういうケースならアリかな(?)」と個人的に思う例を3つほど書いておきます。

(1) 長いメソッド等のリファクタ系

特に、実行結果が毎回変わらないものがよさそうです。

describe MyReservationsController do
  in_sale = Reservation.statuses[:in_sale]
  reserved = Reservation.statuses[:reserved]
  cancelled = Reservation.statuses[:cancelled]
  out_of_sale = Reservation.statuses[:out_of_sale]

  let(:client) { FactoryGirl.create(:client) }
  let(:parsed_response_body) { JSON.parse(response.body) }
  
  describe '#index' do
    context '販売中のもの'
      context 'ログイン前' do
        it '予約が見つからない' do
          get :index, status: in_sale, count: 10
          expect(parsed_response_body['Reservations']).to be nil
        end
      end
      context 'ログイン後' do
        before do
          client.login
        end
        it '販売中の予約が取得できる' do
          get :index, status: in_sale, count: 10
          parsed_response_body['Reservations'].each do |r|
            expect(r['status'])
          end
        end
      end
      context 'メンテナンス中' do
        before do
          expect(MyService).to receive(:maintenance?).and_return(true)
        end
        it '予約が見つからない' do
          get :index, status: in_sale, count: 10
          expect(parsed_response_body['Reservations']).to be nil
        end
      end
    end
  end
end

もちろん、 let で書けないことはないし、↑の例だと get ... をまとめて subject 化するのもアリと思います。

(2) セットアップに時間のかかるオブジェクトを1回だけ生成し、結果を利用したい

下はプリミティブな DB のコネクション・ハンドラを自作しているような前提です。

conn = MyDatabaseConnector.setup!

describe MyTransactionApp do
  context 'normal' do
    it do
      Foo.query(conn, type: :normal)
      :
    end
  end
  context 'strange' do
    it do
      Foo.query(conn, type: :strange)
      :
    end
  end
end

RSpec の流儀に合いそうな別解としては、下のように書き換えられることもあるかもしれません:

describe MyTransactionApp do
  let(:conn) { MyDatabaseConnector.connection }

  before :all do
    MyDatabaseConnector.setup!
  end
  
  :
end

あるいは、 helper module にできるケースもあるかもしれません。

(3) 時間がかかるわけでもないが、1回だけセットアップして結果を利用したい

target_path = Foo.method_to_get_path(some_arguments, ...)
tmp_path = Pathname.new(Dir.tmp_dir) + 'foo'

describe MyApp do
  before :all do
    # 対象ファイルがあれば退避
    if File.readable?(target_path) do
      FileUtils.move(target_path, tmp_path)
    end
  end

  after :all do
    # 対象ファイルを復帰
    if File.readable?(tmp_path) do
      FileUtils.move(tmp_path, target_path)
    end
  end
  
  context 'A' do
    before do
      File.write(target_path, 'A')
    end
    after do
      FileUtils.remove(target_path)
    end
    
    it 'target_path の内容に依存するテスト' do
      :
    end
  end

  context 'B' do
    before do
      File.write(target_path, 'B')
    end
    after do
      FileUtils.remove(target_path)
    end
    
    it 'target_path の内容に依存するテスト' do
      :
    end
  end
end

この場合の別解としては、 target_path を helper method にしたり、 tmp_path をインスタンス変数にして before :all でセットする、というやり方もできそうです。

参考

enhancd を改修して "cd -" や "cd .." の挙動を変えずに使えるようにした

Bash で enhancd を導入することにした

cd の履歴を peco で移動したい。

シェル環境で peco を使いだした人なら、きっとそう思うことがあるでしょう。

zsh だと、cdr と組み合わせることで実現できます*1
下の記事あたりを参考に設定するといいでしょう。

さて、bash の場合、最近は b4b4r07/enhancd を使う人が多いのではないかなと思います。
いくつか作者の @b4b4r07 さんが記事を書いてます。

この enhancd は Bash の小枝集 で紹介されている cdhist.sh をベースに作られたそうです。zsh と fish にも対応しているそうです。
自分も一瞬、何か自作しようかと思ったのですが、enhancd が便利そうだったので、まずは使ってみることにしました。

使い方は README の通りですが、ソースを clone して . init.sh するだけです。
これで cd が enhancd の関数に alias されます。

色々機能がありますが、詳しくは上に挙げた Qiita の記事がわかりやすいだろうと思います。

とりあえず、自分にとって最低限必要だったのは cd - で cd の履歴を検索して移動できるようになることだけでした。
他に、 cd .. すると peco が起動してディレクトリを上方検索してくれました。

Pull Request に至る経緯と改修後の enhancd の設定方法

最初「おぉ。これは便利」と思って使っていたのですが、しばらく経ってから cd -cd .. を打つと、以前の挙動と違って peco の選択画面になるので、戸惑うことに気づきました。
慣れようと思ったこともありましたが、無理でした。

元々、それ以前に peco を使う機能にはキーボード・ショートカットを割り当てることにしていた*2ので、そちらに寄せたいと思いました。
また、以前は運用エンジニアとして色んなサーバに出入りしていたこともあって、あまりデフォルトの挙動を変えたくないという気持ちもありました。

というわけで、 cd -cd .. のデフォルトの挙動を変えずに、ディレクトリの履歴検索や上方検索を別の特別な引数に割り当てられるようにする Pull Request を送りました。

この PR を送ったのは2ヶ月ほど前のことでしたが、一昨日ようやくマージしてもらえました。

~/.bashrc の設定例は次のようになります。

ENHANCD_HYPHEN_ARG="-ls"
ENHANCD_DOT_ARG="-up"
. path/to/enhancd/init.sh
bind -x '"\C-ur": cd -ls'

ENHANCD_HYPHEN_ARGENHANCD_DOT_ARG が追加した設定用の変数です。

上のように設定すると、 cd -lsディレクトリの履歴検索、 cd -up で上方検索が可能になります。
また、 cd -, cd .. がデフォルトの挙動($OLDPWD, .. への移動)に戻ります。

キーボード・ショートカットとしては履歴検索に Ctrl-u + r を割り当てました。
cd -up には割り当てていませんが、覚えていれば問題ないです。たまに便利。

結びに

bash 環境に enhancd を導入し、 cd -, cd .. の挙動を変えずに使える機能拡張をしました。

enhancd をご利用の方や、これから利用される方の参考となれば幸いです。

余談

もう1つ enhancd による cd の挙動変更でときどき戸惑うのは、 cd 単体で実行したときです。
enhancd ではこのときもディレクトリ履歴検索による選択画面になります。

これはとりあえず諦めて受け入れることにしたのですが、今回紹介した変更のように、デフォルトの挙動を変更しないオプションがあってもいいかもしれません。

追記:こちらについてもパッチを送って、デフォルトの挙動を変更しないオプションをサポートしてもらいました。

参考

See Also

脚注