Coverage report: /home/ellis/comp/core/lib/cli/shell.lisp

KindCoveredAll%
expression0215 0.0
branch022 0.0
Key
Not instrumented
Conditionalized out
Executed
Not executed
 
Both branches taken
One branch taken
Neither branch taken
1
 ;;; lib/cli/shell.lisp --- shell utils
2
 
3
 ;; utils for working with shells in different environments
4
 
5
 ;;; Commentary:
6
 
7
 ;;; #$ Read Macro
8
 
9
 ;; A read macro is accessible in the named readtable :SHELL. It has
10
 ;; three modes of operation: read, compile, and eval. In read mode,
11
 ;; input is parsed and embedded lisp forms are expanded. The string is
12
 ;; returned as is. In eval mode, embedded lisp forms are expanded and
13
 ;; the resulting string is wrapped in a call to
14
 ;; SB-EXT:RUN-PROGRAM. Finally, in eval mode the compiled function is
15
 ;; called with default arguments and the result of that call is
16
 ;; returned.
17
 
18
 ;;; Code:
19
 (in-package :cli/shell)
20
 (in-readtable :std)
21
 
22
 (defparameter *shell* "/bin/bash")
23
 (defparameter *shell-input* nil)
24
 (defparameter *shell-output* t)
25
 
26
 (deftype %shell-state () '(member :sh :dolla :pound))
27
 
28
 (defun plain-shell-reader (stream)
29
   (let (out (state :sh))
30
     (declare (type %shell-state state))
31
     (loop for c = (read-char stream)
32
           do (cond
33
                ((eq state :sh)
34
                 (case c
35
                   (#\$ (setq state :dolla))
36
                   (#\# (setq state :pound))
37
                   (t (push c out))))
38
                ((eq state :pound)
39
                 ;; TODO 2025-01-20: we should consider an alternative method
40
                 ;; to remove the explicit evals.
41
 
42
                 ;; check for lisp-mode character
43
                 (cond
44
                   ((char= c #\,) ;; check for splice-mode character
45
                    (if (char= (peek-char nil stream) #\@)
46
                        (progn
47
                          ;; skip it
48
                          (read-char stream)
49
                          ;; eval and push each form individually.
50
                          (let ((form (read stream nil nil)))
51
                            (push
52
                             (coerce
53
                              (format nil "~{~A~^ ~}" (compile-and-eval form))
54
                              'list)
55
                             out)))
56
                        ;; unconditionally read in a single sexp and eval.
57
                        (push (coerce (format nil "~A " (compile-and-eval (read stream nil nil)))
58
                                      'list)
59
                              out)))
60
                   ((or (char= c #\+) (char= c #\-))
61
                    (if (char= 
62
                         (if (sb-int:featurep (let ((*package* sb-int:*keyword-package*)
63
                                                    (sb-impl::*reader-package* nil)
64
                                                    (*read-suppress* nil))
65
                                                (read stream t nil t)))
66
                             #\+ #\-)
67
                         c)
68
                        (push (coerce (format nil "~A " (eval (read stream t nil t))) 'list) out)
69
                        (let ((*read-suppress* t))
70
                          (read stream t nil t)
71
                          (values))))
72
                   (t ;; return as is
73
                    (progn 
74
                      (push #\# out)
75
                      (push c out))))
76
                 (setq state :sh))
77
                ((eq state :dolla)
78
                 (if (char= c #\#)
79
                     (return)
80
                     (progn
81
                       (setq state :sh)
82
                       (push #\$ out)
83
                       (push c out))))))
84
     (concatenate 'string
85
                  (flatten (nreverse out)))))
86
 
87
 (defmacro define-process-output-handler (type &body body)
88
   "Define a new function which handles the result of a SB-EXT:PROCESS in
89
 the context of the $#-reader macro."
90
   (declare (ignore type body)))
91
 
92
 (defun |#$-reader| (stream sub-char numarg)
93
   "Switch on the shell reader, parsing STREAM and returning a
94
 shell program or executing it. In other words, this is an
95
 implementation of the lazy version of SHCL's #$-reader.
96
 
97
 Similar to shcl, we add some reader extensions to enable embedding
98
 lisp forms and other goodies.
99
 
100
 #0$ x=#,(* 2 2) 
101
 echo $x
102
 $#
103
 ;; => 4
104
 
105
 KLUDGE: an escaped SYMBOL can't be immediately followed by the closing tag
106
 '$#' - this causes the reader to consume those characters as part of the
107
 symbol name. One thing we might end up doing is checking for those characters
108
 in the input and unreading those 2 chars.
109
 
110
 An escaped form with parens like the following works fine:
111
 
112
 #0$echo #,(+ 2 2)$# ;; => 4"
113
   (declare (ignore sub-char) ((or (integer 0 9) null) numarg))
114
   (let ((str (plain-shell-reader stream)))
115
     (if numarg
116
         (cond
117
           ((= numarg 0)
118
            (let ((args (list "-c" (format nil "~a" str))))
119
              (lambda (&key input (output *standard-output*) (wait t) (status-hook))
120
                (case output
121
                  (:string (string-right-trim
122
                            '(#\Newline)
123
                            (with-output-to-string (s)
124
                              (sb-ext:run-program *shell* args
125
                                                  :directory *default-pathname-defaults*
126
                                                  :output s
127
                                                  :input input
128
                                                  :wait wait
129
                                                  :status-hook status-hook))))
130
                  (:integer (parse-integer
131
                             (string-right-trim
132
                              '(#\Newline)
133
                              (with-output-to-string (s)
134
                                (sb-ext:run-program *shell* args
135
                                                    :directory *default-pathname-defaults*
136
                                                    :output s
137
                                                    :input input
138
                                                    :wait wait
139
                                                    :status-hook status-hook)))))
140
                  (t (sb-ext:run-program *shell*
141
                                         args
142
                                         :directory *default-pathname-defaults*
143
                                         :output output
144
                                         :input input
145
                                         :wait wait
146
                                         :status-hook status-hook))))))
147
           ((= numarg 1)
148
            (string-right-trim '(#\Newline)
149
                               (with-output-to-string (s)
150
                                 (sb-ext:run-program *shell*
151
                                                     (list "-c" (format nil "~a" str))
152
                                                     :directory *default-pathname-defaults*
153
                                                     :output s
154
                                                     :input *shell-input*))))
155
           (t (nyi!)))
156
         `(sb-ext:run-program *shell*
157
                              (list "-c" (format nil "~a" ,str))
158
                              :directory *default-pathname-defaults*
159
                              :input *shell-input*
160
                              :output *shell-output*))))
161
 
162
 (defreadtable :shell
163
   "The shell readtable"
164
   (:merge :std)
165
   (:dispatch-macro-char #\# #\$ #'|#$-reader|))