Skip to content

nakamuray/owk

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

owk - DSL for structured input stream

owk は JSON 等それなりの構造を持ったデータのストリームに対して、 awk 的に使えることを目指して作られている DSL/interpreter です。

$ owk -e '"hello world"'
"hello world"
$ seq 5 | owk -i line 'map { _ * 2 }'
2
4
6
8
10
$ seq 5 | owk -i line 'map { _ * 2 } >> filter { _ < 10 }'
2
4
6
8
$ echo '{"name":"roi"}{"name":"zak"}' | owk 'map { _.name }'
"roi"
"zak"

owk コマンドについて

実行ファイルであるところの owk コマンドについて説明します。

owk は引数で渡された owk script をまず一度評価し、 最後の式を main 関数として扱います。 その後、標準入力を Stream として main 関数に渡します。

$ seq 10 | owk -io word 'print "init"; map input -> { "[" + input.0 + "]" }'
init
[1]
[2]
[3]
[4]
[5]
[6]
[7]
[8]
[9]
[10]

-e オプションを指定することで、 入力を読み込まず渡された script を実行し、最後の式の値を表示する eval モードになります。

$ seq 10 | owk -e '"hi"'
"hi"

入力をどのようにパースするかは -i オプションで指定できます。 デフォルトでは JSON のストリームとしてパースされます。

$ echo '{ "value": 1 } { "value": 2 } { "value": 3 }' | owk -i json 'map { _.value }'
1
2
3

例えば -i word と指定することで、行ごとにスペースで分割し、数値もしくは文字列として、 数値をキーにした辞書に格納され、渡るようになります。

$ echo '1 2 three' | owk -io word 'map { _ }'
{0: "1 2 three", 1: 1, 2: 2, 3: "three"}

また、出力をどのように変換するかは -o オプションで指定できます。

$ owk -o ltsv -e '{ key: "value", key2: 100 }'
key:value     key2:100

map 関数を使用することで、入力 Stream からのデータそれぞれに対して関数を適用することが出来ます。

$ seq 10 | owk -i line 'map { _ * 2 }'
2
4
6
8
10
12
14
16
18
20

filter 関数で、条件を満たす入力のみを通すことが出来ます。

$ seq 10 | owk -i line 'filter i -> { i > 5 }'
6
7
8
9
10

fold 関数で、入力を畳み込むことが出来ます。

$ seq 10 | owk -i line 'fold (acc -> i -> { acc + i }) 0'
55

また、 >> 演算子により、それぞれの関数を連結した関数とすることが出来ます。

$ seq 10 | owk -i line 'map { _ + 1 } >> map { _ * 2 } >> fold (acc -> i -> acc + i) 0'
130

$> 演算子は左辺の値を右辺の関数に適用します。

$ owk -e '[1, 2, 3, 4] $> filter { _ % 2 == 0 } >>  map { _ * 2 }'
[
    4,
    8
]

言語仕様について

  • 動的型付。
  • lexical scope 。
  • 全て式です。何かしらの値を返します。
  • データ型は基本的に全て immutable です。
  • 変数の再定義は、同一スコープ中ではできません。親スコープの shadowing は可能です。

データ型

String

文字列です。

s = "hello, owk"
print "\u3042" # => あ

Number

数値です。

i = 2
j = 10.1
print (i * j) # => 20.2

Bool

真偽値です。

owk において 偽 と判定されるのは、 false と下で解説する Undef のみです。

t = true
f = false

Dict

ハッシュとか連想配列とも言われるアレです。

キーの文字列に対して、任意のデータを保持できます。

d = { key: "value", key2: 100 }
print d.key # => value
print (d ["key"]) # => value

辞書の更新 (merge) は以下のように行います。

d2 = d { key2: 200, key3: "spam" }
d3 = d2 ["key4", "egg"]

List

リストです。任意のデータを格納できます。

L = [1, 2, 3, "4"]
print (L[0]) # => 1

List[start, count] で slice を取得できます。

L2 = L[1, 2]
print L2 # => [2, 3]

Tuple

タプルです。任意のデータを格納できます。

リストとの使い分けですが、こちらは主に関数へ複数の値を渡すのに利用されます。

t = (1, 2)

Function

関数です。

pattern (guard) -> { block } の形で定義します。 block には複数の式を記述できます。 pattern (guard) -> 部分は省略可能で、その場合 _ -> が補われます。 (guard) 部分のみを省略することもできます。 また、 block が単一の式のみからなる場合は { } を省略することができます。

| で区切ることにより、複数のパターン・ブロックが記述できます。

関数の戻り値は最後の式の値になります。

関数呼び出しは func arg の形になります。 複数の値を渡したい場合は Tuple を渡すか、カリー化された関数を定義することで代用します。

なお、 owk においては0引数の関数呼び出しはできません。 不要だとしても最低1つは引数を渡してください。

f = { print "hi" }
f () # => hi

f2 = _ -> print "hi"
f2 () # => hi

f3 = name -> { print("hi, " + name) }
f3 "nakamuray" # => hi, nakamuray

f4 = (x, y) -> { x * y }
print (f4(2, 3))

f5 = x -> y -> { x * y }
print (f5(2, 3))

f6 = i -> { i * 2 }
print (f6 10) # => 20

f7 = { _ * 2 }
print (f7 10) # => 20

f8 = 0 -> { "zero" } | n -> { n }
print (f8 0) # => zero
print (f8 100) # => 100

f9 = n (n > 5) -> "greater than five" | n -> "less than equal five"
print (f9 5) # => less than equal five
print (f9 6) # => greater than five

Stream

データのストリームです。入力データはこの Stream として処理系より渡されます。

これを操作する関数を組み上げるのが、 owk の基本的な使い方となります。

Ref

参照です。 owk の他のデータ型は全て immutable なので、破壊的変更を行いたい時はこれを使います。

Ref を参照したい時は、関数のように呼び出します。 変更には := 演算子を利用します。

r = ref 0
print (r ()) # => 0
r := 1
print (r ()) # => 1

Undef

未定義値です。

u = undef

パターンマッチ

代入および関数適用の際に、パターンマッチが行われます。

パターンには String, Number, Dict, List, Tuple のリテラルと変数が記述できます。

(a, b) = (1, 2)
[c, [d, e]] = [3, [4, 5]]
f = (("6", 7) = ("6", 7))
{ key1: g, key2: h } = { key1: 8, key2: 9 }

((i, j) -> { print (i, j) }) (10, 11)

Dict のパターンマッチでは、チェックされる値の側に余分なキーがあっても無視されます。

{ key1: k } = { key1: 12, key3: 13 }

変数名@パターン の形で記述することで、パターン全体を変数に入れることができます。

l@{ key: m } = { key: "value", key2: "value!" }
print l # => { key: "value", key2: "value!" }
print m # => value

マッチに失敗した場合、代入なら Undef が返ります。

n = (0 = 1)

関数適用の場合は次のパターン・ブロックを試みます。全てのパターンにマッチしなかった場合は Undef が返ります。

func = 0 -> { 0 } | 1 -> { 1 }
print (func 1) # => 1
print (func 2) # =>

演算子

以下の演算子が利用できます。大体見たままです。

-, +, *, /, >, <, >=, <=, ==, !=, =~, !~, !, &&, ||, :, ?, :=

見たままでないいくつかを説明します。

  • $

    Haskell の $ です。左辺に関数、右辺に引数をとります。

    print $ 1 + 1 # => 2
    print (1 + 1) # => 2
    
  • ?

    左辺に Bool 、右辺に関数を取り、 Bool が 真 のとき右辺を実行します。

    true ? { print "hi" } # => hi
    false ? { print "hi?" }
    
  • :=

    上記 Ref の説明参照。

  • >>

    関数を連結します。 受け取った引数を左辺に渡してその戻り値を右辺に渡すような、新たな関数を作ります。

    f = { _ * 2 }
    g = { _ + 1 }
    h = f >> g
    print $ h 10 # => 21
    

バッククオートで囲むことで、演算子を変数名として参照・代入がきます。

print $ `+` 1 2  # => 3

`+:` = x -> y -> print (x, "plus", y)
1 +: 2  # => 1 plus 2

構文

owk スクリプトは、上記のデータ型と演算子を組み合わせた式の羅列になります。 式同士はセミコロン、もしくは改行で区切られます。 式中に改行を書きたい場合はバックスラッシュで改行をエスケープできます。

なお、以下の箇所では改行は無視されます。

  • Dict, List and Tuple 中の , の左右
  • 関数定義の区切りの | の左右
  • 2項演算子の右側

コメントは # から改行までです。

組み込み関数

TODO: いつか書く。

About

awk like tool for JSON and other inputs

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published