唯物是真 @Scaled_Wurm

プログラミング(主にPython2.7)とか機械学習とか

nodenvに小さなバグを見つけてプルリクエストを送ってマージされた

仕事で使っていたコードのCIのスクリプトがある日突然動かなくなって、調べたらデバッグ出力用にnodenv versionsのコマンドを実行していたのが原因だったのがわかりました
少しの修正で直りそうな問題だったので休日に直してプルリクエストを送りました
英文を書くのは久々だったので苦労しましたが、プルリクエストをしたらすぐにマージしていただいたのでありがたかったです

github.com

f:id:sucrose:20190128001517p:plain

バグの原因が、あまりBashに慣れていない自分が把握してなかった仕様の話でおもしろかったのでメモ代わりに記事にしておきます

詳細

`nodenv versions` exit(1) unexpectedly when no "system" Node is installed · Issue #131 · nodenv/nodenv · GitHub

問題

nodenvはプロジェクトごとなどで様々なnodeのバージョンを切り替えながら使いたいときに使うソフトウェアでnodenv versionsを実行するとインストールされているnodeのバージョン一覧が出力されます
このときnodenv用にインストールしたnodeとシステムに元から入っているnodeが出力されるのですが、あるコミットの後からnodenv用にインストールしたnodeはあるけど、システムのnodeがない場合エラーを出して終了するようになっていました

if [ "$num_versions" -eq 0 ] && [ -n "$include_system" ]; then
  echo "Warning: no Node detected on the system" >&2
  exit 1
fi

$num_versionsにはインストールされているnodeの個数が入っていて、本来1つ以上nodeがインストールされていれば上のif文の中身は実行されないはずなのですが、何故かこの分岐に入るようになっていました

原因

最近のコミットを見ると$num_versionsは以前はforループの中で計算されていたのが、以下のようなパイプでwhileに渡してループするというコードに修正がされていました(print_versionの中でカウントされています)

{
  shopt -s nullglob
  for path in "$versions_dir"/*; do
    if [ -d "$path" ]; then
      if [ -n "$skip_aliases" ] && [ -L "$path" ]; then
        target="$(realpath "$path")"
        [ "${target%/*}" != "$versions_dir" ] || continue
      fi
      echo "${path##*/}"
    fi
  done
  shopt -u nullglob
} \
| sort_versions \
| while read -r version; do print_version "$version"; done

一瞬見ただけだと正しそうに見えるのですがこれがバグの原因でした

パイプで渡すとコマンドがサブシェルで実行されるので、| while read -r version; do 何らかの処理; doneの中で変数を操作しても外側の同名の変数には影響しません

count=0
seq 1 10 | while read -r x; do count=$(($count+1)); done
echo $count # $count は上のwhileループに影響されずに0のまま

www.atmarkit.co.jp

修正

というわけで以下のようなパイプを使わない形に修正して無事解決しました

count=0
while read -r x; do
  count=$(($count+1))
done < <(seq 1 10)
echo $count # $count は10になる

雑学

ちなみにzshだとまた動作が違ってパイプで繋がれた最後のコマンドの場合同じスコープで計算されるようです。bashでも shopt -s lastpipeすると同様になるらしい(シェルを対話的に動かしてるとlastpipeしてもダメだった)
パイプラインとサブシェルの問題はシェル依存 - 拡張 POSIX シェルスクリプト Advent Calendar 2013 - ダメ出し Blog

MySQLでヌル文字(NUL)などの制御文字が含まれている文字列を探す方法のメモ

ヌル文字(NUL) の判定

MySQLではnull文字は '\0'CHAR(0) で表せます

なので ヌル文字を含む文字列は 文字列 LIKE '%\0%' のような感じで判定できます(他の文字列系の関数でもよいはず)

mysql> SELECT CHAR(0) LIKE '%\0%';
+---------------------+
| CHAR(0) LIKE '%\0%' |
+---------------------+
|                   1 |
+---------------------+

f:id:sucrose:20181022201914p:plain:w0

その他の制御文字の判定

単に制御文字が含まれるかどうかなら、正規表現を使って 文字列 REGEXP '[[:cntrl:]]'でよさそう
ただし、注意点としては試した環境(MySQL 5.7)では正規表現だとヌル文字の判定はできませんでした(正規表現ライブラリの関係?)

制御文字の特定の範囲を指定したい場合

CHAR(文字の番号)

力技で行くと、CHAR(文字の番号)を使って正規表現で文字列の範囲の[]を文字列結合で作ります

mysql> SELECT CHAR(10) REGEXP CONCAT('[', CHAR(1), '-', CHAR(20), ']');
+----------------------------------------------------------+
| CHAR(10) REGEXP CONCAT('[', CHAR(1), '-', CHAR(20), ']') |
+----------------------------------------------------------+
|                                                        1 |
+----------------------------------------------------------+
[.characters.]

または[.characters.] という構文(charactersは文字の名前)がMySQLの正規表現では使えるらしいのでこういう風にも書けます
MySQL :: MySQL 5.6 リファレンスマニュアル :: 12.5.2 正規表現

SELECT CHAR(10) REGEXP '[[.NUL.]-[.DLE.]]';
直接制御文字を入力

使っているシェルで直接制御文字を入力できるならそのまま書けます
たとえばbashだと以下の記事のようにCtrl+Vの後に打ちたい制御文字に対応したキー、例えばCtrl+Aを打つと直接制御文字が入力できます(この場合は表示上^Aが入力されます)
qiita.com

これを使って正規表現の範囲を指定できます

SELECT CHAR(10) REGEXP '[^A-^P]';