標準愚痴出力

個人的なIT作業ログです。もしかしたら一般的に参考になることが書いているかもしれません(弱気

コマンドラインフィルターを使って nyagos の文法を手直しする

以前もコマンドラインフィルターについて次のような文書を書いたことがあるのですが、説明がほとんどなかったので、改めて解説文書を書いてみました。

できれば、ゼロベースの説明としたかったところですが、やはりLua製ということもあり、Lua の知識が必要となります。

シングルクォートを使いたい

上の記事中、nyagos 上で jq 用のワンライナーを書こうとしたのですが、シングルクォーテーションが引用符として使えないので、\" がたくさん登場して、かなり見辛いことになりました。

jq -r ". | map(\"#\"+(.number|tostring)+\" [\"+ .user.login + \"](\"+ .user.html_url + \")\") | join(\"\r\n\")" issues.json

PowerShell だとシングルクォートも引用符として使えるのでこういうことはないのですが、nyagos はDOS由来の書式とUNIX由来の書式の共存を意図したシェルなので、そういう互換性や慣習が衝突する部分については導入を避けてきました。そのせいもあって、nyagos はかなり保守的なイメージがあると思います。

しかしながら、あきらめる必要はありません。nyagos には「コマンドラインフィルター」という強力な機能があります。

コマンドラインフィルターとは

Lua 関数を使って、ユーザの入力を自動的に加工する機能です。

具体的には、nyagos.filter というフックに、引数1個、戻り値1個の関数を登録します。 コマンドラインの内容が引数として与えられるので、それを加工して return すれば OK です。

サンプル

local orgfilter = nyagos.filter

nyagos.filter = function(cmdline)
    cmdline = string.gsub(cmdline,"@date",os.date())
    if orgfilter then
        cmdline = orgfilter(cmdline)
    end
    return cmdline
end

コマンドラインの中の @date というテキストがあったら、それを日付に置換するという簡単なサンプルです。nyagos.filter には他のフィルターが既に登録されていたりしますので、置換を実行した後、それらを呼び出すのを忘れないようにしてください。 ( 忘れると、逆クォートが効かなくなったりします )

これを~/.nyagos にコピペしたあと、nyagos で実験してみます。

$ echo @date
30 Jun 24 22:09 JST

期待どおり動作しているようです。

シングルクォーテーションを使えるようにする

イメージとしては次のとおりにユーザが入力するテキストを置換すれば、/bin/sh 互換とはいかないまでも用は足りそうです 1

  • " で囲まれていない ' は、" に置換する
  • ' で囲まれている " は、\" に置換する
ユーザが入力するコード 内部的に置き変わるコード 備考
'(print "ahaha")' "(print \"ahaha\")" 新書式
"(print \"ahaha\")" "(print \"ahaha\")" そのまま※1
"(print 'ahaha')" "(print 'ahaha')" そのまま※2

※ 1 今までの書式もそのまま使えないといけないので
※ 2 一部のシングルクォーテーションを引用符に使える言語向け(lua,goawkなど)

これをコードにしたものが次のスクリプトです。

sq2dq.lua

local function sq2dq(source)
    local sq = false
    local dq = false
    return (string.gsub(source,"[\"']",function(c)
        if c == "'" then
            if not dq then
                sq = not sq
                return '"'
            end
        end
        if c == '"' then
            if sq then
                return [[\"]]
            end
            dq = not dq
        end
        return c
    end))
end

if nyagos then
    local oldfilter = nyagos.filter
    nyagos.filter = function(s)
        s = sq2dq(s)
        if oldfilter then
            s = oldfilter(s)
        end
        return s
    end
else
    assert(sq2dq([['(print "ahaha")']])   == [["(print \"ahaha\")"]])
    assert(sq2dq([["(print \"ahaha\")"]]) == [["(print \"ahaha\")"]])
    assert(sq2dq([["(print 'ahaha')"]])   == [["(print 'ahaha')"]]  )
end

スクリプトの後半にはテストコードも書いておきました。

識別子の nyagos は nyagos の中にいる時だけしかセットされないので、not nyagos な時のブロックは直接、普通の lua.exe から実行する時だけ動かすコードなどを書くことができます。ここにテストコードを書いておけばよいでしょう。

[[〜]]lua の文字列表現の一つです。今回はテスト用の文字列の中に "' も使いますんで、こういう時にこそ使う表現ですね。

これが意図通り動作するか…全部 .nyagos に埋め込むと行数が膨れて見辛くなるので、sq2dq.lua という別ファイルに書いて、.nyagos から include するようにしましょう。

package.path = package.path ..  ";(sq2dq.luaの置き場所のディレクトリ)\\?.lua"
require "sq2dq"

nyagos を起動して実験です。

$ gawk 'BEGIN{ print "ahaha" }'
ahaha

$ gmnlisp -e '(format (standard-output) "~S~%" (cons 1 2))'
(1 . 2)

最初に問題視したワンライナーも見易くなりました。

$ jq -r '. | map("#"+(.number|tostring)+" ["+ .user.login + "]("+ .user.html_url + ")") | join("\r\n")' issues.json
  :出力結果は人のアカウント情報などがあるのでちょっと省略

  1. 本当はシングルクォーテーション内のシェル変数は置換しない、ダブルクォーテーション内の変数はシェル置換するというルールがあります。そこまで対応するのはたいへんなので、今回は見送ります。