Browse code
feat(awk): initial implementation of awk-like DSL
Edward Langley authored on 12/03/2022 20:07:20
Showing 1 changed files
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 |
+ ) |