fish - rangerの多重起動を避ける設定

rangerというCLIのファイラがあるのを知って最近使っている。
参考: CLI で Linux ファイルマネージャ ranger を使うことのメモ

Macならbrew install rangerで入る。homebrewほんと便利。

そんなrangerの設定というかカスタマイズで「多重起動を避ける」というのがある。
上記の参考サイトではrangerの多重起動を避けるという項目で説明されている。

ここで書かれている設定はbash/zsh向けなので、fishの場合の書き方をメモ。

rangerの多重起動を避ける

bash/zshではこうなっている。

ranger() { [ -n "$RANGER_LEVEL" ] && exit || command ranger "$@"; }

fishでは次の定義を~/.config/fish/config.fishに定義する。
(~/.config/fish/functions/配下にファイルを作っても良いんだろうけど)

# ranger->サブシェル->rangerの多重起動を避ける
function ranger
  if set -q -x RANGER_LEVEL
    exit
  else
    command ranger $argv
  end
end

参考サイトにある通りRANGER_LEVEL変数が定義されていればexitして、それ以外(初回)なら普通にrangerを起動する。
注意としてはサブシェルからranger /tmpのように引数有りで起動してもexitして元のrangerに戻るだけなので、引数で渡したディレクトリで起動しない。

rangerのサブシェルの場合はプロンプトに表示

bash/zshではこうなっている。

[ -n "$RANGER_LEVEL" ] && PS1="(RANGER) $PS1"

fishでは次の定義を~/.config/fish/config.fishに定義する。
(~/.config/fish/functions/配下にファイルを作っても良いんだろうけど)

# rangerのサブシェルから起動された場合はpromptを変更する
if set -q -x RANGER_LEVEL
  set -g _origin_prompt (fish_prompt)
  function fish_prompt
    echo "(RANGER)$_origin_prompt"
  end
end
  1. 上記と同じようにRANGER_LEVELが設定されているかどうかで、通常のシェルかrangerサブシェルかを判断
  2. (fish_prompt)の実行結果、つまり既存のプロンプト表示を変数に保持する
    • -gでグローバル定義にしないとfish_prompt関数から見れなかった。ちょっと気持ち悪い
  3. fish_promptを再定義する

これで↓のように(RANGER)が付いた状態でプロンプトが表示される。

(RANGER) ~/.config/fish ⟩

PHPのテストで本当にdataProviderを使う必要があるのだろうか

PHPUnitにはdataProviderという機能がある。
パラメタライズドテスト、またはテーブルドリブン(駆動)テストで書くための仕組み。

僕はずっとこの機能が使いづらくて、普通にforeachで回す方が良いのではと思っている。
今日たまたまdataProviderを推奨している記事を続けて目にしたので、改めて考えを整理しておく。

dataProviderのメリット

foreachと比較したdataProviderのメリットはこちらの記事が分かりやすかったので引用する。
PHPUnitとデータプロバイダとテストケース生成

メリット1: setUp/tearDownが使える

確かにモックを使うテストならその方が助かる。

メリット2: どのデータでテストが落ちたのか分かりやすい

確かにforeachだとちょっと面倒。Goでテストを書いている時にt.Error()をいちいち書いていく感覚に似てる。

メリット3: 例外のテストとの組み合わせ

確かに例外が発生した段階で止まっちゃうと面倒。

dataProviderの(個人的)デメリット

上記のメリットはどれも同意するけど、個人的にはそれ以上にデメリットを感じてしまう。
それはテストの本体とdataProviderのメソッドが離れていること。ロジックとデータは近いところにある方が読みやすいと思うけど好みなのかもしれない。

例えばPHPUnitリポジトリにあるこのテストコード。
https://github.com/sebastianbergmann/phpunit/blob/cca308e970eebb1f21cd702071b09bfd40a1c705/tests/unit/Util/RegularExpressionTest.php

  1. testValidRegexのテストを読もうとする
  2. validRegexpProviderの定義を見る

という視線の移動と記憶が必要になる。
これなら↓のようにテスト本体とデータは一緒にあった方が個人的には読みやすい。

<?php
public function testValidRegex(): void
{
    foreach([
        ['#valid regexp#', 'valid regexp', 1],
        [';val.*xp;', 'valid regexp', 1],
        ['/val.*xp/i', 'VALID REGEXP', 1],
        ['/a val.*p/', 'valid regexp', 0],
    ] as $case) {
        $this->assertEquals($case[2], RegularExpression::safeMatch($case[1], $case[0]));
    }
}

(エラーメッセージとか一時変数に置くとかは省いている)

もちろんdataProviderの方が適切というケースもあると思うけど、同様に普通にforeachの方が良いケースもあると思う。何でもかんでもdataProviderを使うのは違う気がする。
このテストとか冗長なだけなのでは。
ただ「使うかどうかは個人の判断」にしちゃうとテストの書き方に統一性がなくなるから、プロジェクトとしては「必ず使う」でルール化した方がいいのかもしれない。

また個人的に読みにくいと感じる理由の一つは、dataProvider側だけを見ても値の意図が分からないから。

<?php
public function canonicalizeProvider(): array
{
    return [
        ['{"name":"John","age":"35"}', '{"age":"35","name":"John"}', false],
        ['{"name":"John","age":"35","kids":[{"name":"Petr","age":"5"}]}', '{"age":"35","kids":[{"age":"5","name":"Petr"}],"name":"John"}', false],
        ['"name":"John","age":"35"}', '{"age":"35","name":"John"}', true],
    ];
}

これはphpunit/tests/unit/Util/JsonTest.phpから拝借した。
それぞれの値の意図は、ここを見ただけでは分からない。
テストコード側の引数を見て初めて理解する。

<?php
public function testCanonicalize($actual, $expected, $expectError): void

テストコードとデータを行き来しないと読み解けない。これが僕にはつらい。

結局どっちが良いのか

最初に引用したdataProviderのメリットのうち2つは、「テストが落ちた時のメンテ性を上げる」ためのものだった。
僕があげたdataProviderのデメリットは「他人がコードを読む時」を想定したものだった。

つまり優劣というより重要視する観点が違うんだと思う。僕は可読性を大事にしている。でも「dataProviderの方が見やすい。foreachの方が見づらい」って人もいるだろうから、そうなるとforeachの方が良い理由はほとんどないのかな。

phpcon2018のLTで見たPREG_JIT_STACKLIMIT_ERRORについて調べた

先日のPHP Conference 2018でこのようなLTがありました。
RegExp Error caused by PHP upgrade 5.6 to 7.2

スライドはLT用にシンプルなので、同じ発表者さんのQitta記事を見た方が伝わるかもしれません。
PHPを7.2にバージョンアップしたら正規表現でマッチしない現象に出くわした

上記の背景は「5.6から7.2へのバージョンアップで発生した」わけなので、5.6と同じ挙動になるならpcre.jit=0で問題ないという判断かと思います。
(Qiitaにも書いてある通り本来はロジックを変えるのが理想だが、それが困難な状況ではJITを無効にするしかない)

でも、何か気になったので調べました。先に述べておくと、pcre.jit=0にするという結論に違いはありません。

簡潔なまとめ

安定のstackoverflow。ベストアンサーで簡潔にまとまっています。
PHP PREG_JIT_STACKLIMIT_ERROR - inefficient regex
ちなみにPHP7.2であれば、この質問者のコードをそのままコピペして実行すれば再現できます。
最後にvar_dump(preg_last_error());を追加すれば6(PREG_JIT_STACKLIMIT_ERROR)が表示されます。

後述するようにテキスト量ではなく正規表現の複雑さが問題になるので、自分で再現するコードを書くのは面倒なため助かります。
ただしこの記事の最後に書いた通り、PHP7.3だと正常終了します。

詳細なまとめ

PCRE JITの仕様書から

https://www.pcre.org/original/doc/html/pcrejit.html よりいくつか抜粋します。

JIT support applies only to the traditional Perl-compatible matching function. It does not apply when the DFA matching function is being used. The code for this support was written by Zoltan Herczeg.

いきなりですが、ここは良くわからなかった。
JITPerl互換のあるマッチングの場合のみ利用するよ。DFAマッチングの場合はJITは効かないからね」とありますがPCREはNFAなのでは?DFAで動くケースがあるんだろうか。

JIT support is an optional feature of PCRE. The "configure" option --enable-jit (or equivalent CMake option) must be set when PCRE is built if you want to use JIT

「pcreをインストールする時に--enable-jitオプションを付ければJITが有効になるよ」
PHP7からPCRE JITはデフォルト有効なので強制的に--enable-jitを付けているのかな。(未確認)

When a pattern is matched using JIT execution, the return values are the same as those given by the interpretive pcre_exec() code, with the addition of one new error code: PCRE_ERROR_JIT_STACKLIMIT. This means that the memory used for the JIT stack was insufficient

本題。
JITによる実行ではpcre_exec()と同じ実行結果に加え、PCRE_ERROR_JIT_STACKLIMITという新しいエラーコードを追加します。このエラーコードはJITスタックのメモリ確保が不十分という意味です」
PCRE_ERROR_JIT_STACKLIMITは冒頭のPREG_JIT_STACKLIMIT_ERRORと違う定数ですが、これは後述のコードリーディングにて。

When the compiled JIT code runs, it needs a block of memory to use as a stack. By default, it uses 32K on the machine stack. However, some large or complicated patterns need more than this. The error PCRE_ERROR_JIT_STACKLIMIT is given when there is not enough stack.

JITの実行にはスタック用にメモリを確保します。これはデフォルトでは32KBです。もしこのサイズで不十分な場合はPCRE_ERROR_JIT_STACKLIMITを発生させます」
デフォルトでは、とあるものの設定値をいじれるのはPCREのソースレベル(コンパイル前のC言語レベル)なので、PHP側から変更できる手段はないはず。

PCRE (and JIT) is a recursive, depth-first engine, so it needs a stack where the local data of the current node is pushed before checking its child nodes

「PCRE(とJIT)は深さ優先探索なので、ノードを確保するためにスタックが必要なんだ」

つまり↓のような理解です。

  • PCRE(とJIT)は正規表現のロジックとして深さ優先探索であり、各ノードを読み込むためにメモリ確保が必要
  • この処理のために使われるメモリは32KBで、これを超えるとエラーが発生する
    • この上限値はPHP側では変更できない
  • 深さ優先探索用のノードなので、テキスト量ではなく正規表現の書き方に依存する

PCREのソースコードから

PHP7.2.13のソースコードを読んでいます。
まず冒頭にあった発端のPREG_JIT_STACKLIMIT_ERRORの定義から。

https://github.com/php/php-src/blob/1549c6d26ed866f0322ba740effc7820a54805c8/ext/pcre/php_pcre.c#L222

REGISTER_LONG_CONSTANT("PREG_JIT_STACKLIMIT_ERROR", PHP_PCRE_JIT_STACKLIMIT_ERROR, CONST_CS | CONST_PERSISTENT);

PHP_PCRE_JIT_STACKLIMIT_ERRORという値を使って定義されていて、この定数はここで利用されている。

https://github.com/php/php-src/blob/1549c6d26ed866f0322ba740effc7820a54805c8/ext/pcre/php_pcre.c#L105-L109

#ifdef HAVE_PCRE_JIT_SUPPORT
  case PCRE_ERROR_JIT_STACKLIMIT:
    preg_code = PHP_PCRE_JIT_STACKLIMIT_ERROR;
    break;
#endif

エラーコードがPCRE_ERROR_JIT_STACKLIMITだったら代入していて、このエラーコードはたぶんこの辺りの処理。たぶん。

https://github.com/php/php-src/blob/1549c6d26ed866f0322ba740effc7820a54805c8/ext/pcre/pcrelib/pcre_jit_compile.c#L11249-L11250

/* Allocating stack, returns with PCRE_ERROR_JIT_STACKLIMIT if fails. */
/* This is a (really) rare case. */

「この例外はレアなケースさ(マジで)」というコメントから始まり

https://github.com/php/php-src/blob/1549c6d26ed866f0322ba740effc7820a54805c8/ext/pcre/pcrelib/pcre_jit_compile.c#L11271-L11272

/* We break the return address cache here, but this is a really rare case. */
OP1(SLJIT_MOV, SLJIT_RETURN_REG, 0, SLJIT_IMM, PCRE_ERROR_JIT_STACKLIMIT);

「でもコレはマジでレアなケースだから」というコメントに終わる。
ここの処理はC言語力が無くて良く分からなかった。雰囲気でC言語を読んでいる僕はここが限界。

コメントにある通り実装者はレアなケースだと想定したんだろうけど、ググってみるとそれなりにハマっている人はいるようだった。

PHP7.3でスタックサイズの確保が増えた

「動いていた正規表現が動かなくなった」というバグ報告により、スタックサイズが多めに確保されるようになりました。
(php-src側に変更が入った)

そのため、冒頭のstackoverflowのサンプルコードではエラーが再現しません。
結局サイズを超えたら同じ現象になりますが、7.2の頃よりは発生しにくくなったのだと思います。

fishとzsh - プロセス置換などコマンドの書き方の違いあれこれ

普段はfishを使っていて満足だけど、次のようなケースでbash/zshとの違いに戸惑うことがある。

  • Web上の記事からコピペで実行したい時
    • そういえばfishのコマンドがそのまま載った記事はほとんど見ない気がする
  • チーム開発をしていて、他の人がシェアしたコマンドをコピペ実行したい時
  • チーム開発をしていて、自分が使ったコマンドを他の人にシェアしたい時
    • そのまま載せたらbash/zsh環境では動かない時

ほとんどの場合エラーメッセージに答えがあるけど一応メモしておく。
以下のコマンド例は注釈がなければfishを指す。

変数定義

zshではAAA=111と書けるが、fishではsetを使う。

AAA=111
Unsupported use of '='. In fish, please use 'set AAA 111'.

⟩ set AAA 111echo $AAA
111

参考: set:シェル変数の設定・一覧・消去・確認する6活用

RAILS_ENV=test rails consoleみたいな書き方

コマンド実行時だけ変数定義をしたい時、zshと同じような指定はエラーになる。

RAILS_ENV=test ./bin/rails c
Unsupported use of '='. To run './bin/rails' with a modified environment, please use 'env RAILS_ENV=test ./bin/rails…'

エラーメッセージに書いてある通りenvを付ける。

⟩ env RAILS_ENV=test ./bin/rails c
Running via Spring preloader in process 59475
Loading test environment (Rails 5.0.0.1)

&&や||の書き方

fishとzshの違いで良く見るケース個人的1位。
fishでは&&||をサポートしていない。代わりに;and/orを使う。

echo 1 && echo 2
Unsupported use of '&&'. In fish, please use 'COMMAND; and COMMAND'.
fish: echo 1 && echo 2echo 1 ;and echo 2
1
2

# orの例
⟩ cat aa ;or echo 2
cat: aa: No such file or directory
2

ちなみにandを忘れて;だけにすると、コマンドが失敗しても継続されるので注意。

# andがないと継続される
⟩ cat a ; echo 2
cat: a: No such file or directory
2

# andがあれば継続されない
⟩ cat a ;and echo 2
cat: a: No such file or directory

終了ステータスのとり方

$?ではなくて$statusを使う。

echo $?
$? is not the exit status. In fish, please use $status.
fish: echo $?
            ^

⟩ echo $status
0

$(...) - コマンド置換

コマンドの実行結果を他のコマンドに渡す時の書き方。

⟩ cat $(echo out.txt)
$(...) is not supported. In fish, please use '(echo)'.
fish: cat $(echo out.txt)
          ^

⟩ cat (echo out.txt)
this is out

なおfishではバッククォートをサポートしていない。

⟩ cat `echo out.txt`
cat: `echo: No such file or directory
cat: out.txt`: No such file or directory

<(...) - プロセス置換

例えばディレクトリ内に含まれるファイル一覧の差分を取りたいとき。

/Users/kanno/tmp% tree dir1 dir2
dir1
└── a.txt
dir2
└── b.txt

zshでは<()を使うことで一時ファイルを通すことなく比較できる。
参考: 一時ファイルはもういらない - プロセス置換

# これはzsh
/Users/kanno/tmp% diff -u <(ls dir1) <(ls dir2)
--- /dev/fd/11  2018-12-15 05:17:56.000000000 +0900
+++ /dev/fd/12  2018-12-15 05:17:56.000000000 +0900
@@ -1 +1 @@
-a.txt
+b.txt

普段使わないけど、前職でサーバーに入ってサクッと使っている人がいてカッコいいと思った。

同じことをfishでやるにはpsubを組み合わせる。

⟩ diff -u (ls dir1 | psub) (ls dir2 | psub)
--- /var/folders/n5/z3tch4l11q796wz2m5m_6xd80000gn/T//.psub.eIiVpGBx3M  2018-12-15 05:20:18.000000000 +0900
+++ /var/folders/n5/z3tch4l11q796wz2m5m_6xd80000gn/T//.psub.QhISb6ERoH  2018-12-15 05:20:18.000000000 +0900
@@ -1 +1 @@
-a.txt
+b.txt

参考: psub:fish独自のプロセス置換を行う

リダイレクトの書き方

標準エラー出力^を使う。追記する場合は^^

⟩ cat aaa ^out.txt

⟩ cat out.txt
cat: aaa: No such file or directory

存在しないファイルを上書きしないようにするには>?^?を使う。

echo aa >? out.txt
The file 'out.txt' already exists

標準出力と標準エラー出力を同時に出すにはこう書く。

echo aa > out.txt ^&1

絶対忘れるが、bash/zsh2>&1も毎回ググっているので問題ない^^

ワイルドカードの解釈

**/*.phpのような指定をすると、zshと違いカレントディレクトリは対象外になる。
カレントディレクトリも対象とするには***.phpとする。

ls **/*.php
php/empty.php

⟩ ls ***.php
algo.php
php/empty.php

iTerm2からTerminal.appに戻ろうとして断念した

最近Vimとターミナル環境をリニューアルしている。
これまでずっとiTerm2を使っていたけど特に理由はなかった。最初にMac環境を色々構築した時に「iTerm2がナウいよ」って記事を読んで、良く分からないまま適当に入れただけだった。
標準のTerminal.appに出来なくてiTerm2じゃないと出来ないこと(使う理由)も良く分かっていない。

「背景画像を設定できる」のと「縦分割ができる」というのを良く見かけるが、僕は背景画像を設定しないし、縦分割はtmuxで出来るので必要ない。

そんなわけでiTerm2をやめてTerminal.appに戻ろうとしたんだけど、日本語を入力した時の挙動が気になってしまった。

f:id:kanno_kanno:20181213035929g:plain

入力中の日本語に応じて右にずれていく。撮り忘れたけど確定すると元の表示位置に戻る。
ちなみにGifでは縦分割だが、1ペインで入力中も時刻など右側に表示しているものがずれていく。
iTerm2だと再現しない。

調査する時間は持てず、結局iTerm2に戻ってきた。