jqfpyが--input-format=rawをサポートすればsed,awk的な処理にも対応できるのでは?と思ったので実装してみた。けっこう便利だった。

github.com

以前にもちょっとしたjqfpyの活用方法の記事は書いていた。

今回もそれらと似たような話。

--input-format=raw

0.6.0からformatにrawが追加された。とくにJSONYAMLなどフォーマットを気にせずそのまま文字列として返される(正確には readline().rstrip())。

なので、利用者としては単なる改行区切りの文字列と認識してテキトーにコードを書いてあげれば良い。

function gen() { (for i in $(seq 5); do echo $i; done) }
$ gen | jqfpy -i raw
"1"
"2"
"3"
"4"
"5"

クォートされるのが邪魔なら -r, --raw-output

$ gen | jqfpy -i raw -r
1
2
3
4
5

1つにまとめたければ --slurp

$ gen | jqfpy -i raw -r --slurp -c
["1", "2", "3", "4", "5"]

もちろん、 -u, --unbuffered にも対応している。少しずつ進行するようなコマンドでも大丈夫。

$ function gen2() { (for i in $(seq 5); do echo $i; sleep 1; done) }
$ gen2 | jqfpy -u -i raw 'import time; [time.time(), get()]' -c
[1560638479.4626064, "1"]
[1560638480.2339017, "2"]
[1560638481.2378538, "3"]
[1560638482.2408028, "4"]
[1560638483.2435904, "5"]

# -u なし
$ gen2 | jqfpy -i raw 'import time; [time.time(), get()]' -c
[1560638535.1345422, "1"]
[1560638535.1405966, "2"]
[1560638535.1406453, "3"]
[1560638535.1406796, "4"]
[1560638535.1407096, "5"]

Noneを返すものは無視される

ついでにNoneを返すものは無視するようにした。これは過去にもこのブログで言及していた(jqfpyにloadfile(),dumpfile()を追加していた で dev/nullへのリダイレクトは邪魔だよねという形で)。

$ function gen3() { printf 'foo\nbar\n\nboo'; }
$ gen3 | jqfpy -i raw
"foo"
"bar"
""
"boo"

$ gen3 | jqfpy -i raw 'get() or None'
"foo"
"bar"
"boo"

無視しないようにするには --show-none を付ける。

$ gen3 | jqfpy --show-none -i raw 'get() or None'
"foo"
"bar"
null
"boo"

nullと表示されている通り、デフォルトではJSONとしての出力される。それを止めたければ -o, --output-format を付けてあげる。

$ gen3 | jqfpy --show-none -i raw -o raw 'get() or None'
'foo'
'bar'
None
'boo'

dictknife の各種コマンドは -i, --input-format, -o, --output-format の他にそれらを同時に指定する f, --format があるのだけれど、こちらにも付けてあげても良いかもしれない。

sedの代わり

例えばsedは以下の様な使いかたをする。oを@に変える。一番シンプルな例かも。

$ echo foo | sed 's/o/@/g'
f@@

ふつうのpythonの文字列処理。もちろんpythonでやる分長くはなるけれど。

$ echo foo | jqfpy -i raw 'get().replace("o","@")' -r
f@@

正規表現でのマッチなどをやる分には3.8に期待。

awkの代わり

awkはtsvやcsvになりきれていないようなスペース区切りの文字列の一部分だけを取り出すのに使ったりする。個人的にはcutコマンドの上位版というくらいの認識(それ以上詳しくはない)。

$ echo 'foo 20 F' | awk '{printf "%s(%s)さん\n", $1, $2, "\n"}'
foo(20)さん

この辺りもまぁsplit()とかと組み合わせた文字列処理。

$ echo 'foo 20 F' | jqfpy -i raw 'row = get().split(" "); f"{row[0]}({row[1]})さん"' -r
foo(20)さん

ちなみにちょっと形を変えてJSONで出力するのも楽といえば楽。zip()との組み合わせはありと言えばあり。

行区切りの文字列をJSONとして

$ echo 'foo 20 F' | jqfpy -i raw 'row = get().split(" "); props = ["name","age","nickname"];  dict(zip(props, row))' -r
{
  "name": "foo",
  "age": "20",
  "nickname": "F"
}

スクリプトに持っていくときにpythonの関数に

ちなみに --show-code を実行してあげれば、ただのpythonの関数としてけっこう手軽に取り出せると思う。オプションが残ったままでも大丈夫(-i raw とか -r の話)

$ echo 'foo 20 F' | jqfpy --show-code -i raw 'row = get().split(" "); props = ["name","age","nickname"];  dict(zip(props, row))' -r
def _transform(get, h=None):
    row = get().split(" ")
    props = ["name","age","nickname"]
    return dict(zip(props, row))

ちょこっと書き換えればこういう感じに変更するだけなので。

def _transform(line):
    row = line.split(" ")
    props = ["name","age","nickname"]
    return dict(zip(props, row))

おわりに

もう少し複雑な例があれば翻訳してみても良いかも。それなりに冗長にはなるもののpythonがネイティブの人には新しくunix系のコマンドのshell芸的なテクニックを覚えるよりも心理的な負担が少なくなるかも?

ちなみによりawkなどに近づけたものとしてはtseなどがあったりします。

github.com