This repository has been archived by the owner on Sep 25, 2024. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 9
/
lish.lisp
2159 lines (2022 loc) · 73.8 KB
/
lish.lisp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
;;;
;;; lish.lisp - A Lisp shell.
;;;
;; This file contains the basic REPL and dispatch, and some other odds and ends.
(in-package :lish)
;; The "result" argument is not for the caller, but rather so we can detect
;; cycles in the package inheritance graph.
(defun flattened-package-use-list (package &optional result)
(loop :for p :in (package-use-list package) :do
(when (not (position p result))
(push p result)
(loop :for ip :in (flattened-package-use-list p result) :do
(pushnew ip result))))
result)
;; This tries to keep :LISH-USER up to date with respect to :CL-USER,
;; because I just so love to push the package system beyond it's limits.
;; This would probably be better done with something like conduits.
(defun update-user-package ()
;; Update uses
(loop :with isym :and isymbol-type :and esym :and esymbol-type
:for p :in (package-use-list :cl-user) :do
(when (not (position p (flattened-package-use-list *lish-user-package*)))
;; Things directly in lish-user are uninterned in favor of one
;; in cl-user.
(unintern-conflicts *lish-user-package* p)
;; Conflicts in inherited symbols are resolved by having the
;; "explicitly" used package symbol (i.e. things used by :lish-user
;; such as :lish) interned and made shadowing.
(do-symbols (sym p)
(setf (values esym esymbol-type)
(find-symbol (symbol-name sym) p)
(values isym isymbol-type)
(find-symbol (symbol-name sym) *lish-user-package*))
(when (not (equal esym isym))
(case isymbol-type
((:internal :external)
(shadow isym *lish-user-package*))
(:inherited
(when (not (eq (symbol-package esym) (symbol-package isym)))
(shadowing-import isym *lish-user-package*))))))
(use-package p *lish-user-package*)))
;; Update all symbols
(do-symbols (sym :cl-user)
;; @@@ deal with conflicts between imported symbols from different packages
;; @@@ keep symbols from packages used directly by :lish-user
(when (not (find-symbol (symbol-name sym) *lish-user-package*))
(import sym *lish-user-package*)))
;; Export exported symbols
(do-external-symbols (sym :cl-user)
(export sym *lish-user-package*)))
;; Get rid of this is if it's unnecessary.
(defun modified-context (context
&key
(in-pipe nil in-pipe-p)
(out-pipe nil out-pipe-p)
(environment nil environment-p)
(flipped-io nil flipped-io-p)
(pipe-plus nil pipe-plus-p)
(pipe-dot nil pipe-dot-p)
(pipe-both nil pipe-both-p)
(background nil background-p))
"Return a new context based on ‘context’, with the given slots."
(if (not context)
(make-context
:in-pipe in-pipe
:out-pipe out-pipe
:environment environment
:flipped-io flipped-io
:pipe-plus pipe-plus
:pipe-dot pipe-dot
:pipe-both pipe-both
:background background)
(let ((c (copy-structure context)))
(when in-pipe-p (setf (context-in-pipe c) in-pipe))
(when out-pipe-p (setf (context-out-pipe c) out-pipe))
(when environment-p (setf (context-environment c) environment))
(when flipped-io-p (setf (context-flipped-io c) flipped-io))
(when pipe-plus-p (setf (context-pipe-plus c) pipe-plus))
(when pipe-dot-p (setf (context-pipe-dot c) pipe-dot))
(when pipe-both-p (setf (context-pipe-both c) pipe-both))
(when background-p (setf (context-background c) background))
c)))
(defun %find-shell-word (expr position &optional (word-num 0))
(loop
:for word :in (shell-expr-words expr)
:do
(typecase word
(shell-word
(when (<= position (shell-word-end word))
(throw 'found (list
(if (>= position (shell-word-start word))
word
nil)
word-num))))
(cons
(when (and (keywordp (first word))
(shell-expr-p (second word)))
(%find-shell-word (second word) position word-num))))
(incf word-num))
(list nil nil))
(defun find-shell-word (expr position)
(values-list
(catch 'found
(%find-shell-word expr
position
;; (min position
;; ;; (1- (length (shell-expr-line expr)))
;; (length (shell-expr-line expr))
))))
(defun shell-word-num (expr pos)
"Return the shell expression's word that position POS is in."
(multiple-value-bind (word num) (find-shell-word expr pos)
(if word
num
(and num (max 0 (1- num))))))
(defun shell-word-at (expr pos)
"Return the shell expression's word that position POS is in."
(first (multiple-value-list (find-shell-word expr pos))))
(defun word-word (word)
"Word is bond."
(typecase word
(shell-word (shell-word-word word))
(t word)))
(defun word-quoted (word)
(typecase word
(shell-word (shell-word-quoted word))
(t nil)))
(defun in-word (word position)
"Return true if ‘position’ is in shell-word ‘word’."
(and (>= position (shell-word-start word))
(<= position (shell-word-end word))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Job control
(cffi:defcallback sigtstp-handler :void ((signal-number :int))
(declare (ignore signal-number))
;;(format t "[Terminal Stop]~%") (finish-output)
#+unix (setf uos::*got-tstp* :dont-suspend)
;; I'm scared of this.
;; (invoke-restart (find-restart 'abort))
;;(throw 'interactive-interrupt nil)
)
(defun start-job-control ()
#+unix
(setf (os-unix:signal-action os-unix:+SIGTSTP+) 'sigtstp-handler
(os-unix:signal-action os-unix:+SIGTTIN+) :ignore
(os-unix:signal-action os-unix:+SIGTTOU+) :ignore))
(defun stop-job-control (saved-sigs)
#-unix
(declare (ignore saved-sigs))
#+unix
(let ((tstp (first saved-sigs))
(ttin (second saved-sigs))
(ttou (third saved-sigs)))
(setf (os-unix:signal-action os-unix:+SIGTSTP+)
(if (keywordp tstp) tstp :default)
(os-unix:signal-action os-unix:+SIGTTIN+)
(if (keywordp ttin) ttin :default)
(os-unix:signal-action os-unix:+SIGTTOU+)
(if (keywordp ttou) ttou :default))))
(defun job-control-signals ()
#+unix (list (os-unix:signal-action os-unix:+SIGTSTP+)
(os-unix:signal-action os-unix:+SIGTTIN+)
(os-unix:signal-action os-unix:+SIGTTOU+)))
(defun set-default-job-sigs ()
#+unix
(setf (os-unix:signal-action os-unix:+SIGTSTP+) :default
(os-unix:signal-action os-unix:+SIGTTIN+) :default
(os-unix:signal-action os-unix:+SIGTTOU+) :default))
;(defun run (cmd args)
; block sigchld & sigint
; give terminal to child if not running in bg?
; fork
; in the child:
; unblock sigchld & sigint
; set default action tty signals (TSTP, TIN, TOU)
; or ignore them if not going to be in the foreground
; set the process group setpgid to it's own pid (or group of the pipeline)
; give terminal to child's process group
; exec
; in the parent:
; just to be sure:
; set the child's process group (setpgid) to it's own pid
; (or group of the pipeline)
; unblock sigchld & sigint
; wait for the child
; )
; (defun set-terminal-group (tty group)
; "Make the terminal TTY be controled by process group GROUP."
; ;block TTOU TTIN TSTP & CHLD while we do this:
; (tcsetpgrp tty group))
; (defun init-job-control (sh)
; (let ((our-process-group (getpgid 0))
; (tty-process-group (tcgetpgrp tty))
; (our-pid (getpid)))
; (loop :while (/= our-process-group tty-process-group)
; :do
; ;; If we're not the foreground process
; ;; Signal the process group that we want input, which will likely
; ;; stop us. Keep demanding the tty until we get it or die.
; (kill SIGTTIN)
; (setf tty-process-group (tcgetpgrp tty)))
; ;; If for some reason we're not the process group leader,
; ;; then become it, and take control of the terminal.
; (when (/= our-process-group our-pid)
; (setpgid 0 our-pid)
; (set-terminal-group our-pid))))
(defun handle-job-change (job result status &key foreground)
"Take appropriate action when JOB changes status."
(case status
(:exited
;; Only announce normal exits when not running in the foreground.
(when (not foreground)
(format t ";; Exited ~a ~a~%" (job-name job) result))
(finish-output)
(delete-job job)
result)
((:signaled :coredump)
(format t ";; Killed ~a ~a" (job-name job) (job-pid job))
#+unix (progn
(when (and result (integerp result))
(format t " ~a" (os-unix:signal-description result)))
(when (eq status :coredump)
(format t " Core dump")))
(terpri)
(finish-output)
(delete-job job)
nil)
(:error
(format t ";; Error ~a" result)
#+unix (when (and result (integerp result))
(format t " ~a" (os-unix:error-message result)))
(terpri)
(finish-output)
;; (delete-job job) ;; ??? Should we?
result)
(:stopped
(format t ";; Stopped ~a ~a~%" (job-name job) (job-pid job))
(finish-output)
(setf (job-status job) :suspended)
nil)))
(defun wait-for-jobs (sh)
"Wait for any jobs we started in a pipeline."
#+unix
(loop
:with pid :and result :and status :and job
;; :while (shell-wait-for sh)
;; :do
;; (format t "WAIT: -------~%")
;; ;; (multiple-value-setq (pid result status) (uos:wait))
;; (multiple-value-setq (pid result status) (uos:check-jobs t))
;; (format t "WAIT: ~s ~s ~s~%" pid result status)
;; (cond
;; ((not pid)
;; (return nil))
;; ((setf job (find pid (shell-wait-for sh)
;; :key #'job-pid))
;; (handle-job-change job result status :foreground t)
;; (setf (shell-wait-for sh)
;; (delete job (shell-wait-for sh))))
;; (t
;; (format t "WAIT: some other job?~%"))))
:while (shell-wait-for sh)
:do
;; (format t "WAIT: -------~%")
;; (multiple-value-setq (pid result status) (uos:wait))
(multiple-value-setq (pid result status) (uos:check-jobs))
;; (format t "WAIT: ~s ~s ~s~%" pid result status)
(cond
((not pid)
(return nil))
((setf job (find pid (shell-wait-for sh)
:key #'job-pid))
(handle-job-change job result status :foreground t)
(setf (shell-wait-for sh)
(delete job (shell-wait-for sh))))
(t
;; (format t "WAIT: some other job?~%")
)))
#-unix
(declare (ignore sh)))
(defparameter *compound-expr-strings*
'(:and "&&"
:or "||"
:sequence "^"
:redirect-to ">"
:redirect-both-to ">&"
:append-to ">>"
:redirect-from "<"
:pipe "|"
:pipe-plus "|+"
:pipe-dot "|.")
"For reconstructing expression strings.")
(defun compound-tag-string (keyword)
"Return the string representation for the compound operator keyword THING."
(getf *compound-expr-strings* keyword))
(defun %shell-words-to-string (words stream &key literal-line)
"The internal part of shell-words-to-*-string."
(declare (ignore literal-line))
(let (#| start end |# skip)
(labels ((write-thing (w)
(typecase w
((or string fat-string) (princ (quotify w) stream))
((or character fatchar) (princ w stream))
(cons
(let ((s (compound-tag-string (car w))))
(if (and s (shell-expr-p (second w)))
(format stream
"~a ~a~a"
(shell-words-to-string
(shell-expr-words (second w)))
s
(shell-words-to-string (rest w)))
(write w :stream stream :readably t :case :downcase))))
(t ;; @@@ is this reasonable?
(princ w stream))))
(write-it (w &optional space)
(setf skip nil)
;; @@@ This whole literal-line thing is dubious because
;; the start and end positions in words can be totally messed
;; up, especially in regard to the line fragment we're passed.
#|
(when literal-line
(cond
((and (shell-word-p w)
(shell-word-start w) (shell-word-end w))
;; A word with positions, just update start and end
;; and skip further processing.
(when (not start)
(setf start (shell-word-start w)))
(setf end (shell-word-end w)
skip t))
((and (shell-word-p w)
(or (not (shell-word-start w))
(not (shell-word-end w)))
start end)
;; A word without positions, write out what we got, and
;; clear the start and end.
(when space
(write-char #\space stream))
(write-string (subseq literal-line start end)
stream)
(setf start nil end nil))))
|#
(when (not skip)
(when space
(write-char #\space stream))
(cond
((and (shell-word-p w) (word-quoted w))
(write-char #\" stream)
(write-thing (word-word w))
(write-char #\" stream))
(t
(write-thing (word-word w)))))))
(when (first words)
(write-it (first words)))
(loop :for w :in (rest words)
:do (write-it w t))
#|
(when (and literal-line start end)
;; Write out the last piece.
(write-char #\space stream)
(write-string (subseq literal-line start end) stream))
|#
)))
(defun shell-words-to-string (words &key literal-line)
"Put a list of shell words, properly quoted, into a string separated by
spaces. This of course loses some data in the words. If LITERAL-LINE is given,
try to take as much as we can from it as the original line."
(with-output-to-string (stream)
(%shell-words-to-string words stream :literal-line literal-line)))
(defun shell-words-to-fat-string (words &key literal-line)
"Put a list of shell words, properly quoted, into a fat string separated by
spaces. This of course loses some data in the words. If LITERAL-LINE is given,
try to take as much as we can from it as the original line."
(with-output-to-fat-string (stream)
(%shell-words-to-string words stream :literal-line literal-line)))
(defun shell-words-to-list (words)
"Return shell words as a list of strings."
(mapcar #'word-word words))
;; @@@ This is very WIP at moment.
(rl:defsingle shell-help-key (editor)
(handler-case
(use-first-context (editor)
(with-context ()
(multiple-value-bind (type word)
(guess-word-before (rl:get-buffer-string editor) inator::point)
(labels ((symbol-help ()
(let ((symbol (symbolify word :no-new t)))
(if symbol
(let* (result
(doc
(fatchar-io:with-output-to-fat-string (str)
(setf result
(%doc symbol :all t
:stream str)))))
(inator:message
editor
"~s ~s~%~/fatchar-io:print-string/"
type symbol
(or (and result doc)
(and (fboundp symbol)
(function-help symbol 0))))
(setf (rl::keep-message editor) t))
(inator:message
editor "FAIL ~s ~s ~s~%" type symbol word))))
(command-help ()
(inator:message
editor
"~s ~s~%~/fatchar-io:print-string/"
type word
(fatchar-io:with-output-to-fat-string (stream)
(%doc word :stream stream)))
(setf (rl::keep-message editor) t)))
(case type
(:symbol (symbol-help))
(:command (command-help))
(:command-or-symbol
(case (command-type *shell* word)
((:builtin-commandd :shell-command :command)
(command-help))
(:external-command
;; @@@ if it has a man page show it
;; otherwise do our fake command help
(command-help))
(:file
;; @@@ show a man page if it exists
)
(:directory
;; @@@ like maybe ls -l or something, but that violates
;; dependencies :(
)
(t (symbol-help))))
(otherwise
;; (inator:message editor "Sorry. No help for a ~s." type)
(error "FUCKALL")
))))))
(condition (x)
(inator:message editor "Help got an error: ~s" x))))
(defun nth-expr-word (n expr)
"Return the Nth, potentially unwrapped, word of the shell-expr."
(let ((w (nth n (shell-expr-words expr))))
(typecase w
(shell-word
(shell-word-word w))
(t w))))
(defun resolve-command (command &optional seen)
"Try to figure out what the command really is, for testing accepts."
(let ((alias (gethash command (shell-aliases *shell*)))
word)
(if alias
(progn
(setf word (nth-expr-word 0 (shell-read alias)))
(if (not (position command seen :test #'equal)) ; don't circle
(progn
(pushnew command seen :test #'equal)
(resolve-command word seen))
word))
command)))
(defun get-accepts (expr)
(typecase expr
(shell-expr
(get-accepts (nth-expr-word 0 expr)))
(list
(get-accepts (if (keywordp (car expr))
(cdr expr)
(car expr))))
(string
(let* ((cmd-name (resolve-command expr))
(cmd (get-command cmd-name)))
(and cmd (command-accepts cmd))))
(t
:unspecified)))
;; *accepts*
;; accept-type
;; where
;; accept-type =>
;; keyword
;; or
;; Fake types are keywords.
;; Fake types are for things that would be hard to specify with a type
(defun accepts (first-type &rest other-types)
"Return true if *ACCEPTS* matches or is a subtype of one of the given types.
This should be used rather than directly testing *ACCEPTS*."
;; (let ((types (cons first-type other-types)))
(let ((types (append (list first-type) other-types)))
(labels ((is-like (x type)
;; (format t "is-like ~s ~s~%" x type)
(or (equal x type)
#+clisp (ignore-errors (subtypep x type))
;; #-clisp (subtypep x type)
#-clisp (ignore-errors (subtypep x type))
)))
(typecase *accepts*
(cons
(if (keywordp (car *accepts*))
(some (_ (position _ *accepts* :test #'is-like)) types)
(some (_ (is-like *accepts* _)) types)))
(keyword (some (_ (eq _ *accepts*)) types))
(t (some (_ (is-like _ *accepts*)) types))))))
(defun successful (obj)
"Return true if the object represents a successful command result."
(or
;; Zero return value from a system command?
(and obj (and (numberp obj) (zerop obj)))
(consp obj))) ;; Any other value from lisp code or commands.
(defun read-parenless-args (string)
"Read and shell-eval all the expressions possible from a string and return
them as a list."
;;; @@@ I think I want to change this to do a shell-read
;;(format t "p-l line = ~s~%" string)
(loop :with start = 0 :and expr
:while (progn
(setf (values expr start)
(read-from-string string nil *real-eof-symbol*
:start start))
(not (eq expr *real-eof-symbol*)))
;;:do
;;(format t "p-l before eval arg ~s of type ~a~%" expr (type-of expr))
:collect (eval expr)))
(defmacro with-first-value-to-output (&body body)
"Evaluuate BODY and set *OUTPUT* to first value."
(with-names (vals)
`(values-list
(let ((,vals (multiple-value-list (progn ,@body))))
(setf (output) (first ,vals))
,vals))))
(defun command-type (sh command &key already-known)
"Return a keyword representing the command type of COMMAND, or NIL.
If ALREADY-KNOWN is true, only check for already cached commands, don't bother
consulting the file system."
;;(declare (ignore already-known)) ;; @@@
;; The order here is important and should reflect what actually happens
;; in shell-eval.
(let (cmd path)
(cond
((setf cmd (gethash command (lish-commands)))
(typecase cmd
(external-command :external-command)
(builtin-command :builtin-command)
(shell-command :shell-command)
(t :command)))
((gethash command (shell-aliases sh)) :alias)
((gethash command (lish-global-aliases sh)) :global-alias)
;; @@@ A loadable system isn't really a command, rather a potential
;; command, so maybe it shouldn't be in here?
((loadable-system-p command) :loadable-system)
((setf path (get-command-path
command :already-known already-known))
(if (source-command-p path)
:source-command
:file))
((and (lish-auto-cd sh)
(directory-p (expand-tilde command))) :directory)
((and (fboundp (symbolify command))) :function)
(t nil))))
;; @@@ Maybe this would be better done by typep, but then we'd have to somehow
;; put aliaes, systems, and files, into the type system?
(defun maybe-a-command-p (expr)
"Return true if ‘expr’ has a first symbol which might be a command."
(and expr
(symbolp (car expr))
(not (member (command-type *shell* (string-downcase (car expr))
:already-known t)
'(:directory :function)))))
(defun call-parenless (func line context)
"Apply the function to the line, and return the proper values. If there are
not enough arguements supplied, and *INPUT* is set, i.e. it's a recipient of
a non-I/O pipeline, supply *INPUT* as the missing tail argument."
(let ((parenless-args (read-parenless-args line))
(function-args (argument-list
(if (functionp func)
(third
(multiple-value-list
(function-lambda-expression func)))
func)))
(*context* context))
(if (and (< (length parenless-args) (length function-args))
*input*)
(progn
(if parenless-args
(progn
(with-first-value-to-output
(if (context-pipe-plus *context*)
(let ((curried
(_ (with-input (_)
(apply func `(,@parenless-args ,_))))))
;; @@@ maybe there's faster way to do this?
(apply #'omap curried (list *input*)))
(apply func `(,@parenless-args ,*input*)))))
(progn
(with-first-value-to-output
(if (context-pipe-plus *context*)
(let ((wrapper
(_ (with-input (_)
(funcall func _)))))
(omap wrapper *input*))
(apply func (list *input*)))))))
;; no *input* stuffing
(with-first-value-to-output (apply func parenless-args)))))
(defmacro maybe-do-in-background ((bg-p name args) &body body)
(with-names (thunk string-name args-val)
`(flet ((,thunk () (progn ,@body)))
(if (and ,bg-p nos:*supports-threads-p*)
(progn
(let ((,string-name (prin1-to-string ,name))
(,args-val ,args))
(setf (lish-last-background-job *shell*)
(add-job ,string-name
(or (and ,args-val
(shell-words-to-string ,args-val)) "")
(nos:make-thread #',thunk :name ,string-name)))))
(progn
(funcall #',thunk))))))
(defun eval-lisp-expr (expr)
"A wrapper to eval that does the right thing for an invidual expression as an
element in a pipeline."
(let ((- expr))
(if (context-pipe-plus *context*)
(let ((wrapper
(_ (with-input (_)
(eval expr)))))
(omap wrapper *input*))
(eval expr))))
(defun post-command (name type)
"Things to do after a command."
(run-hooks *post-command-hook* name type)
(finish-output))
(defun call-thing (thing args context &optional parenless)
"Call a command or function with the given POSIX style arguments.
THING is a COMMAND object or a function/callable symbol.
ARGS is a list of POSIX style arguments, which are converted to Lisp arguments
by POSIX-TO-LISP-ARGS and given to the COMMAND's function.
If OUT-PIPE is true, return the values:
a list of the values returned by COMMAND
a input stream from which can be read the output of command
and NIL.
If IN-PIPE is true, it should be an input stream to which *STANDARD-INPUT* is
bound during command.
If PARENLESS is set, it's the text of rest of the line to be fed to
CALL-PARENLESS."
(with-slots (in-pipe out-pipe environement) context
(let ((command-p (typep thing 'base-command))
(bg (context-background context)))
(labels ((runky (thing args)
(cond
((typep thing 'autoloaded-command)
(let ((*context* context))
;; The args can't be converted yet.
(maybe-do-in-background (bg (command-name thing) args)
(invoke-command thing args))))
(command-p
(let ((lisp-args (posix-to-lisp-args thing args))
(*context* context))
(maybe-do-in-background (bg (command-name thing) args)
(invoke-command thing lisp-args))))
(parenless
(call-parenless thing parenless context))
(t
(maybe-do-in-background (bg thing args)
(let ((- thing))
(eval-lisp-expr thing)))))))
(if out-pipe
(let ((out-str (make-stretchy-string 20)))
(values
;; @@@ This is totally stupid
(list (with-output-to-string (*standard-output* out-str)
(if in-pipe
(let ((*standard-input* in-pipe))
(runky thing args))
(runky thing args))))
(let ((oo (make-string-input-stream out-str)))
;; (format t "out-str = ~w~%" out-str)
;; (format t "(slurp oo) = ~w~%" (slurp oo))
;; (file-position oo 0)
oo)
nil))
(if in-pipe
(let ((*standard-input* in-pipe))
(if command-p
(runky thing args)
(let ((vals (multiple-value-list (runky thing args))))
(values vals nil t))))
(if command-p
(runky thing args)
;; (values (list (runky thing args)) nil t))))))))
(let ((vals (multiple-value-list (runky thing args))))
(values vals nil t)))))))))
(defun expr-to-args (expr)
"Convert the shell expression ‘expr’ to system command arguments, which must
be a list of strings. Spread expression words that need it."
(mapcan (_ (let ((x (spread (word-word _))))
(if (listp x)
(mapcar #'princ-to-string x)
(list (princ-to-string x)))))
(shell-expr-words expr)))
(defun do-system-command (expr context)
"Run a system command.
EXPR is a shell-expr.
IN-PIPE is an input stream to read from, if non-nil.
OUT-PIPE is T to return a input stream which the output of the command can be
read from."
(let* ((command-line
;; System command arguments must be strings
;; (mapcar (_ (or (and (stringp _) _)
;; (princ-to-string (shell-word-word _))))
;; (shell-expr-words expr)))
(expr-to-args expr))
(program (car command-line))
(args (cdr command-line))
(path #| (get-command-path program) |#)
result result-stream pid status job)
;; Since run-program can't throw an error when the program is not found,
;; we try to do it here.
(loop
:while
(not (with-simple-restart (continue "Try the command again.")
(setf path (get-command-path program))
(when (not path)
(signal
'unknown-command-error
:name 'path
:command-string program :format "not found."))
(setf path (get-command-path program)))))
;; This actually should be in the child process:
;;(set-default-job-sigs)
(with-slots (in-pipe out-pipe environment background) context
(if (or in-pipe out-pipe)
(progn
;; (when in-pipe
;; (format t "thingy: ~s~%would have been: ~s~%"
;; in-pipe
;; (slurp in-pipe))
;; (file-position in-pipe 0))
(setf (values result-stream pid)
(apply #'nos:pipe-program
`(,path ,args
,@(when in-pipe `(:in-stream ,in-pipe))
,@(when (not out-pipe) '(:out-stream t))
,@(when environment
`(:environment ,environment)))))
(when (not out-pipe)
(setf result-stream nil)))
;; No pipes
(let (#|(tail (last args))
background |#)
;;(format t "tail = ~s~%" tail)
#|
(when (equal (car tail) "&")
(setf args (nbutlast args))
(setf background t)
;; (format t "background = ~a~%args = ~s~%" background args)
)
|#
(setf pid
(apply
;; #+(or clisp ecl lispworks) #'fork-and-exec
;; #-(or clisp ecl lispworks) #'nos:run-program
#+unix #'uos::forky
#-unix #'nos:run-program
`(,path ,args
,@(when environment
`(:environment ,environment))
:background ,background))
job (add-job program
(join-by-string command-line #\space) pid))
;; &&& temporarily re-get the terminal so we can debug
;; (sleep .2)
;; (uos::syscall (uos:tcsetpgrp 0 (uos:getpid)))
;; (cerror "Keep going" "Your breakpoint, sir?")
(if background
(setf (job-status job) :running
(lish-last-background-job *shell*) job)
(progn
;; Wait for it...
(multiple-value-setq (result status)
(nos:wait-and-chill pid))
(handle-job-change job result status :foreground t))))))
(values (or result '(0)) result-stream)))
;; This ends up calling one of the following to do the actual work:
;; do-system-command , if it's an external command
;; call-parenless , if it's a function
;; call-thing , if it's a Lish command
;; eval , if it's a object
;; This also directs alias expansion, and lisp sub-expression evaluation.
(defun shell-eval-command (sh expr context &key no-alias)
"Evaluate a shell expression that is a command.
If the first word is an alias, expand the alias and re-evaluate.
If the first word is a system that can be loaded, load it and try to call it
as a lish command. This is vaugely like autoload.
If the first word is a lish command, call it.
If the first word is an existing directory and the auto-cd option is set, try
to change to it.
If the first word is an executable file in the system path, try to execute it.
If the first word is a symbol bound to a function, call it with the arguments,
which are read like lisp code. This is like a ‘parenless’ function call.
Otherwise just try to execute it with the system command executor, which will
probably fail, but perhaps in similar way to other shells."
(let* (;(words (shell-expr-words expr))
(cmd (word-word (nth-expr-word 0 expr)))
(command (get-command cmd))
(alias (gethash cmd (shell-aliases sh)))
(expanded-expr (lisp-exp-eval expr))
result result-stream path)
;; These are in order of precedence, so:
;; aliases, lisp path, commands, system path
(flet ((sys-cmd ()
"Do a system command."
(run-hooks *pre-command-hook* cmd :system-command)
(setf (values result result-stream)
(do-system-command expanded-expr context))
(post-command cmd :system-command)
(when (not result)
(format t "Command failed.~%"))
;; (finish-output) ; @@@ is this really a good place for this?
(values result result-stream nil))
(rest-of-the-line (expr)
"Return the rest of the line after the first word."
(if (> (length (shell-expr-words expr)) 1)
(shell-words-to-string (rest (shell-expr-words expr)))
""))
(literal-rest-of-the-line (expr)
(if (> (length (shell-expr-words expr)) 1)
(shell-words-to-string (rest (shell-expr-words expr))
:literal-line (shell-expr-line expr))
""))
(run-fun (func line)
"Apply the func to the line, and return the proper values."
(run-hooks *pre-command-hook* cmd :function)
(values-list
(let ((vals (multiple-value-list
(call-thing func '() context line))))
(post-command cmd :function)
;;(finish-output) ; @@@ is this really a good place for this?
vals))))
(cond
;; Alias
((and alias (not no-alias))
;; re-read and re-eval the line with the alias expanded
(shell-eval (expand-alias alias expanded-expr)
:context context
:no-expansions t))
;; Lish command
((typep command '(or internal-command autoloaded-command))
(call-thing command (subseq (shell-expr-words expanded-expr) 1)
context))
;; external command
((typep command 'external-command)
(run-hooks *pre-command-hook* cmd :command)
(sys-cmd))
((functionp cmd)
;; (format t "CHOWZA ~s~%" (rest-of-the-line expr))
(run-fun cmd (rest-of-the-line expr)))
((and (symbolp cmd) (fboundp cmd))
;; (format t "FLEOOP ~s~%" (rest-of-the-line expr))
(run-fun (symbol-function cmd) (rest-of-the-line expr)))
;; Autoload
;; @@@ perhaps we should cache since it seems dumb to check each time
;; for things we already know are a system command?
((and (lish-autoload-from-asdf sh)
(in-lisp-path cmd)
(setf command (load-lisp-command
cmd :silent (lish-autoload-quietly sh))))
(call-thing command (subseq (shell-expr-words expanded-expr) 1)
context))
;; Source a file
((and (not (listp cmd))
(setf path (get-command-path cmd))
(source-command-p path)
(setf command (make-source-command path)))
(call-thing command (subseq (shell-expr-words expanded-expr) 1)
context))
((stringp cmd)
;; If we can find a command in the path, try it first.
(cond
(path
(sys-cmd))
((and (lish-auto-cd sh) (directory-p cmd))
(when (> (length (shell-expr-words expr)) 1)
(cerror "Ignore the rest of the line."
"Arguments aren't allowed after the auto-cd directory."))
(change-directory cmd))
(t ;; Otherwise try a parenless Lisp line.
(multiple-value-bind (symb pos)
(read-from-string (shell-expr-line expr) nil nil)
(declare (ignore pos))
(if (and (symbolp symb) (fboundp symb))
(if (macro-function symb)
(progn
(shell-eval (cons symb
(read-parenless-args
(rest-of-the-line expr)))
:context context))
(progn
(run-fun (symbol-function symb)
;;(subseq (shell-expr-line expr) pos)
;;(rest-of-the-line expr)
(literal-rest-of-the-line expr)
)))
;; Just try a system command anyway, which will likely fail.
(sys-cmd))))))
(t ;; Some other type, just return it, like it's self evaluating.
;;(values (multiple-value-list (eval cmd)) nil t))))))
;; (values (multiple-value-list
;; ;;(with-first-value-to-output
;; (call-thing (car cmd) (cdr cmd) context))
;; nil t)
;; The first word was probably a Lisp expression, evaluating or not.
;; Just treat the rest of the words as potentially Lisp expressions.
(let ((first-word (elt (shell-expr-words expr) 0)))
(if (and (shell-word-p first-word)
(shell-word-eval first-word))
(call-thing cmd nil context)
(values (list (eval-lisp-expr cmd) nil t))))
;; @@@ How can we evaluate the words after the first and return all
;; their possibly multiple values?
;; (loop :for w :in (shell-expr-words expr)
;; (when (shell-word-eval w)
;; (call-thing (word-word w) nil context)
;; ;; @@@@@
)))))
;; This does normal expansions, sets up piping and redirections and
;; eventually calls shell-eval-command.
(defun shell-eval (expr &key no-expansions (shell *shell*) (context *context*))
"Evaluate the shell expression EXPR. If NO-EXPANSIONS is true, don't expand
aliases. Return a list of the result values, a stream or NIL, and a boolean
which is true to show the values.
Generally SHELL-EVAL takes the result of SHELL-READ. EXPR is either a
SHELL-EXPR structure or some other Lisp type. If it's not a SHELL-EXPR then
just eval it. If it is a SHELL-EXPR then do the shell expansions on it, as done
by DO-EXPANSIONS. If the first word of EXPR is a list, then it is a compound
command, which is a :PIPE, :AND, :OR, :SEQUENCE.
:PIPE takes the output from the piped command, and feeds it as input to
the subcommand.
:AND evaluates each subcommand until one of them is false.
:OR evaluates each subcommand until one of them is true.
:SEQUENCE evaluates each subcommand in sequence, ignoring return values.
"
(let ((*context* (or context (make-context)))
first-word vals out-stream show-vals)
(with-slots (in-pipe out-pipe environment flipped-io pipe-plus pipe-dot
background)
*context*
(setf flipped-io nil)
(macrolet
((eval-compound (test new-pipe)
"Do a compound command. TEST determines whether the next~
part of the command gets done. NEW-PIPE is true to make a~