git.fiddlerwoaroof.com
Browse code

feat(awk): initial implementation of awk-like DSL

Edward Langley authored on 12/03/2022 20:07:20
Showing 1 changed files
1 1
new file mode 100644
... ...
@@ -0,0 +1,204 @@
1
+(defpackage :fwoar.lisp-sandbox.awk
2
+  (:use :cl )
3
+  (:export ))
4
+(in-package :fwoar.lisp-sandbox.awk)
5
+
6
+(named-readtables:defreadtable :fwoar-awk
7
+  (:macro-char #\@ 'read-col-designator t))
8
+
9
+(defun read-col-designator (s _)
10
+  (declare (ignore _))
11
+  (let ((designator (read s t nil t)))
12
+    `(resolve-column *client*
13
+                     *record*
14
+                     ,(etypecase designator
15
+                        (fixnum designator)
16
+                        (symbol (string-downcase designator))
17
+                        (string designator)))))
18
+
19
+(defmacro with-command-output ((s command &rest args &key (output nil output-p)
20
+                                &allow-other-keys)
21
+                               &body body)
22
+  (declare (ignore output))
23
+  (when output-p
24
+    (error "can't override :output"))
25
+  `(let ((,s (uiop:process-info-output
26
+              (uiop:launch-program ,command :output :stream ,@args))))
27
+     ,@body))
28
+
29
+(defvar *eof-sentinel* '#:eof)
30
+(defgeneric make-client (client source))
31
+(defgeneric next-record (client source))
32
+(defgeneric parse-record (client raw-record))
33
+(defgeneric unpack-binders (client record))
34
+(defgeneric field-count (client record))
35
+(defgeneric resolve-column (client record column-designator))
36
+
37
+(defclass lines ()
38
+  ((%column-cache :initform (make-hash-table))))
39
+(fw.lu:defclass+ stream-lines (lines)
40
+  ())
41
+(fw.lu:defclass+ string-lines (lines)
42
+  ((%pos :initform 0 :accessor lines-pos)))
43
+
44
+(defmethod resolve-column ((client lines) (record cons) (column-designator number))
45
+  (let ((column-designator (1- column-designator)))
46
+    (with-slots (%column-cache) client
47
+      ;; manually tuned
48
+      (if (> column-designator 60)
49
+          (alexandria:ensure-gethash column-designator
50
+                                     %column-cache
51
+                                     (nth column-designator record))
52
+          (nth column-designator record)))))
53
+(defmethod parse-record ((client lines) (raw-record string))
54
+  (serapeum:tokens raw-record))
55
+(defmethod field-count ((client lines) (record list))
56
+  (length record))
57
+(defmethod unpack-binders ((client lines) (record list))
58
+  record)
59
+
60
+
61
+(defmethod make-client ((client (eql :lines)) (source stream))
62
+  (stream-lines))
63
+(defmethod make-client ((client (eql :lines)) (source string))
64
+  (string-lines))
65
+
66
+(defmethod next-record :before ((client lines) (source stream))
67
+  (clrhash (slot-value client '%column-cache)))
68
+(defmethod next-record ((client stream-lines) (source stream))
69
+  (read-line source nil *eof-sentinel*))
70
+(defmethod next-record ((client string-lines) (source string))
71
+  (let ((next-newline (position #\newline source :start (lines-pos client))))
72
+    (if (< (lines-pos client) (length source))
73
+        (prog1 (subseq source (lines-pos client) next-newline)
74
+          (setf (lines-pos client) (if next-newline
75
+                                       (1+ next-newline)
76
+                                       (length source))))
77
+        *eof-sentinel*)))
78
+
79
+(fw.lu:defclass+ ndjson ()
80
+  ())
81
+
82
+(defmethod make-client ((client (eql :ndjson)) (source stream))
83
+  (ndjson))
84
+(defmethod next-record ((client ndjson) (source stream))
85
+  (let ((line (read-line source nil *eof-sentinel*)))
86
+    (if (eql line *eof-sentinel*)
87
+        line
88
+        (let ((yason:*parse-json-arrays-as-vectors* t))
89
+          (yason:parse line)))))
90
+(defmethod resolve-column ((client ndjson) (record hash-table) column-designator)
91
+  (gethash column-designator record))
92
+(defmethod resolve-column ((client ndjson) (record vector) (column-designator number))
93
+  (aref record (1- column-designator)))
94
+(defmethod parse-record ((client ndjson) raw-record)
95
+  raw-record)
96
+(defmethod field-count ((client ndjson) record)
97
+  0)
98
+(defmethod field-count ((client ndjson) (record vector))
99
+  (length record))
100
+(defmethod unpack-binders ((client ndjson) record)
101
+  nil)
102
+(defmethod unpack-binders ((client ndjson) (record vector))
103
+  (coerce record 'list))
104
+
105
+
106
+(defvar *client*)
107
+(defvar *record*)
108
+(defvar *nr*)
109
+(defvar *nf*)
110
+(defmacro do-lines ((line s &optional (client :lines)) &body body)
111
+  (multiple-value-bind (body decls)
112
+      (alexandria:parse-body body)
113
+    (alexandria:with-gensyms (client-instance)
114
+      (alexandria:once-only (s)
115
+        `(let* ((,client-instance (make-client ,client ,s))
116
+                (*client* ,client-instance))
117
+           (loop for ,line = (next-record *client* ,s)
118
+                 until (eql ,line *eof-sentinel*)
119
+                 do ((lambda (,line)
120
+                       ,@decls
121
+                       (let ((*client* ,client-instance))
122
+                         ,@body))
123
+                     ,line)))))))
124
+
125
+(defmacro awk ((s &key (args nil args-p) (client :lines)) &body pattern-actions)
126
+  (let* ((begin (when (eql (caar pattern-actions) :begin)
127
+                  (car pattern-actions)))
128
+         (end (when (eql (caar (last pattern-actions)) :end)
129
+                (car (last pattern-actions))))
130
+         (pattern-actions (if begin
131
+                              (cdr pattern-actions)
132
+                              pattern-actions))
133
+         (pattern-actions (if end
134
+                              (butlast pattern-actions)
135
+                              pattern-actions))
136
+         (binders (when args-p
137
+                    (mapcar (lambda (n)
138
+                              (intern (format nil "$~d" n)))
139
+                            (alexandria:iota args :start 1)))))
140
+    `(block nil
141
+       ,@(cdr begin)
142
+       (let ((*nr* 0))
143
+         (do-lines ($0 ,s ,client)
144
+           (declare (ignorable $0))
145
+           (let* (($* (parse-record *client* $0))
146
+                  (*record* $*)
147
+                  (*nf* (field-count *client* $*)))
148
+             (declare (ignorable $*))
149
+             (destructuring-bind (&optional ,@binders &rest $@)
150
+                 (unpack-binders *client* $*)
151
+               (declare (ignorable $@ ,@binders))
152
+               ,@(mapcar (lambda (it)
153
+                           (if (= 1 (length it))
154
+                               (alexandria:with-gensyms (v)
155
+                                 `(let ((,v ,(car it)))
156
+                                    (when ,v
157
+                                      (princ $0)
158
+                                      (terpri))))
159
+                               (cons 'when it)))
160
+                         pattern-actions)))
161
+           (incf *nr*)))
162
+       ,@(cdr end)
163
+       (values))))
164
+
165
+(defmacro defawk (name (s args) &body body)
166
+  `(defun ,name (,s)
167
+     (awk (,s :args ,args)
168
+       ,@body)))
169
+
170
+#+(or)
171
+(
172
+ (spinneret:with-html
173
+   (:table
174
+    (with-input-from-string (s (format nil "a b~%c d~% e f"))
175
+      (awk (s :args 2)
176
+        (:begin (:thead (:th "first") (:th "second")))
177
+        (t (:tr (mapc (lambda (cell)
178
+                        (:td $1 cell))
179
+                      $*)))
180
+        (:end (:tfoot (:td "end first") (:td "end second")))))))
181
+
182
+ (spinneret:with-html
183
+   (:table
184
+    (awk ((format nil "a b~%c d~% e f~%g") :args 2)
185
+      (t (:tr (mapc (lambda (cell)
186
+                      (:td $1 cell))
187
+                    $*)))
188
+      (:end (:tfoot (:td "end first") (:td "end second"))))))
189
+
190
+ (spinneret:with-html
191
+   (:table
192
+    (with-command-output (s "ps aux")
193
+      (awk (s :args 9)
194
+        (:begin (:thead (:th "first") (:th "second")))
195
+        (t (:tr (:td $1) (:td $2) (:td $3 "%") (:td $4)
196
+                (:td (serapeum:string-join $@ " "))))
197
+        (:end (:tfoot (:td "end first") (:td "end second")))))))
198
+
199
+ (serapeum:with-collector (c)
200
+   (with-command-output (s "ps aux")
201
+     (awk (s :args 10)
202
+       ((> *nf* 30) (c *nf* (car $@))))))
203
+
204
+ )