shellspec 0.14.0 リリースしました

今回のリリースでは「テスト実行順のランダム化」を実装しました。正直気は進まなかったんですけどね・・・。shuf コマンドでいいやん。

なんで実装したかというと、bats-core のリポジトリを眺めていてこのプルリクを見つけたからです。

github.com

あー、対応するんだー、対応するんやー。って感じで。

「テスト実行順のランダム化」は、基本の実行順番のランダム化の機能に加えて、SEED値を与えることで同じ順番を再現する機能があります。後者の機能は「SEED値 + ファイル名 + example ID」の文字列のハッシュ値を計算しその値を元に乱数を求めて、その順番で並び替えることで実現しています。つまり、ハッシュ計算、乱数計算、ソート の3つの処理が必要になります。

bats-core では awk とか paste とか使ってやってるようでギリギリPOSIX準拠の範囲での実装かな?とも思うのですが、awk は gawk やら nawk やら mawk やらいろいろ派生バージョンがあって互換性でハマるのが見えてるのであまり使いたくない所です。

ランダム値の取得は bash であれば $RANDOM 変数からを取れるのですが、POSIX準拠の範囲ではawkを使う以外では(おそらく)ないんじゃないかと思います。/dev/random や /dev/urandom も必ずしも実装されているわけじゃないようですし。

またハッシュ計算は、POSIX準拠の範囲では cksum コマンドがあるのですが、データの入力は当たり前ではありますが(複数の)ファイルもしくは標準入力からとなっています。今回やるべきことは全てのテスト項目「SEED値 + ファイル名 + example ID」のそれぞれに対してハッシュ値を求めることなのですが、例えば shellspec 自身のテストは 400 以上あります。つまり400個以上のプロセス起動かファイル作成が必要になります。ちょっとやりたくないですね。

ということで、ハッシュ計算と乱数計算をシェルスクリプトだけで実装することにしました。実装が簡単で処理が速そうなものと言う観点から、ハッシュ計算として FNV-1a、 乱数計算としてXorshift を採用しました。どちらもコードが短く四則演算とビット演算の機能だけで実装できます。

ソートに関しては sort コマンドを実行しています。シェルスクリプトで実装するのは手間な割に必要な呼び出し回数は1回だけだし、POSIX準拠コマンドの中でも基本のコマンドなので busybox を含め動かないということはないでしょう。

shellspec 0.13.0 リリースしました

今回は、フィルタ関連の機能を強化しました。

**** Ranges / Filters ****

You can select examples range to run by appending the line numbers or id to the filename

  shellspec path/to/a_spec.sh:10:20     # Run the groups or examples that includes lines 10 and 20
  shellspec path/to/a_spec.sh:@1-5:@1-6 # Run the 5th and 6th groups/examples defined in the 1st group

You can filter examples to run with the following options

  --focus                         Run focused groups / examples only
                                    To focus, prepend 'f' to groups / examples in specfiles
                                    e.g. Describe -> fDescribe, It -> fIt
  --pattern PATTERN               Load files matching pattern [default: "*_spec.sh"]
  --example STRING                Run examples whose names include STRING
  --tag TAG[:VALUE]               Run examples with the specified TAG
  --default-path PATH             Set the default path where shellspec looks for examples [defualt: "spec"]

行番号指定の他に新たにIDでの指定が可能になりました。またexample名、tagによる絞り込みとファイルパターンの指定が可能になりました。

IDでの指定のフォーマットはrspecの指定方法とは変えています。

rspec path/to/a_spec.rb[1:5,1:6] # run the 5th and 6th examples/groups defined in the 1st g

rspecの指定方法はIDをカンマ区切りにして [ ] でくくる書き方ですが、この [ ] はzshではメタ文字扱いなのでダブルクォートが必要になるのとパースが面倒だからです。shellspecは前提としてIDの区切り文字を : ではなく - にして、各ID、行番号の区切り記号は : と共通の文字を使い、ID指定の場合は、最初に @ をつけるようにしました。行番号、IDともに : 区切りで扱えばいいのでパースが簡単で、行番号指定とID指定を混ぜることもできます。(あまりやる人はいないでしょうが)

そして --list オプション (旧 --list-specfiles, --list-examples オプション)で表示する、全specfileと全exampleのリストを出力するコードを再実装しました。今まではリスト出力のために独立したコードを書いていたのですが、これだとフィルタ関連の機能を追加したときに、specfile実行で使用する(トランスレータの)フィルタ機能と別にコードを書かなくてはならず対応が難しくなるからです。(今までは行番号指定と --focus だけなので対応できていたのですが)

specfileの実行とリスト出力のコードの共通化はなかなか面白くて、もともとトランスレータはspecfileの解析部分とどのように変換するかを分離していました。なのでロジック部分(難しい箇所)は共通で、specfileの実行では実行のためのコードに変換し、リスト出力ではリスト出力のためのコードに変換しています。結果リスト出力のコードは難しいロジックはなくなり、前の実装よりも短くなりました。(そのかわり実行速度が落ちてるはずですが)

そして全exampleのリスト出力は行番号つきの出力の他にIDつきの出力に対応しました。IDつきでexample一覧を出力し、その出力をそのまま shellspec の引数として使用できるわけです。

shellspec の JUnit XML 出力対応について

shellspec を JUnit XML 出力に対応させようかなーって考えてます。

が、しかし、JUnit XML の公式な仕様が見つからない。というかそういうのはなさそう・・・

dev.classmethod.jp

いろいろ探しても情報が古そうだったりして、JUnit5で入れ子が可能になったはずだけど、XMLに変更はないのだろうか?以下はいろいろ探して見つけたリンク

github.com

<testsuites>
  <testsuite errors="0" failures="0" hostname="archbuild" id="0"
      name="StudioAllTests" package="test.infor.clearux.studio.integration"
      tests="456" time="107.824" timestamp="2008-01-23T10:49:26">
    <testcase classname="test.foo.bar.DefaultIntegrationTest" name="experimentsWithJavaElements" time="0.0" />
    <testcase classname="test.foo.bar.BundleResolverIntegrationTest" name="testGetBundle" time="0.0" />
    <testcase classname="test.foo.bar.BundleResolverIntegrationTest" name="testGetBundleLocation" time="0.656" />
    <testcase classname="test.foo.bar.ProjectSettingsTest" name="testNatureAddition" time="0.125" />
    <testcase classname="test.foo.bar.ProjectSettingsTest" name="testNatureRemoval" time="0.11" />
  </testsuite>
</testsuites>

上のXMLは jenkinsci に含まれてるテスト用の junit-report-1233.xml の抜粋ですが、このフォーマットはちょっときつい。testcase よりも前にある testsuite に実行したテスト数や失敗した数(tests, failures)がある。

XMLとしては、まあ妥当なものだとは思うのですが、この場合、下にある testcase を実行してから出ないと testsuite が生成できないということになります。shellspec のレポーターの設計は基本的に処理したものから随時出力することで素早いフィードバックを可能にしているのですが、このXMLの形式だと全てのテストを実行してからでないとレポートが生成できないということになります。

もちろんメモリやファイルにデータを貯めていって全部そろってからレポートを生成すればいいのですが、そうするとテスト実行中は真っ暗な画面のまま待っていなければいけないということです。それは寂しいので実行中は実行ログを表示させる?→あれ?それって標準のフォーマッター出力でよくね?と考えると、そもそも画面表示とテスト結果の保存は別々の機能なのではないのか?ということに思い至りました。

そこで今は次のようなオプションで考えています。

shellspec --format progress --out "junit:result-junit.xml" --out "cppunit:result-cppunit.xml" --out "html:result.html"

従来のフォーマッター(画面表示用)は一つだけ指定可能、テスト結果の保存は複数の形式で出力にしています。テストを再実行せずにHTMLやXMLで見たいということもありますしね。TAP形式ははたしてフォーマッターなのかテスト結果なのか悩むところはありますが。

それからもう一つ、テスト実行にかかった時間(例 time="107.824")にも悩んでいます。というか悩んでいました。POSXの範囲だと小数点以下の時間を取得するのが難しいからです。date コマンドはGNU拡張で取得できますがPOSIXの範囲ではできません。それにテスト毎に外部コマンドを実行するとパフォーマンスの低下に繋がります。そこで思いついたのが以下の方法です。

  1. shellspec 起動時にバックグラウンドプロセスを一つ作る
  2. このバックグラウンドプロセスは無限ループで i=$((i+1)) と数を数える
  3. このバックグラウンドプロセスはシグナルを受け取ると、現在の i を記録する
  4. 各テストの開始と終了時にバックグラウンドプロセスにシグナルを送信し現在の i を記録する
  5. テスト全体の実行時間は time コマンドで取得可能
  6. 記録された i より、各テストの実行時間が計算できる。

というバックグラウンドプロセス1つでCPUをぶん回して測定するという富豪的な解決方法です(笑)

というのは、JUnitの対応とは別にプロファイラ機能を作ろうかな?と考えてて思いついたものですが、ところでそもそもテストにプロファイラって必要なのでしょうか?通常プロファイラはアプリの遅いところ、ボトルネックを調べるためにありますが、アプリの実際の使用とテストとでは関数の使い方が違います。テストで遅いとわかったからと言ってそこを直せばアプリが早くなるわけでもなく、テスト実行でのプロファイラが役に立つことはあまりないでしょう。遅いテストを調べるという意味はありますが、テストが遅いのは問題ではありますが、それと修正すべきかどうか、修正できるかどうかは別の話です。アプリは問題ないがテストだけ遅いということもあるわけです。

そういうわけで考えてはいたが実装していなかった仕組みなんですが、JUnitでは、time属性があるんですよね?これ必須なんだろうか?

ここ によると required と書かれているんですが、こことか見ると optional なんですが?公式な仕様がないからわからん。

ということで対応はできるが面倒くさいなぁという話でした。shunit2 か bats が対応したらやろう

shellspec 0.12.0 リリースしました

前回書いたのは、0.7.0の時ですか・・・。リリースするたびにブログ書かなきゃなと思いつつ、コード書いていたのでずいぶんバージョンが上がりました。

まだメジャーバージョン0なのを良いことに、互換性がない変更もガシガシしています。といってももうそろそろ落ち着いたかなーって感じですが。

ということで 0.12.0 までの変更について駆け足で紹介したいと思います。

0.8.0での変更

一番の変更点は It の意味を Example のエイリアスに変更した所です。

やっつけで作っていたサンプルと、shellspec自身のテストを見直していたのですが、英語的に適切な文章になるようにと。そうするとやっぱり、 ItExample のエイリアスであったほうが良いなと思い直したので変更しました。これで自然な文章が作れるようになりました。

でもこの「自然な文章」というのはやっぱり英語なわけで、テストを書いてるのか英語の文章の書き方を練習しているのかよくわからない状態になりましたw 意味として通るような文章にするにはどうすればよいのか、そこは英語が母国語でない人の辛い所ですね。日本語で書くなら ExampleSpecify を使ったほうがいいのかもしれません。

そしてこのバージョンでの一番の目玉は、Data Helper と %text Directive です。両方共シェルスクリプトで使いづらいヒアドキュメントを改善するためにあります。

shellspec の specfile はシェルスクリプトと互換性がある文法ですがブロック構造をサポートしています。そのためインデントを多用することになります。こんな感じですね。

Describe 'sample' # Example group
  Describe 'bc command'
    add() { echo "$1 + $2" | bc; }

    It 'performs addition' # Example
      When call add 2 3 # Evaluation
      The output should eq 5  # Expectation
    End
  End
End

シェルスクリプトはヒアドキュメントをサポートしているのですが、ここにヒアドキュメントを入れるとこんな感じになってしまいます。

Describe 'sample' # Example group
  Describe 'bc command'
    add() { echo "$1 + $2" | bc; }
    func() {
        cat <<HERE
FOO
BAR
BAZ
HERE
    }

    It 'performs addition' # Example
      When call add 2 3 # Evaluation
      The output should eq 5  # Expectation
    End
  End
End

インデントが台無しです。一応シェルスクリプトのヒアドキュメントでもインデントはできるのですがタブ文字限定のうえ、ヒアドキュメントの終わりはインデントできません。

この問題を解決するのが、Data Helper と %text Directive です。具体的にはこのように書くことができます。

Describe 'sample'
  Describe 'sort command'
    Data
        #|FOO
        #|BAR
        #|BAZ
    End

    It 'sorts'
      When call sort
      The line 1 of output should eq BAR
      The line 2 of output should eq BAZ
      The line 3 of output should eq FOO
    End
  End

  Describe 'wc command'
    func() {
        %text
        #|FOO
        #|BAR
        #|BAZ
    }
    It 'counts lines'
      When call 'func | wc -l'
      The output should eq 3
    End
  End
End

コメントの形でテキストを書いておく事ができるのでインデントを崩さずに書くことができます。

0.9.0での変更

特に大きな変更はなし^^;

0.10.0での変更

このバージョンでは、新たに %puts Directive と %putsn Directive が加わりました。これは echo の代わりに使えるもので、シェル毎に細かい違いが有る echo とは違いエスケープ文字の解釈を行わずにそのまま出力します。(puts は出力の終わりに改行なし、putsn は改行ありです。それぞれ %-%= というエイリアスが使えます。)

そしてもう一つの目玉が、テストの並列実行です。面倒なので正直作りたくなかった(笑) どうして実装したかと言うと bats-core で実装されていたからです。気づいたときは、えーそれやってるのー?って感じで。まあ bats-core が実装してるなら、対抗して実装しましょうと。

bats-core の実装は GNU parallel を使っているようです。(以前これ使って数日かけて並列でデータの変換を行ったことがあるなぁと懐かしくなりつつ)ですが、shellspec は全てのシェルで動くのもアピールポイントの一つなので & を使用したバックグラウンドプロセスを使って実装しています。排他制御やCTRL-Cによる中断などへの対応などシェルスクリプトの機能だけを使って並列処理を行うのはなかなか大変でした。

並列でテストを実行すると(テスト内容やハードウェアに依存すると思いますが)Linux環境で2~3倍ぐらいまで速度をあげられるようです。実装していて気づいたのが並列じゃなくても意外とコアを複数利用していたことです。specfileの変換、テストの実行、レポーター、と複数のプロセスをパイプを使って処理させているので設計通りではあるのですがちゃんと調べていませんでした。そしてレポーターが意外とボトルネックになっていることにも気づきました。shellspec 自体のテストでは、テストの実行は終わっているのにレポーター単体でCPUを使ってる時間が半分ぐらいあったりと。そこを改善すればもう少し速くなるかもしれません。

0.11.0での変更

このバージョンでは、前から欲しかった行番号指定によるテストの実行とDSLの前に f をつけたもの(fDescribe, fContext, fExample, fSpecify, fIt)だけを実行する機能を実装しました。(仕様は rspec のパクリですw)これでようやくテストの実行が楽になりました。

shellspec を実行してエラーがあると以下のようなエラーが表示されるのですが、この一行をコピペするだけで該当のものだけテストできるのです。便利ですね!(もちろん rspec のパクリですw)

Failure examples:

shellspec spec/04.evaluation_spec.sh:27 # 3) evaluation sample call evaluation must be one call each example FAILED
shellspec spec/04.evaluation_spec.sh:37 # 4) evaluation sample call evaluation can not be called after expectation FAILED

rspec の仕様をパクりまくってる shellspec ですが少し異なる違う部分がありまして、rspec では実行するテストの中に f がついたものが一つでもあれば、それだけを実行するのに対して shellspec では --focus オプションが必要ということです。この理由は shellspec の設計がspecfileを1パスで処理するようになっているからです。つまりspecfileの実行前にどのようなテストがあるかを確認していません。specfileの変換を行うと同時に実行しています。(唯一の例外は TAP 出力を行うときで、TAPの仕様で最初にテスト総数を出力する必要があるため先にテストの数を数えています。この場合でもテストの実行は行っていません。)

0.12.0での変更

これは落ち葉拾い的なリリースですね。shellspec ではテストの順番はファイルが見つかった順に行うのですが、rspec にはテスト順のランダム化の機能があります。これを実装するためにはシェルスクリプトだけで乱数を計算する機能があるのですが少し面倒なのと、個人的にテスト順のランダム化をあまり重視してないので、代わりに(テストを実行せずに)全specfileの一覧と全exampleの一覧を出すオプションを追加しました。これを利用するとこんな感じでランダム順でテストできます。ただし --list-examples の方は example の数だけ specfile の変換とレポート処理を行うのでかなり遅くなりますが。

shellspec $(shellspec --list-specfiles | shuf)
shellspec $(shellspec --list-examples | shuf)

そしてもう一つの追加機能が --env-from によるスクリプトファイルを使った環境変数の設定機能です。これを使うと実行してるシェルが bash や zsh などの配列をサポートしているシェルの場合だけ、追加のテストを読み込む(ための環境変数を設定する)ことができます。似たようなことは DSL の Skip で行うことができるのですが、文法によっては、dash で読み込むだけでシンタックスエラーになってしまうものがあります。そのため拡張子を変更しておき、デフォルトでは _spec.sh のみ、配列をサポートしているシェルでは spec.sh と *spec.array.sh を読み込むように環境変数 SHELLSPEC_FILTER を設定しています。まだ bash や zsh 固有の機能に対応する機能がそんなにあるわけではないのですが、これで将来的に対応しやすくなりました。

shellspec README.md 日本語版

shellspecのREADME.mdの日本語版です。

github.com

英語と日本語両方メンテナンスするのが嫌なので、日本語版はなくてもいいかなーとも思っていたんですが、気の迷いで書いちゃいました。(でもリポジトリには入れない予定)

頑張って英語で書いて、それを自分で日本語化するとかいう、なんという無駄感

あ、 Table of Contents のリンクがおかしいけど面倒なので直さないっす。ページ内検索でもしてください。

shellspec

POSIX互換シェルスクリプト用のBDDベースのテスティングフレームワーク

シェルスクリプトをテストしましょう!

f:id:koichi_nakashima:20190311010858p:plain

Table of Contents

イントロダクション

スペックファイル

Describe 'sample' # Example group block
  Describe 'bc command'
    add() { echo "$1 + $2" | bc; }

    Example 'perform addition' # Example block
      When call add 2 3 # Evaluation
      The output should eq 5  # Expectation
    End
  End

  Describe 'implemented by shell function'
    . ./mylib.sh # add() function defined

    Example 'perform addition'
      When call add 2 3
      The output should eq 5
    End
  End
End

特徴

  • POSIX互換シェルに対応 (dash、bash、ksh、busybox等)
  • BDD スタイルのシンタックス
  • シェルスクリプト言語の構文と互換性のあるスペックファイル
  • 純粋なシェルスクリプト実装
  • 最小限の依存関係 (僅かなPOSIX互換コマンドのみ使用)
  • ネスト可能なグループとレキシカルスコープ風のスコープの提供
  • Before / After フック
  • Skip / Pending 機能
  • モックとスタブ (一時的な関数のオーバーライド)
  • 内存の簡易タスクランナー
  • モダンなレポート (カラー対応, エラー行番号表示)
  • 拡張可能なアーキテクチャ (カスタムマッチャー、カスタムフォーマッター等)
  • shellspec は shellspec 自身でテスト

サポートシェル

dash, bash, ksh, mksh, pdksh, zsh, posh, yash, busybox (ash)

動作確認プラットフォーム

  • Linux (ubuntu, debian, alpine)
  • Windows 10 (WSL, cygwin, Git Bash)
  • macOS Mojave
  • Solaris 11

確認済み旧バージョン(このバージョン以降で動作するでしょう)

  • ash 0.3.8 (debian 3.0)
  • dash 0.5.3 (debian 4.0)
  • busybox ash 1.1.3 (debian 4.0)
  • bash 2.03 (debian 2.2)
  • zsh 3.1.9 (debian 2.2)
  • pdksh 5.2.14 (debian 2.2)
  • mksh 28 (debian 4.0)
  • ksh93 93q (debian 3.1)
  • ksh88 0.5.11 (solaris 11)
  • posh 0.3.14 (debian 3.1)
  • yash 2.30 (debian 7)

依存関係

shellspec はシェルスクリプトによって実装されているので、そのため動作に 必要なものはシェルと僅かなPOSIX互換コマンドのみです。

現在使用している外部コマンド

date, mkdir, rm, mv (推奨: printf, ps, readlink, time)

チュートリアル

インストール

shellspec をダウンロードしてPATHにシンボリックリンクを作成するだけです

$ cd /SOME/WHERE/TO/INSTALL
$ wget https://github.com/ko1nksm/shellspec/archive/{VERSION}.tar.gz
$ tar xzvf shellspec-{VERSION}.tar.gz

$ ln -s /SOME/WHERE/TO/INSTALL/shellspec-{VERSION}/shellspec /EXECUTABLE/PATH/
# (例 /EXECUTABLE/PATH/ = /usr/local/bin/, $HOME/bin/)

またはシンボリックリンクを作成するために shellspec を作成します (もし readlink がない場合)

$ cat<<'HERE'>/EXECUTABLE/PATH/shellspec
#!/bin/sh
exec /SOME/WHERE/TO/INSTALL/shellspec-{VERSION}/shellspec "$@"
HERE
$ chmod +x /EXECUTABLE/PATH/shellspec

入門

プロジェクトディレクトリを作成して shellspec --init を実行します。

# プロジェクトディレクトリの作成
$ mkdir <your-project-directory>
$ cd <your-project-directory>

# 初期設定
$ shellspec --init
  create .shellspec
  create spec/spec_helper.sh

# スペックファイルを作成します (好みのテキストエディタを使用できます)
$ cat<<'HERE'>spec/hello_spec.sh
Describe 'hello.sh'
  . lib/hello.sh
  Example 'hello'
    When call hello shellspec
    The output should equal 'Hello shellspec!'
  End
End
HERE

# lib/hello.sh の作成
$ mkdir lib
$ touch lib/hello.sh

# 関数を実装していないため失敗します
$ shellspec

# hello 関数の作成 (好みのテキストエディタを使用できます)
$ cat<<'HERE'>lib/hello.sh
hello() {
  echo "Hello ${1}!"
}
HERE

# 成功します
$ shellspec

サンプル

sample directoryを参照、 shellspec ディレクトリで shellspec sample で実行できます。

基本構造

これらのDSLで構造化された Example を記述できます。

DSL 説明
Describe Example group ブロックを定義します。Example group はネスト可能です。
Context Describe の別名です。
Example Example ブロックを定義します。 この中にexampleを書きます。
Specify Example の別名です。
End Example group または Example ブロックの終了です。
Todo 空の example と同じですが、ブロックではありません。実装予定を示すワンライナー構文です。

ネスト可能なグループとスコープ

スペックファイルは正当なシェルスクリプトの構文ですが、スコープや行番号を実装するために変換処理を行っています。

それぞれの example group ブロック と example ブロックはサブシェルに変換されます。 そのためブロックの中で行われた変更はブロックの外に影響を与えません。

つまりローカル変数とローカル関数をスペックファイルで実現しています。 これは構造化されたスペックを記述するのにとても便利です。

もしどのような変換が行われるのか興味がある場合は、 --translate オプションを使用してください。

一時的なブロックのスキップ

Describe, Context, Example, Specify ブロックの頭に x をつけて xDescribe, xContext, xExample, xSpecify とすることで一時的にブロックの実行をスキップできます。

フック

example の前後に実行されるフックを定義できます。

DSL 説明
Before example 実行前に呼ばれるフックを定義します。
After example 実行後に呼ばれるフックを定義します。

モック と スタブ

現在、shellspec はモックやスタブのための特別な関数は提供していません。 しかしシェル関数を再定義することで既存のシェル関数や外部コマンドをオーバーライドできます。 これをモックやスタブとして代用することが出来ます。

Describe, Context, Example, Specify ブロックがサブシェルで実行されることを思い出してください。 ブロックを抜けた時、オーバーライドした関数は元に戻ります。

Example

example ブロックは、仕様を記述する場所です。 最大一つの Evaluation と 複数の Expectations から構成されます。

Evaluation

検証のためのアクションを定義します。それぞれの Example は 最大一つの Evaluation を記述できます。

When call             echo hello world
     <- evaluation type ->
DSL 説明
When evaluation を定義します。
evaluation type 説明
call シェル関数または外部コマンドを実行します。
invoke シェル関数または外部コマンドをサブシェルで実行します。
run 外部コマンドを実行します。

通常は call を使用することでしょう。 invokecall に似ていますが、サブシェルで実行します。 invokeevaluation のみで関数をオーバーライドしたい 時と exit をトラップしたい時に便利です。

Expectation

検証する内容を定義します。

DSL 説明
The The ステートメントを定義します。
It It ステートメントを定義します。

The ステートメント

これは一番基本的な Expectation です。検証対象(subject)が予想された値であるかを評価(Evaluate)します。

The output        should equal         4
    <- subject ->        <- matcher -> <- expected value ->

modifier は 評価前に subject を加工します。

The line 2  of     output        should equal 4
    <- modofier -> <- subject ->

modifier はつなげることが出来ます。

The word 1 of line 2  of    output        should equal 4
    <- multiple modifier -> <- subject ->

modifier の最初の引数が数値の場合は、代わりに序数を使用することも出来ます。

The second line of output        should equal 4
    <- modofier -> <- subject ->

It ステートメント

It ステートメントThe ステートメント のシンタックスシュガーです。長い主語を避けるために使用できます。

注意: rspecとは違い、ItExample の別名ではありません。

以下の2つの文章は同じ意味です。

The word 1 of line 2 of output should equal 4
It should equal 4 the word 1 of line 2 of output

ランゲージチェイン

shellspec は chai.js に似たランゲージチェインをサポートします。

ランゲージチェインは可読性を上げるためだけのものです。 Expectation の実行には影響を与えません。

  • a
  • an
  • as
  • the

以下の2つの文章は同じ意味です。

The first word of second line of output should valid number
The first word of the second line of output should valid as a number

Subject

検証を行う対象です。

Subject (検証対象) 説明
output
stdout
Evaluation の標準出力を subject として使用します。
error
stderr
Evaluation の標準エラー出力を subject として使用します。
status
exit status
Evaluation の終了ステータスを subject として使用します。
funciton <NAME> 関数実行の標準出力を subject として使用します。
path <PATH>
file <PATH>
dir <PATH>
(別名を解決した) パスを subject として使用します。
value <VALUE>
string <VALUE>
指定した値を subject として使用します。
variable <NAME> 指定した変数の値を subject として使用します。

Modifier

検証を行う対象を加工します。

Modifier 説明
line <NUMBER> subject の指定された行を subject として使用します。
lines subject の行番号を subject として使用します。
word <NUMBER> subject の指定された単語を subject として使用します。
contents ファイルの中身を subject として使用します。 (現在の subject はファイルパスであること)
length subject の長さを subject として使用します。

Matcher

検証を行う処理の内容です。

exit status (subjectは終了ステータスであることを想定)

Matcher 説明
be success 終了ステータスは成功であること
be failure 終了ステータスは失敗であること

stat (subjectはファイルパスであることを想定)

Matcher 説明
be exist ファイルは存在していること
be file ファイルはファイルであること
be directory ファイルはディレクトリであること
be empty ファイルは空であること
be symlink ファイルはシンボリックリンクであること
be pipe ファイルはパイプであること
be socket ファイルはソケットであること
be readable ファイルは読み取り可能であること
be writable ファイルは書込み可能であること
be executable ファイルは実行可能であること
be block_device ファイルはブロックデバイスであること
be charactor_device ファイルはキャラクターデバイスであること
has setgid ファイルは setgid フラグを持っていること
has setuid ファイルは setuid フラグを持っていること

valid

Matcher 説明
be valid number subject は数字として正しいこと
be valid funcname subject は関数名として正しいこと

variable (subject は変数名であることを想定)

Matcher 説明
be defined 変数は定義されていること (set)
be undefined 変数は未定義であること (unset)
be blank 変数は空であること (unset または 空文字)
be present 変数は存在すること (空文字でない文字列)

string

Matcher 説明
start with <STRING> subject は <STRING> で始まっていること
end with <STRING> subject は <STRING> で終わっていること
equal <STRING> subject は <STRING> と一致していること
include <STRING> subject は <STRING> を含んでいること
match <PATTERN> subject は <PATTERN> とマッチすること

other

Matcher 説明
satisfy <FUNCTION> [ARGUMENTS...] subject <FUNCTION> を満たすこと

Helper

DSL 説明
Set 変数に値を代入します。
Unset 変数を未定義にします。
Path
File
Dir
パスの別名を定義します。
Debug デバッグメッセージを出力します

Path alias

読みやすさのために長いパスに短い名前を定義することが出来ます。

Example 'not use path alias'
  The file "/etc/hosts" should be exist
End

Example 'use path alias'
  File hosts="/etc/hosts"
  The file hosts should be exist
End

Skip と Pending

現在実行しているブロックを Skip または Pending します。

DSL 説明
Skip <REASON> 現在のブロックを Skip します。
Skip if <REASON> <FUNCTION> [ARGUMENTS...] 現在のブロックを条件付きで Skip します。
Pending <REASON> 現在のブロックを Pending します。

shellspec コマンド

デフォルトオプションの設定

shellspec コマンドのデフォルトオプションを変更する場合は、オプションファイルを作成します。 以下の順番で読み込みを行い、オプションを上書きます。

  1. $XDG_CONFIG_HOME/shellspec/options
  2. $HOME/.shellspec
  3. ./.shellspec
  4. ./.shellspec-local (VCSで管理しないでください)

タスクランナー

--task オプションでタスクの実行を行います。

環境変数

Name 説明
SHELLSPEC_ROOT shellspec ルートディレクトリ 指定されていない場合は自動的判定します。(もし readlink がない場合は失敗するでしょう)
SHELLSPEC_LIB shellspec lib ディレクトリ 指定されていない場合は $SHELLSPEC_ROOT/lib
SHELLSPEC_LIBEXEC shellspec libexec ディレクトリ 指定されていない場合は $SHELLSPEC_ROOT/libexec
SHELLSPEC_TMPDIR shellspec が使用する一時ディレクトリ 指定されていない場合は $TMPDIR or /tmp
SHELLSPEC_SPECDIR スペックファイルのディレクトリ 現在のディレクトリ下の spec ディレクトリです
SHELLSPEC_LOAD_PATH ライブラリのロードパス $SHELLSPEC_SPECDIR:$SHELLSPEC_LIB:$SHELLSPEC_LIB/formatters

スペックディレクトリ以下の特殊ファイルとディレクトリ

spec/spec_helper.sh

spec_helper.sh--require spec_helper オプションでロードされます。

このファイルは example の実行のための準備を行います。(カスタムマッチャーの定義等)

spec/support/

このディレクトリはカスタムマッチャーやタスクファイルを作成に利用されます。

spec/banner

もし spec/banner ファイルがある場合、shellspec コマンド実行時にバナーを表示します。 バナー表示を無効にする場合は --no-banner オプションを使用します。

qiitaにシェルスクリプトのecho関連の記事を2本投稿しました

qiita.com

qiita.com

こういう記事はqiitaに書くかブログに書くか迷いますね。

とりあえず技術的な話はqiitaに書いたとして、ここには雑談的なことを書くことにしましょう。

echoに関するのこの問題は、shellspec の開発中に問題になりました。

shellspecはPOSIX互換のシェルで動く方針であるため、各シェルの互換性問題を解決しなければなりませんでした。一番基本であるechoに互換性がなかったのは辛かったですね。

shellspecはユーザーが書いた文字列をエラーメッセージとして(しかも色付きで)表示することがあるため、文字列にエスケープシーケンスが入る可能性を否定できません。引数の文字列をそのまま出力する必要があります。文字列をそのまま出力するには printf '%s\n' を使えば良いのですが、そうすると一部のシェルで極端に遅くなってしまいました。ビルトインではなく外部コマンドのprintfを呼び出しているからです。WSL上だとさらに遅くなってしまいました。

printfがなくともZSHやKSHでは似たような事ができるprintが使えるので問題ないのですがposhが大変でした。(使ってる人は殆ど居ないと思うんですが)

poshはprintfはビルトインではない。printはない。どうしたもんかと。仕方がないのでエスケープ文字の\\\に変換するようロジックで頑張ることになりました。まあ大したコードじゃないですけどね。

もちろんそのテストはshellspec自身で行っています。

ターミナル操作のデモ動画作成用のツール (ghostplay) を作った

こういうのをやりたかったんですよ。

これは https://shellspec.info に載せているデモ動画です。何も知らない初見さんに対して、実際に使ってもらうことなくどういう事ができるというのをぱっと見せることが出来ます。

この動画は、動画の右下にも書いてあるように asciinema - Record and share your terminal sessions, the right way というサービスを利用しています。 shellspec が大きく影響を受けている rspec で使っているのを見て知りました。実はこれ画像ではなくテキストなのでコピペできるんですよ。

asciinema はターミナル操作を録画して、それをシェアするためのツールです。

ただですね? ターミナルを操作するのは人間なんですよ。

ライブコーディングとか一発勝負、ミスも含めて全部録画。みたいなのであればそれでも良いのですが、プログラム紹介のデモ動画にそこまでの緊張感やライブ感はいりません。また将来修正が入るかもしれませんし、そのたびに長い文章を何度も入力するなんで面倒な作業ですよね?

ということで作ったのが、スクリプトの自動入力ツール ghostplay です。

github.com

使い方は簡単で、

1. 適当なシェルスクリプトを書きます。

[example/script.sh]

#!/bin/sh
echo This is your script.
echo ghostplay types your script and execute.

2. ghostplayを使用して実行します。

ghostplay example/script.sh

するとこのようにスクリプトを1文字1文字入力して実行します。(まるで幽霊が演奏するかのように)

f:id:koichi_nakashima:20190304181428g:plain

↑これは asciinema ではなく ttyrec で録画したものを seq2gif でアニメーションGIFに変換しています。GitHubのREADME.md に乗せる場合に便利です。

ghostplay 自体はスクリプトの自動入力ツールなので、ターミナル操作の録画を行う時は asciinema や ttyrec と組み合わせて使います。 asciinema と ttyrec の両方に録画開始と同時にコマンド実行するオプションがあるので以下のように簡単に連携させることが出来ます。

asciinema

# Record terminal
asciinema rec -c "ghostplay example/script.sh"

ttyrec & seq2gif

# Record terminal to a file "ttyrecord'
ttyrec -e "ghostplay example/script.sh"

# Convert to animated gif
seq2gif -i ttyrecord -o demo.gif

類似ソフト

spielbash

似たようなソフトに spielbash というのを見つけました。リポジトリが複数あって分かりづらいのですが以下のような関係みたいです。

  1. https://github.com/redhat-cip/spielbash オリジナル? 2016年頃に開発。Python製
  2. https://github.com/justinazoff/spielbash 1.をフォークして2017年頃に修正を加えたもの
  3. https://github.com/Malinskiy/spielbash 1.をRubyで書き直したもの。2018年頃に開発。多分一番高機能。

おそらく機能的には spielbash の方が上です。ghostplay ではシェルスクリプトで自動実行できるものを対象としているため vim や irb 等の対話型ツールを自動操作できませんが spielbash (3.のやつ) では scenario_1.yaml を見る限り、それができるようです。

それに対する ghostplay の優位性はスクリプトファイルの記述が簡単であるという点です。ghostplay のスクリプトファイルはシェルスクリプトそのものです。(実際にシェルスクリプトとしてシェル上で実行しています。)操作内容を書いたシェルスクリプトを作成すれば、それがそのまま(もしくはわずかな修正で) ghostplay のスクリプトファイルとして使用できます。