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
/
Copy pathbuiltin.lisp
2199 lines (2035 loc) · 75.9 KB
/
builtin.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
;;;
;;; builtin.lisp - Lish built-in commands.
;;;
;; Here we define the commands that are built in to Lish.
;; Most of these are for familiarity and superficial compatability with a
;; POSIX shell. You could get rid of them, or use different names, since most
;; of their functionality is contained in Lisp functions.
(in-package :lish)
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Command definitions
(defun home-directory ()
"Return a namestring of the user's home directory in a particular way."
(or (nos:environment-variable "HOME") (nos:user-home)
(namestring (user-homedir-pathname))))
(defun push-directory-ring (dir)
"Push ‘dir’ on the directory ring."
(with-slots (directory-ring directory-ring-size) *shell*
(when (> (length directory-ring) directory-ring-size)
(pop directory-ring))
(setf directory-ring (append directory-ring (list dir)))))
(defun find-directory-in-ring (regexp)
"Return a directory from the directory ring matching ‘regexp’. If not found
just return the current directory."
(let ((result (find regexp (lish-directory-ring *shell*)
:test #'cl-ppcre:scan)))
(or result (nos:current-directory))))
(defbuiltin cd ((directory directory :help "Directory to change to."))
"Change the current directory to DIRECTORY. If DIRECTORY isn't specified,
use *input* if it's a directory or the shell's idea of the user's home
directory."
(when (equal directory "-")
(setf directory (lish-old-pwd *shell*)))
(when (begins-with "=" directory)
(setf directory (find-directory-in-ring (subseq directory 1))))
(push-directory-ring (nos:current-directory))
(setf (lish-old-pwd *shell*) (nos:current-directory))
(nos:change-directory (or directory
(and *input* (nos:directory-p *input*) *input*)
(home-directory)))
;; Update $PWD like traditional Unix shells.
;; @@@ Maybe someday we can get rid of this.
(setf (nos:environment-variable "PWD") (nos:current-directory)))
(defbuiltin pwd ()
"Print the current working directory."
(format t "~a~%" (setf *output* (nos:current-directory)))
*output*)
(defbuiltin pushd
((directory directory :help "Directory to push on the stack."))
"Change the current directory to DIRECTORY and push it on the the front of the
directory stack."
(when (not directory)
(setf directory (pop (lish-dir-list *shell*))))
(push (nos:current-directory) (lish-dir-list *shell*))
(!cd :directory directory))
(defbuiltin popd ((number number :help "Number of item to pop."))
"Change the current directory to the top of the directory stack and remove it
from stack."
(declare (ignore number))
(let ((dir (pop (lish-dir-list *shell*))))
(!cd :directory dir)
dir))
(defbuiltin dirs ()
"Show the directory stack."
(format t "~{~a~%~}" (lish-dir-list *shell*))
(setf *output* (lish-dir-list *shell*)))
(defbuiltin suspend ()
"Suspend the shell."
(opsys:suspend-process))
(defun job-id-list ()
"Return a list of suspended job ids."
(loop :for j :in (lish-jobs *shell*)
:collect (job-id j)))
(define-builtin-arg-type job-descriptor (arg-lenient-choice)
"A job descriptor."
()
(:default-initargs
:choice-func #'job-id-list))
;; (defclass arg-job-descriptor (arg-lenient-choice)
;; ()
;; (:default-initargs
;; :choice-func #'job-id-list)
;; (:documentation "A job descriptor."))
(defbuiltin fg
((job-descriptor job-descriptor :optional t :help "Job to resume."))
"Resume a suspended job."
(let (job)
(cond
((or (null (lish-jobs *shell*))
(= (length (lish-jobs *shell*)) 0))
(format t "No jobs to resume.~%")
(return-from !fg (values)))
((= (length (lish-jobs *shell*)) 1)
(setf job (first (lish-jobs *shell*))))
(t
(setf job (find-job job-descriptor))))
(if (not job)
(format t "Couldn't find a job matching ~a.~%" job-descriptor)
(continue-job-in-foreground job))))
(defbuiltin bg
((job-descriptor job-descriptor :optional t :help "Job to backaground."))
"Put a job in the background."
(let ((job (find-job job-descriptor)))
;; (format t "job-descriptor = ~s ~a job = ~s~%"
;; job-descriptor (type-of job-descriptor) job)
(if job
(continue-job-in-background job)
(format t "Couldn't find a job matching ~a.~%" job-descriptor)))
(values))
(defbuiltin jobs
((long boolean :short-arg #\l :help "Show the longer output.")
(all boolean :short-arg #\a :help "Show all (or at least maybe more) jobs."))
"Lists spawned processes that are active."
(flet ((job-row (j)
(with-slots (id name command-line status) j
(let ((result
(list id (job-type-name j) name command-line))
(extra
(when long
(typecase j
(lisp-job
(job-resume-function j))
(system-job
(s+ status " " (job-pid j)))))))
(if extra
(nconc result (list extra))
result)))))
(let ((table
(make-table-from
(loop :for j :in (if all
`(,@(lish-jobs *shell*)
,@(list-all-jobs 'lisp-job)
,@(list-all-jobs 'thread-job))
(lish-jobs *shell*))
:collect (job-row j))
:columns
`((:name "ID" :type number)
(:name "Type")
(:name "Name")
(:name "Command")
,(when long '(:name "Extra"))))))
(with-grout ()
(grout-print-table table :print-titles nil))
(setf *output* table))))
(defbuiltin history
((clear boolean :short-arg #\c
:help "Clear the history.")
(write boolean :short-arg #\w
:help "Write the history to the history file.")
(read boolean :short-arg #\r
:help "Read the history from the history file.")
(append boolean :short-arg #\a
:help "Append the history to the history file.")
(read-not-read boolean :short-arg #\n
:help "Read history items not already read from the history file.")
;; @@@ I think I'd lke it if it would only apply only for that command,
;; but that probably involves changes to the history-store protocol.
(filename pathname :short-arg #\f
:help "Use ‘pathname’ as the history file. If given with other commands,
then the pathname is changed before the command.")
(show-times boolean :short-arg #\t
:help "Show history times.")
(delete integer :short-arg #\d
:help "Delete the numbered history entry.")
(table boolean :short-arg #\T
:help "True to return a table of the history.")
(yes boolean :short-arg #\y :help "Don't warn for destructive operations.")
(dedup boolean :short-arg #\D :help "Remove duplicates."))
"Show a list of the previously entered commands."
;; Check argument conflicts
(cond
;; @@@ Could this kind of thing be done automatically?
;; Yes, it totally could with the grammer based argument parser.
((and clear (or write read append read-not-read filename show-times dedup
delete))
(error "CLEAR should not be given with any other arguments."))
((and delete (or write read append read-not-read filename show-times dedup
clear))
(error "DELETE should not be given with any other arguments."))
((and dedup (or write read append read-not-read filename show-times clear
delete))
(error "DEDUP should not be given with any other arguments."))
((> (count t `(,write ,read ,append ,read-not-read)) 1)
(error
"Only one of WRITE, READ, APPEND, or READ-NOT-READ should be given."))
((and filename (not (or read write append read-not-read)))
(error
"FILENAME is only useful with READ, WRITE, APPEND, or READ-NOT-READ.")))
;; Set the file name before other commands.
(when filename
(setf (history-store-file-name (lish-history-store *shell*))
filename))
(cond
(clear
(when (or yes (yes-or-no-p "Are you sure you want to erase history?"))
(rl:history-clear :lish)))
(dedup
(when (or yes (yes-or-no-p
"Are you sure you want to remove duplicates from history?"))
(rl:history-dedup :lish)))
;; @@@ It might be nice if we could say what happend, like how many
;; history items were saved or loaded.
(write (save-history *shell*))
(read (load-history *shell*))
(append (save-history *shell* :update t))
(read-not-read (load-history *shell* :update t))
(delete (format t "Sorry, delete is not implemented yet.~%"))
(table (setf *output* (rl:history-table :context :lish)))
(t (rl:show-history :context :lish :show-time show-times
;; :show-extra (get-option *shell* 'history-save-values)
))))
;; This seems stupid and unnecessary.
;; (defbuiltin #:|:| (("args" t :repeating t))
;; "Arguments are evaluated for side effects."
;; (declare (ignore args))
;; (values))
(defbuiltin echo
((no-newline boolean :short-arg #\n :help "Don't output a newline.")
(args t :rest t :help "Words to print."))
"Output the arguments. If -n is given, then don't output a newline a the end."
(let ((echo-list (or args (if (consp *input*) *input* (list *input*)))))
(format t "~{~a~#[~:; ~]~}" echo-list)
(when (not no-newline)
(terpri))
;; pop the bubble
(setf *output* (if (olength-at-least-p 2 echo-list)
echo-list
(oelt echo-list 0)))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; help
(defun print-columnar-help (rows)
(with-grout ()
(let ((table (make-table-from
rows
:columns '((:name "Name")
(:name "Description" :align :wrap)))))
(grout-print-table table :print-titles nil)
table)))
(defun command-list (type)
"Return a list of commands of TYPE."
(sort (loop :for k :being :the :hash-values :of (lish-commands)
:when (typep k type)
:collect k)
#'string-lessp
:key #'command-name))
(defun print-multiple-command-help (commands)
(let ((rows
(loop
:for k :in commands
:collect
(etypecase k ;; @@@ make generic?
(autoloaded-command
(list (command-name k)
(string-downcase (command-load-from k))
;; (documentation
;; (command-function-name (command-name k)) 'function)
))
(command
(let* ((doc (documentation (command-function k) 'function))
(pos (position #\. doc)))
(list
(command-name k)
;; Only the first sentance, i.e. up to the first period,
;; without newlines.
(substitute #\space #\newline
(if pos
(subseq doc 0 (1+ pos))
doc)))))))))
(print-columnar-help rows)))
;; This has to make sure to be able to operate without a current shell or even,
;; current terminal, since it's called by the documentation method.
(defun print-command-help (cmd &key (stream
;; (or *terminal* *standard-output*)
*standard-output*)
indent)
"Print documentation for a command. Return a table."
(declare (ignore indent))
(with-grout (*grout* stream)
(grout-format "~a~%" (documentation cmd 'function))
(let (table)
(when (and (command-arglist cmd)
(not (zerop (length (command-arglist cmd)))))
(grout-format "Arguments:~%")
(grout-print-table
(setf table
(make-table-from
(loop :for a :in (command-arglist cmd)
:when (not (arg-hidden a))
:collect
(list (if (arg-short-arg a) (s+ " -" (arg-short-arg a)) " ")
(if (arg-long-arg a) (s+ "--" (arg-long-arg a))
(if (arg-short-arg a) "" (arg-name a)))
#| (or (arg-default a) "") |#
(string-downcase (arg-type a))
(or (and (slot-boundp a 'help)
(substitute #\space #\newline (arg-help a)))
(arg-name a))))
:columns
'((:name "Short") (:name "Long") (:name "Type")
(:name "Help" :align :wrap))))
:long-titles nil :print-titles nil #|:max-width (get-cols)|#
:trailing-spaces nil))
(when (and (command-accepts cmd)
(not (eq (command-accepts cmd) :unspecified)))
(grout-format "~&Accepts: ~a~%" (command-accepts cmd)))
(typecase cmd
(command
(when (and (not (command-built-in-p cmd))
(command-loaded-from cmd))
(grout-format "Loaded from: ~a~%" (command-loaded-from cmd))))
;; (autoloaded-command
;; (grout-format "Load from: ~a~%" (command-load-from cmd)))
)
table)))
;; For use by other things. Like my "doc" command.
;; This used to be (eql :command), but that was taken by lispworks, so fuck it.
(defmethod documentation ((symbol symbol) (type (eql 'command)))
(let ((cmd (get-command (string-downcase (symbol-name symbol)))))
(when cmd
(with-output-to-string (str)
(print-command-help cmd :stream str)))))
(defmethod documentation ((command command) (type (eql 'command)))
(with-output-to-string (str)
(print-command-help command :stream str)))
(defmethod describe-object ((object command) stream)
(format stream "~s [~s]~%" (command-name object) (type-of object))
(print-command-help object :stream stream)
(call-next-method))
(defvar *help-table* nil
"Hash table of help functions.")
(eval-when (:compile-toplevel :load-toplevel :execute)
(defun help-function-name (subject)
"Return the help function for ‘subject’."
(symbolify (s+ "help-" subject)))
(defun help-table ()
;; Allow shells to have a customized help table.
(or (and *shell* (shell-help-table *shell*))
*help-table*
(setf *help-table*
(make-hash-table :size 16 :test 'equalp))))
(defmacro defhelp (subject () &body body)
"Define a help subject for ‘subject-name’, which can be invoked by the ‘help’
command. You should provide a docstring in the body which will be the subject
description. The body should probably output the help to *standard-output*,
but could do whatever you think is helpful, such as invoking a more sophisticated
help viewer."
(let ((func-name (help-function-name subject)))
`(progn
(defun ,func-name ()
,@body)
(setf (gethash (string ',subject) (help-table))
(function ,func-name))))))
(defparameter *basic-help*
"~
Lish version ~a help:
This is Lish, a command line shell with Lisp.
A brief summary of what to type:
command [arg*...] Run a program in your path with the given ARGs.
([expressions]...) Evaluate Lisp expressions.
help [subject] Show help on the subject.
exit Exit the shell.
Further help is available on these subjects:
")
(defhelp "" ()
"Basic help."
(format t *basic-help* *version*)
(let ((table
(make-table-from
(loop :for s :in (remove "" (help-subjects) :test #'equal)
:collect (list s (help-subject-description s)))
:columns
'((:name "Subject" :format " ~va")
(:name "Description" :format " ~va")))))
(with-grout ()
(grout-print-table table :print-titles nil))
(setf *output* table)))
(defhelp builtins ()
"Built-in commands."
(format t "Built-in commands:~%")
(setf *output*
(print-multiple-command-help (command-list 'builtin-command))))
(defhelp commands ()
"Defined commands."
(format t "Defined commands:~%")
(setf *output*
(print-multiple-command-help (command-list 'shell-command))))
(defhelp external ()
"Defined external commands."
(format t "Defined external commands:~%")
(setf *output*
(print-multiple-command-help (command-list 'external-command))))
(defparameter *editor-help*
"You can use some Emacs-like commands to edit the command line.
Some notable keys are:
<Tab> Try to complete the word in front of the cursor. Twice for more.
? Show what input is expected. List possibilities.
<Control-D> Quit, when on an empty line, or delete the following character.
<Control-P> Previous history line. Also the <Up Arrow> key.
<Control-N> Next history line. Also the <Down Arrow> key.
<Control-B> Move the cursor back one character. Also the <Left Arrow> key.
<Control-F> Move the cursor forward one character. Also the <Right Arrow> key.
<Control-Q> Quote next character, like if you want to really type '?'.
")
;; This doesn't really work unless you start lish from inside a tiny-repl, so we
;; probably shouldn't advertise it. It's relatively pointless anyway.
;; <F9> Switch back and forth between LISH and the lisp REPL.
(defhelp editor ()
"The line editor."
(format t *editor-help*))
;; @@@ I want this to be markdown or something. At least fit the paragraphs to
;; the width of the terminal.
(defparameter *syntax-help*
"The syntax is a combination of POSIX shell and Lisp, hopefully in a way that
is familiar and not too surprising to those who know either.
It is vaguely like:
; comment
command [arg...]
command \"string\" ,*lisp-object* (lisp-code) $ENV_VAR
command *.glob ?ooba[rz]
command word\\ with\\ spaces \"string \\\" with a double quote\"
command | command | ...
command < file-name
command > file-name
([lisp expressions...])
Basically, inside parentheses you get Lisp reader syntax. Outside parentheses,
you get a very simplified shell syntax with Lisp strings and comments.
Some typical shell expansions are done in command arguments, such as shell
globbing with *,?,and [], environment variable expansions with $VAR, and
home directory expansions with ~~user. Pipeline and redirections should work
nearly as expected.
Commands can be:
- System executables in your standard PATH
- Built-in or later defined commands, defined with DEFCOMMAND
- Names of systems in your ASDF \"path\" which are expected to define a
command with the same name as the system, which is then invoked.
- Lisp functions or methods
")
(defhelp syntax ()
"Shell syntax."
(format t *syntax-help*))
(defparameter *differences-help*
"Lish is different from a POSIX shell. The most notable differences are:
- Parentheses switch to Lisp syntax, and don't mean run in sub-shell.
Lisp inside parentheses is evaluated and substituted in the current line.
- String quoting is done only with double quote \". Single quote ' and back
quote `, are not special to avoid confusion with Lisp syntax.
- The prefix VAR=value isn't supported. Use the ‘env’ command instead.
- Redirection syntax is different, e.g \"2>&1\" doesn't work.
- Commands can be Lish commands and Lisp functions, as well as executables
in your PATH. Lish commands can be searched for and automatically loaded
from ASDF places, manipulated by the ‘ldirs’ command.
- Most scripting related shell commands are missing, e.g. if, test, case.
Scripting parameter expansion like $1 $* ${} are missing. Use Lisp instead.
- Shell expansions are different. Many expansions can be done by Lish functions
starting with ! , such as (!_ \"ss\") expands to a list of strings of the
lines of output, (!? \"grep fuse /proc/filesystems\") returns a boolean status.
Comma can be used to substitute a Lisp value, e.g. \"echo ,*package*\".
- Comments start with ; not #
For more detail see the section ‘Differences from POSIX shells’ in docs/doc.org
")
(defhelp differences ()
"Differences from other ehells."
(format t *differences-help*))
(defhelp options ()
"Shell options."
(format t "~
Options can be examined and changed with the ‘opt’ command.~%~
Shell options:~%")
(setf *output*
(print-columnar-help
(loop :for o :in (lish-options *shell*) :collect
`(,(arg-name o)
,(substitute #\space #\newline (arg-help o)))))))
(defhelp keys ()
"Key bindings."
(format t "Here are the keys active in the editor:~%")
(!bind :print-bindings t))
(defparameter *startup-help*
"Here's what the shell does when starting:
- Increment the *lish-level* and LISH_LEVEL.
- Update the LISH-USER package with symbols from CL-USER.
- Load the startup commands file, which comes from one of:
- The value passed to the shell with the :init-file keyword
- The value of *lishrc*
- The standard place for configuration files:
(opsys:path-append (opsys:config-dir \"lish\") \"lishrc\")
which is: ~s
- The value of *default-lishrc*,
which is: ~s
The current value is: ~s
- Make sure *theme* is set, using the (default-theme) if necessary.
- Make a new line editor, which might make a new *terminal*. See the
documentation for RL for details.
- Evaluate the *enter-shell-hook* functions.
The current value is: ~a.~%~%")
(defhelp startup ()
"What happens when the shell starts."
(format t *startup-help*
(opsys:path-append (opsys:config-dir "lish") "lishrc")
*default-lishrc*
*lishrc*
*enter-shell-hook*))
(defun help-subjects ()
"Return a list of help subjects."
(sort (omap-as 'list (_ (string-downcase (oelt _ 0))) (help-table)) #'string<))
(defun help-function (subject)
"Return the help function for for ‘subject’, or NIL if it isn't defined."
(gethash (string subject) (help-table)))
(defun help-subject-description (subject)
"Return the description of help subject ‘subject’."
;; (documentation (help-function-name subject) 'function))
(documentation (help-function subject) 'function))
(defun help-on (subject)
"Print help on ‘subject’."
(let ((func (help-function subject)))
(if func
(funcall func)
(format t "I don't know about the subject \"~a\"~%" subject))))
(defun help-choices ()
"Return a list of choices for a help subject."
(concatenate
'list (remove "" (help-subjects) :test #'equal)
(mapcar #'(lambda (x)
(or (and (symbolp x) (string-downcase (symbol-name x)))
(and (stringp x) x)
x))
*command-list*)))
(define-builtin-arg-type help-subject (arg-choice)
"Something which we can get help on."
()
(:default-initargs
:choice-func
#+clisp 'help-choices ; I'm not sure why.
#-clisp #'help-choices))
(defbuiltin help
((subject help-subject :help "Subject to get help on."))
"Show help on the subject. Without a subject show some subjects that are
available."
(when (null subject)
(setf subject ""))
(let (cmd)
(cond
((help-function subject)
(help-on subject))
((setf cmd (get-command subject)) ;; Try a specific command
(let* ((symb (intern (string-upcase subject) :lish))
(doc (when cmd (documentation cmd 'function)))
(fdoc (when (fboundp symb)
(documentation (symbol-function symb) 'function))))
(cond
(doc (setf *output* (print-command-help cmd)))
(fdoc (format t "Lisp function:~%~a~%" fdoc))
(cmd (format t "Sorry, there's no help for \"~a\".~%" subject)))))
(t
(format t "I don't know about the subject \"~a\"~%" subject)))))
(defmethod documentation ((b command) (doctype (eql 'function)))
"Return the documentation string for the given shell command."
(with-output-to-string (str)
(format str "~a" (posix-synopsis b))
(let ((doc (documentation (command-function b) 'function)))
(when doc
(format str "~%~a" doc)))))
;; (when (command-loaded-from b)
;; (format str "~%Loaded from ~a" (command-loaded-from b)))
(defun set-alias (name expansion &key global (shell *shell*))
"Define NAME to be an alias for EXPANSION.
NAME is replaced by EXPANSION before any other evaluation."
(setf (gethash name
(if global
(lish-global-aliases shell)
(shell-aliases shell)))
expansion))
(defun unset-alias (name &key global (shell *shell*))
"Remove the definition of NAME as an alias."
(remhash name (if global
(lish-global-aliases shell)
(shell-aliases shell))))
(defun get-alias (name &key global (shell *shell*))
"Return the alias definition of ‘NAME’, or NIL if there isn't one.
- SHELL The shell instance to look in, which defaults to *SHELL*.
- GLOBAL Return the 'global' alias."
(if global
(gethash name (lish-global-aliases shell))
(gethash name (shell-aliases shell))))
(defun alias (name)
"Return the alias definition of NAME, or NIL if there isn't one."
(gethash name (shell-aliases *shell*)))
(defsetf alias set-alias
"Set the alias definition.")
(defun edit-alias (name &key global)
(rl :prompt (s+ "alias " name " ")
:string (or (get-alias name :global global
:shell *shell*)
"")))
(defbuiltin alias
((global boolean :short-arg #\g :help "True to define a global alias.")
(edit boolean :short-arg #\e :help "True to edit the alias's value.")
(name string :help "Name of the alias.")
(expansion string :rest t :help "Text to expand to."))
"Define NAME to expand to EXPANSION when starting a line."
(if (not name)
(let ((table
(make-table-from
(loop
:for a :being :the :hash-keys
:of (if global
(lish-global-aliases *shell*)
(shell-aliases *shell*))
:collect (list a (get-alias a :global global :shell *shell*)))
:columns '((:name "Alias") (:name "Expansion")))))
(omapn (_ (format t "alias ~a ~:[is not defined~;~:*~w~]~%"
(oelt _ 0) (oelt _ 1)))
table)
(setf *output* table))
(if (not expansion)
(if edit
(set-alias name (edit-alias name) :global global)
(format t "alias ~a ~:[is not defined~;~:*~w~]~%"
name (get-alias name :global global :shell *shell*)))
(set-alias name (join-by-string expansion " ")
:global global :shell *shell*))))
(defbuiltin unalias
((global boolean :short-arg #\g :help "True to define a global alias.")
(name string :optional nil :help "Name of the alias to forget."))
"Remove the definition of NAME as an alias."
(unset-alias name :global global :shell *shell*))
(defbuiltin exit ((values string :repeating t :help "Values to return."))
"Exit from the shell. Optionally return values."
(when values
(setf (shell-exit-values *shell*) (loop :for v :in values :collect v)))
(setf (shell-exit-flag *shell*) t))
;; Override an implementations quit function, so that we only exit one level
;; of the shell, not the whole Lisp system.
(defbuiltin quit ((values string :repeating t :help "Values to return."))
:keys-as args
"Exit from the shell. Optionally return values."
(apply #'!exit args))
(defbuiltin source ((filename pathname :optional nil
:help "Filename to read."))
"Evalute lish commands in the given file."
(without-warning (load-file filename)))
;; XXX I wish this would work without using the :use-supplied-flag, just using
;; the default value of :toggle in boolean-toggle, but there is some kind of
;; bug or something about class default args at compile time that I don't
;; understand.
(defbuiltin debug
((state boolean-toggle :help "State of debugging." :use-supplied-flag t))
"Toggle shell debugging."
(setf (lish-debug *shell*)
(if (or (not state-supplied-p) (eql state :toggle))
(not (lish-debug *shell*))
state))
(format t "Debugging is ~:[OFF~;ON~].~%" (lish-debug *shell*)))
(defbuiltin export
((remove boolean :short-arg #\n
:help "True to stop the NAME from being exported.")
(edit boolean :short-arg #\e
:help "True to edit the value of the variable.")
(name string :help "Name of the variable to export.")
(value string :help "Value of the variable to export."))
"Set environment variable NAME to be VALUE. Omitting VALUE, just makes sure
the current value of NAME is exported. Omitting both, prints all the exported
environment variables. If NAME and VALUE are converted to strings if necessary.
If NAME has an equal sign ‘=’ in it, do the POSIX shell style of NAME=value."
(when (not name)
(setf name *input*))
(when (and name (not (stringp name)))
(setf name (princ-to-string name)))
(when (and value (not (stringp value)))
(setf value (princ-to-string value)))
(setf *output*
(if name
(let (pos)
(cond
((setf pos (position #\= name)) ; POSIX style
(let ((n (subseq name 0 pos))
(v (subseq name (1+ pos))))
(setf (nos:environment-variable n) v)))
(remove
(prog1 (nos:environment-variable name)
(setf (nos:environment-variable name) nil)))
(edit
(setf (nos:environment-variable name)
(rl :prompt (s+ "export " name #\=)
:string (or value (nos:environment-variable name)))))
(value
(setf (nos:environment-variable name) value))
(t
(nos:environment-variable name)))) ; Actually does nothing
(progn
(dlib-interactive:printenv)
(nos:environment)))))
#|-+
|\| So we have (from a man page):
|\|
|\| env [-i] [name=value ...] [utility [argument ...]]
|\|
|\| as a lambda list:
|\|
|\| (&key ignore-environment variable-assignments shell-command)
|\|
|\| but that doesn't have enough information. So
|\|
|\| (:or "-i" "var=value" (:and "cmd")
|\|@@@@@@@@@@@@@@@
|\|
|\| (("ignore-environment" boolean :short-arg #\i
|\| :help "Ignore the environment.")
|\| :positional
|\| ("variable-assignment" string :repeating t :matches "\\S+=\\S+"
|\| :help "Assingment to make in the environment.")
|\| ("shell-command" shell-command
|\| :help "Command to execute with the possibly modified environment.")
|\| ("arguments" string :repeating t
|\| :help "Variable assignments, commands and command arguments."))
|\|
Vauguely like how I would like:
(defbuiltin env
((ignore-environment boolean :short-arg #\i
:help "Ignore the environment.")
(variable-assignment string :repeating t
:help "Assingment to make in the environment.")
(shell-command shell-command
:help "Command to execute with the possibly modified environment.")
(arguments string :repeating t
:help "Variable assignments, commands and command arguments."))
"Modify the command environment. If ignore-environment"
(if (and (not shell-command) (not arguments))
;; Just print variables
(loop :for v :in variable-assignment
:do
(let ((var (if (position #\= v)
(first (split-sequence #\= v))
v)))
(when var
(format t "~a=~a~%" var (nos:environment-variable var)))))
;; Set variables and execute command
(progn
(loop :for v :in variable-assignment
:do
(let ((pos (position #\= v))
var val seq)
(if pos
(setf seq (split-sequence #\= v)
var (first seq)
val (third seq))
(setf var v))
(when (and var val)
(setf (nos:environment-variable var) val))))
(apply #'do-system-command
`(,`(,shell-command ,@arguments)
,(if ignore-environment
(modified-context *context* :environment nil)
*context*))))))
But instead we have to to a kludgey version:
|#
;; Another problem is, it's rather counter-intuitive to POSIX shell users
;; (including me), that, say,
;;
;; env "CFLAGS=-g -Wall -O2" ./configure --enable-impossible-color ...
;;
;; works, but
;;
;; env CFLAGS="-g -Wall -O2" ./configure --enable-impossible-color ...
;;
;; doesn't work.
;; It doesn't work because the shell reader splits words at double quotes,
;; so it becomes "env" "CFLAGS" "=-g -Wall -O2". The reader splits words like
;; that for what I think is good reason, so I'm not sure I want to change it.
(defbuiltin env
((ignore-environment boolean :short-arg #\i
:help "Ignore the environment.")
(arguments string :rest t
:help "Variable assignments, command and command arguments.
Any number of leading words with equal signs '=' are taken to be environment
variable assignments. The rest of the arguments are taken to be a command with
and it's arguments. The environment variable assignments are only in effect for
the command."))
"Modify the command environment. If ignore-environment is true, only
variables explicitly set in arguments are passed to the command."
(if arguments
;; Set variables and execute command
(let (env new-env cmd (a arguments) args)
;; Accumulate environment modifications in env
(loop
:while (and a (position #\= (car a)))
:do
(let* ((seq (split-sequence #\= (car a)))
(var (first seq))
(val (second seq)))
(when var
;; (format t "push env var=~s val=~s~%" var val)
(push (cons var val) env)))
(setf a (cdr a)))
;; Set the command and arguments
(setf cmd (car a)
args (cdr a))
(when (not ignore-environment)
(setf new-env (or (context-environment *context*)
(environment))))
;; Set the variables
;; (format t "cmd = ~s~%args = ~s~%env = ~s~%" cmd args env)
;; (finish-output)
(loop :with e
:for (var . val) :in env
:do
(setf e (assoc (intern var :keyword) new-env))
(if e
(rplacd e val)
(setf new-env (acons (intern var :keyword) val new-env))))
;; Unixify the new-env
;;(setf new-env
;; (mapcar (_ (format nil "~a=~a" (car _) (cdr _))) new-env))
;; Run the command
;; @@@ This should respect piping!!!
(when cmd
;; (funcall #'do-system-command
;; (make-shell-expr :words `(,cmd ,@args))
;; (modified-context *context* :environment new-env))))
(funcall #'shell-eval
;; (make-shell-expr :words `(,cmd ,@args))
(shell-read (join-by-string (append (list cmd) args) " "))
:context (modified-context *context* :environment new-env))))
;; Just print the variables
(loop :for e :in (environment)
:do (format t "~a=~a~%" (car e) (cdr e)))))
(defun get-cols ()
;; (let ((tty (rl:line-editor-terminal (lish-editor *shell*))))
;; (terminal-get-size tty)
;; (terminal-window-columns tty))
(or (and *terminal* (tt-width)) 80))
(define-builtin-arg-type signal (arg-integer)
"A system signal."
()
:convert string
(or #+unix (position value *signal-names* :test #'equalp)
#+windows (cadr (assoc value *siggy* :test #'equalp))
(ignore-errors (parse-integer value))
value))
(defun pseudo-kill (sig job-or-pid)
"Kill a job or an OS process ID."
(labels ((kill-pid (p)
#+unix (os-unix:kill p (or sig uos:+SIGTERM+))
#+windows (funcall (caddr (find sig *siggy* :key #'second)) p)))
(cond
((stringp job-or-pid)
(let ((job (find-job job-or-pid)) pid-int)
(cond
((job-p job)
(kill-job job :signal sig))
(t
(when (setf pid-int (ignore-errors (parse-integer job-or-pid)))
(kill-pid pid-int))))))
((job-p job-or-pid)
(kill-job job-or-pid :signal sig))
((integerp job-or-pid)
(kill-pid job-or-pid)))))
(defun pid-or-job-list ()
"Return a list of jobs and process IDs."
`(,@(job-id-list) ,@(mapcar #'nos:os-process-id (nos:process-list))))
;; (defclass arg-pid-or-job (arg-lenient-choice)
;; ()
;; (:default-initargs
;; :choice-func #'pid-or-job-list)
;; (:documentation "A job descriptor or process ID."))
(define-builtin-arg-type pid-or-job (arg-lenient-choice)
"A process ID or a job."
()
(:default-initargs
:choice-func #'pid-or-job-list))
(defun rl-yes-or-no-p (&optional (format "") &rest args)
"Like yes-or-no-p but using RL."
(loop :with line
:do
(tt-finish-output) (finish-output)
(setf line (rl:rl
:prompt (s+ (apply #'format nil format args) " (yes or no) ")))
(cond
((equal (trim line) "yes") (return t))
((equal (trim line) "no") (return nil))
(t
(tt-write-line "Please type \"yes\" for yes or \"no\" for no.")
(tt-finish-output)))))
(defbuiltin kill
((list-signals boolean :short-arg #\l :help "List available signals.")
(interactive boolean :short-arg #\i :help "Kill jobs interactively.")
(signal signal :default "TERM" :help "Signal number to send.")
(pids pid-or-job :repeating t :help "Process IDs to signal."))
"Sends SIGNAL to PID."
(let (job)
(cond
(list-signals
(format t (s+ "~{~<~%~1," (get-cols) ":;~a~> ~}~%") ; bogus, but v fails
;; (loop :for i :from 1 :below nos:*signal-count*
;; :collect (format nil "~2d) ~:@(~8a~)" i (nos:signal-name i))))
(loop :for s :in *siggy*
:collect (format nil "~2d) ~:@(~8a~)" (second s) (first s)))))
(interactive
;; I think maybe it's too dangerous to take pids from *input* when
;; not interactive.
(when (and (not pids) *input*)
(setf pids (if (consp *input*) *input* (list *input*))))
(if (not pids)
(error "But what processes to kill?")
(loop :for p :in pids
:when (rl-yes-or-no-p "Kill process ~s" p)
:do (pseudo-kill signal p))))
(t
(cond