(defpackage :fwoar.lisp-sandbox.awk (:use :cl ) (:export )) (in-package :fwoar.lisp-sandbox.awk) (named-readtables:defreadtable :fwoar-awk (:macro-char #\@ 'read-col-designator t)) (defun read-col-designator (s _) (declare (ignore _)) (let ((designator (read s t nil t))) `(resolve-column *client* *record* ,(etypecase designator (fixnum designator) (symbol (string-downcase designator)) (string designator))))) (defmacro with-command-output ((s command &rest args &key (output nil output-p) &allow-other-keys) &body body) (declare (ignore output)) (when output-p (error "can't override :output")) `(let ((,s (uiop:process-info-output (uiop:launch-program ,command :output :stream ,@args)))) ,@body)) (defvar *eof-sentinel* '#:eof) (defgeneric make-client (client source)) (defgeneric next-record (client source)) (defgeneric parse-record (client raw-record)) (defgeneric unpack-binders (client record)) (defgeneric field-count (client record)) (defgeneric resolve-column (client record column-designator)) (defclass lines () ((%column-cache :initform (make-hash-table)))) (fw.lu:defclass+ stream-lines (lines) ()) (fw.lu:defclass+ string-lines (lines) ((%pos :initform 0 :accessor lines-pos))) (defmethod resolve-column ((client lines) (record cons) (column-designator number)) (let ((column-designator (1- column-designator))) (with-slots (%column-cache) client ;; manually tuned (if (> column-designator 60) (alexandria:ensure-gethash column-designator %column-cache (nth column-designator record)) (nth column-designator record))))) (defmethod parse-record ((client lines) (raw-record string)) (serapeum:tokens raw-record)) (defmethod field-count ((client lines) (record list)) (length record)) (defmethod unpack-binders ((client lines) (record list)) record) (defmethod make-client ((client (eql :lines)) (source stream)) (stream-lines)) (defmethod make-client ((client (eql :lines)) (source string)) (string-lines)) (defmethod next-record :before ((client lines) (source stream)) (clrhash (slot-value client '%column-cache))) (defmethod next-record ((client stream-lines) (source stream)) (read-line source nil *eof-sentinel*)) (defmethod next-record ((client string-lines) (source string)) (let ((next-newline (position #\newline source :start (lines-pos client)))) (if (< (lines-pos client) (length source)) (prog1 (subseq source (lines-pos client) next-newline) (setf (lines-pos client) (if next-newline (1+ next-newline) (length source)))) *eof-sentinel*))) (fw.lu:defclass+ ndjson () ()) (defmethod make-client ((client (eql :ndjson)) (source stream)) (ndjson)) (defmethod next-record ((client ndjson) (source stream)) (let ((line (read-line source nil *eof-sentinel*))) (if (eql line *eof-sentinel*) line (let ((yason:*parse-json-arrays-as-vectors* t)) (yason:parse line))))) (defmethod resolve-column ((client ndjson) (record hash-table) column-designator) (gethash column-designator record)) (defmethod resolve-column ((client ndjson) (record vector) (column-designator number)) (aref record (1- column-designator))) (defmethod parse-record ((client ndjson) raw-record) raw-record) (defmethod field-count ((client ndjson) record) 0) (defmethod field-count ((client ndjson) (record vector)) (length record)) (defmethod unpack-binders ((client ndjson) record) nil) (defmethod unpack-binders ((client ndjson) (record vector)) (coerce record 'list)) (defvar *client*) (defvar *record*) (defvar *nr*) (defvar *nf*) (defmacro do-lines ((line s &optional (client :lines)) &body body) (multiple-value-bind (body decls) (alexandria:parse-body body) (alexandria:with-gensyms (client-instance) (alexandria:once-only (s) `(let* ((,client-instance (make-client ,client ,s)) (*client* ,client-instance)) (loop for ,line = (next-record *client* ,s) until (eql ,line *eof-sentinel*) do ((lambda (,line) ,@decls (let ((*client* ,client-instance)) ,@body)) ,line))))))) (defmacro awk ((s &key (args nil args-p) (client :lines)) &body pattern-actions) (let* ((begin (when (eql (caar pattern-actions) :begin) (car pattern-actions))) (end (when (eql (caar (last pattern-actions)) :end) (car (last pattern-actions)))) (pattern-actions (if begin (cdr pattern-actions) pattern-actions)) (pattern-actions (if end (butlast pattern-actions) pattern-actions)) (binders (when args-p (mapcar (lambda (n) (intern (format nil "$~d" n))) (alexandria:iota args :start 1))))) `(block nil ,@(cdr begin) (let ((*nr* 0)) (do-lines ($0 ,s ,client) (declare (ignorable $0)) (let* (($* (parse-record *client* $0)) (*record* $*) (*nf* (field-count *client* $*))) (declare (ignorable $*)) (destructuring-bind (&optional ,@binders &rest $@) (unpack-binders *client* $*) (declare (ignorable $@ ,@binders)) ,@(mapcar (lambda (it) (if (= 1 (length it)) (alexandria:with-gensyms (v) `(let ((,v ,(car it))) (when ,v (princ $0) (terpri)))) (cons 'when it))) pattern-actions))) (incf *nr*))) ,@(cdr end) (values)))) (defmacro defawk (name (s args) &body body) `(defun ,name (,s) (awk (,s :args ,args) ,@body))) #+(or) ( (spinneret:with-html (:table (with-input-from-string (s (format nil "a b~%c d~% e f")) (awk (s :args 2) (:begin (:thead (:th "first") (:th "second"))) (t (:tr (mapc (lambda (cell) (:td $1 cell)) $*))) (:end (:tfoot (:td "end first") (:td "end second"))))))) (spinneret:with-html (:table (awk ((format nil "a b~%c d~% e f~%g") :args 2) (t (:tr (mapc (lambda (cell) (:td $1 cell)) $*))) (:end (:tfoot (:td "end first") (:td "end second")))))) (spinneret:with-html (:table (with-command-output (s "ps aux") (awk (s :args 9) (:begin (:thead (:th "first") (:th "second"))) (t (:tr (:td $1) (:td $2) (:td $3 "%") (:td $4) (:td (serapeum:string-join $@ " ")))) (:end (:tfoot (:td "end first") (:td "end second"))))))) (serapeum:with-collector (c) (with-command-output (s "ps aux") (awk (s :args 10) ((> *nf* 30) (c *nf* (car $@)))))) )