2016-06-06 23:54:23 +01:00
|
|
|
(define-module (sh anguish)
|
2016-11-01 21:33:08 +00:00
|
|
|
:use-module (statprof)
|
|
|
|
|
2016-09-17 20:30:34 +01:00
|
|
|
:use-module (srfi srfi-1)
|
2016-10-10 08:55:38 +01:00
|
|
|
:use-module (srfi srfi-26)
|
2016-10-10 22:09:58 +01:00
|
|
|
:use-module (ice-9 ftw)
|
2016-06-06 23:54:23 +01:00
|
|
|
:use-module (ice-9 getopt-long)
|
2016-10-10 22:09:58 +01:00
|
|
|
:use-module (ice-9 local-eval)
|
2016-06-06 23:54:23 +01:00
|
|
|
:use-module (ice-9 match)
|
|
|
|
:use-module (ice-9 pretty-print)
|
|
|
|
:use-module (ice-9 rdelim)
|
|
|
|
:use-module (ice-9 readline)
|
2016-10-10 22:09:58 +01:00
|
|
|
:use-module (ice-9 regex)
|
2016-06-06 23:54:23 +01:00
|
|
|
|
2016-10-10 08:55:38 +01:00
|
|
|
:use-module (sh pipe)
|
|
|
|
:use-module (sh peg)
|
2016-06-06 23:54:23 +01:00
|
|
|
|
2016-10-10 08:55:38 +01:00
|
|
|
:export (main))
|
2016-06-06 23:54:23 +01:00
|
|
|
|
2016-10-09 20:21:08 +01:00
|
|
|
(define (stdout . o)
|
|
|
|
(map (lambda (o) (display o (current-output-port))) o)
|
2016-10-30 23:19:44 +00:00
|
|
|
(newline (current-output-port))
|
|
|
|
(force-output (current-output-port))
|
2016-10-09 20:21:08 +01:00
|
|
|
o)
|
|
|
|
|
|
|
|
(define (stderr . o)
|
|
|
|
(map (lambda (o) (display o (current-error-port))) o)
|
|
|
|
(newline)
|
|
|
|
o)
|
|
|
|
|
2016-09-17 18:31:58 +01:00
|
|
|
(define (file-to-string filename)
|
|
|
|
((compose read-string open-input-file) filename))
|
|
|
|
|
|
|
|
(define (string-to-ast string)
|
2016-10-10 08:55:38 +01:00
|
|
|
((compose parse remove-shell-comments) string))
|
2016-09-17 18:31:58 +01:00
|
|
|
|
|
|
|
(define (file-to-ast filename)
|
|
|
|
((compose string-to-ast file-to-string) filename))
|
|
|
|
|
2016-06-06 23:54:23 +01:00
|
|
|
(define (main args)
|
2016-11-01 21:33:08 +00:00
|
|
|
(let ((thunk (lambda ()
|
|
|
|
(job-control-init)
|
|
|
|
(let* ((option-spec '((debug (single-char #\d) (value #f))
|
|
|
|
(help (single-char #\h) (value #f))
|
|
|
|
(parse (single-char #\p) (value #f))
|
|
|
|
(version (single-char #\v) (value #f))))
|
|
|
|
(options (getopt-long args option-spec
|
|
|
|
#:stop-at-first-non-option #t ))
|
|
|
|
(help? (option-ref options 'help #f))
|
|
|
|
(parse? (option-ref options 'parse (null? #f)))
|
|
|
|
(version? (option-ref options 'version #f))
|
|
|
|
(files (option-ref options '() '()))
|
|
|
|
(run (lambda (ast) (and ast
|
|
|
|
(cond (parse?
|
|
|
|
(let ((ast- (transform ast)))
|
|
|
|
(format (current-output-port) "parsed : ~s\n\n" ast)
|
|
|
|
(format (current-output-port) "prepared : ~s\n\n" ast-)
|
|
|
|
#t))
|
|
|
|
(#t
|
|
|
|
(sh-exec ast)))))))
|
|
|
|
(cond
|
|
|
|
(help?
|
|
|
|
(display "\
|
2016-06-06 23:54:23 +01:00
|
|
|
anguish [options]
|
|
|
|
-h, --help Display this help
|
|
|
|
-p, --parse Parse the shell script and print the parse tree
|
|
|
|
-v, --version Display the version
|
|
|
|
"))
|
2016-11-01 21:33:08 +00:00
|
|
|
(version?
|
|
|
|
(display "
|
2016-06-06 23:54:23 +01:00
|
|
|
Anguish 0.1
|
|
|
|
Copryright (C) 2016 R.E.W. van Beusekom, rutger.van.beusekom@gmail.com.
|
|
|
|
|
|
|
|
This is anguish, ANother GUIle SHell, or the feeling you might have
|
|
|
|
when your shell lacks a real programming language. Anguish is free
|
|
|
|
software and is covered by the GNU Public License, see COPYING for the
|
|
|
|
copyleft.
|
|
|
|
"))
|
2016-11-01 21:33:08 +00:00
|
|
|
((pair? files)
|
|
|
|
(let* ((asts (map file-to-ast files))
|
|
|
|
(status (map run asts)))
|
|
|
|
(quit (every identity status))))
|
|
|
|
(#t (let* ((HOME (string-append (getenv "HOME") "/.anguishistory"))
|
|
|
|
(thunk (lambda ()
|
|
|
|
(let loop ((line (readline (prompt))))
|
|
|
|
(if (not (eof-object? line))
|
|
|
|
(begin
|
|
|
|
(let ((ast (string-to-ast line)))
|
|
|
|
(if (not (string-null? line))
|
|
|
|
(add-history line))
|
|
|
|
(run ast))
|
|
|
|
(loop (readline (prompt)))))))))
|
|
|
|
(clear-history)
|
|
|
|
(read-history HOME)
|
|
|
|
(with-readline-completion-function completion thunk)
|
|
|
|
(write-history HOME))
|
|
|
|
(newline)))))))
|
|
|
|
;;(statprof thunk #:hz 100 #:count-calls? #t)
|
|
|
|
(thunk)))
|
2016-06-06 23:54:23 +01:00
|
|
|
|
|
|
|
(define (remove-shell-comments s)
|
|
|
|
(string-join (map
|
|
|
|
(lambda (s)
|
|
|
|
(let* ((n (string-index s #\#)))
|
|
|
|
(if n (string-pad-right s (string-length s) #\space 0 n)
|
|
|
|
s)))
|
|
|
|
(string-split s #\newline)) "\n"))
|
|
|
|
|
2016-10-10 08:55:38 +01:00
|
|
|
(define (expand identifier o) ;;identifier-string -> symbol
|
2016-11-01 13:40:17 +00:00
|
|
|
(define (expand- o)
|
2016-10-10 08:55:38 +01:00
|
|
|
(let ((dollar-identifier (string-append "$" identifier)))
|
|
|
|
(match o
|
|
|
|
((? symbol?) o)
|
|
|
|
((? string?) (if (string=? o dollar-identifier) (string->symbol identifier) o))
|
2016-11-01 13:40:17 +00:00
|
|
|
((? list?) (map expand- o))
|
|
|
|
(_ o))))
|
|
|
|
(map expand- o))
|
2016-10-10 08:55:38 +01:00
|
|
|
|
2016-10-10 22:09:58 +01:00
|
|
|
|
2016-11-01 10:25:36 +00:00
|
|
|
;; TODO: add braces
|
2016-10-11 09:44:09 +01:00
|
|
|
|
2016-10-10 22:09:58 +01:00
|
|
|
(define (glob pattern) ;; pattern -> list of path
|
2016-10-11 09:44:09 +01:00
|
|
|
(define (glob? pattern)
|
|
|
|
(string-match "\\?|\\*" pattern))
|
|
|
|
|
2016-10-10 22:09:58 +01:00
|
|
|
(define (glob2regex pattern)
|
2016-11-01 13:40:17 +00:00
|
|
|
(let* ((regex (regexp-substitute/global #f "\\." pattern 'pre "\\." 'post))
|
|
|
|
(regex (regexp-substitute/global #f "\\?" pattern 'pre "." 'post))
|
|
|
|
(regex (regexp-substitute/global #f "\\*" pattern 'pre ".*" 'post)))
|
|
|
|
(make-regexp (string-append "^" regex "$"))))
|
2016-10-10 22:09:58 +01:00
|
|
|
|
2016-11-01 21:33:08 +00:00
|
|
|
(define (glob-match regex path) ;; pattern path -> bool
|
|
|
|
(regexp-match? (regexp-exec regex path)))
|
2016-10-10 22:09:58 +01:00
|
|
|
|
|
|
|
(define (glob- pattern paths)
|
|
|
|
(append-map (lambda (path)
|
|
|
|
(let ((empty? (string=? "" path)))
|
2016-10-11 09:44:09 +01:00
|
|
|
(map (lambda (extension) (if empty? extension (string-join (list path "/" extension) "")))
|
2016-11-01 21:33:08 +00:00
|
|
|
(filter (cute glob-match (glob2regex pattern) <>)
|
|
|
|
(filter (negate (cut string-any #\. <> 0 1)) (scandir (if empty? (getcwd) path)))))))
|
2016-10-10 22:09:58 +01:00
|
|
|
paths))
|
|
|
|
|
2016-10-11 09:44:09 +01:00
|
|
|
(if (glob? pattern)
|
|
|
|
(let ((absolute? (char=? #\/ (string-ref pattern 0))))
|
|
|
|
(let loop ((patterns (string-split pattern #\/))
|
|
|
|
(paths (if absolute? '("/") `(""))))
|
|
|
|
(if (null? patterns) paths
|
|
|
|
(loop (cdr patterns) (glob- (car patterns) paths)))))
|
|
|
|
(list pattern)))
|
2016-10-10 22:09:58 +01:00
|
|
|
|
2016-10-10 08:55:38 +01:00
|
|
|
|
2016-10-11 09:44:09 +01:00
|
|
|
(define (builtin ast)
|
2016-10-10 13:41:51 +01:00
|
|
|
(match ast
|
2016-10-18 23:01:02 +01:00
|
|
|
(('append ('glob "cd") arg) `(apply chdir ,arg))
|
2016-10-25 23:45:12 +01:00
|
|
|
(('append ('glob "fg") ('glob arg)) `(fg ,(string->number arg)))
|
2016-10-26 22:56:26 +01:00
|
|
|
(('append ('glob "bg") ('glob arg)) `(bg ,(string->number arg)))
|
2016-10-30 23:19:44 +00:00
|
|
|
(('append ('glob "echo") args ...) `(stdout (string-join ,@args " ")))
|
|
|
|
(('glob "echo") `(stdout))
|
2016-10-25 23:45:12 +01:00
|
|
|
(('glob "fg") `(fg 1))
|
2016-10-26 22:56:26 +01:00
|
|
|
(('glob "bg") `(bg 1))
|
2016-10-20 08:08:06 +01:00
|
|
|
(('glob "jobs") `(jobs))
|
2016-10-11 09:44:09 +01:00
|
|
|
(('for-each rest ...) ast)
|
|
|
|
(('if rest ...) ast)
|
|
|
|
(_ #f)))
|
|
|
|
|
2016-11-01 10:25:36 +00:00
|
|
|
(define (background ast)
|
|
|
|
(match ast
|
|
|
|
(('pipeline fg rest ...) `(pipeline #f ,@rest))
|
|
|
|
(_ ast)))
|
|
|
|
|
2016-10-30 23:19:44 +00:00
|
|
|
;; transform ast -> list of expr
|
|
|
|
;; such that (map eval expr)
|
|
|
|
|
2016-06-06 23:54:23 +01:00
|
|
|
(define (transform ast)
|
|
|
|
(match ast
|
2016-11-01 10:25:36 +00:00
|
|
|
(('script term "&") (list (background (transform term))))
|
2016-10-30 23:19:44 +00:00
|
|
|
(('script term) (list (transform term)))
|
|
|
|
(('script terms ...) (transform terms))
|
|
|
|
((('term command)) (list (transform command)))
|
|
|
|
((('term command) ...) (map transform command))
|
|
|
|
((('term command) (('term commands) ...)) (map transform (cons command commands)))
|
|
|
|
(('compound-list terms ...) (transform terms))
|
|
|
|
(('if-clause "if" (expression "then" consequent "fi")) `(if (equal? 0 (status:exit-val (begin ,@(transform expression)))) (begin ,@(transform consequent))))
|
|
|
|
(('if-clause "if" (expression "then" consequent ('else-part "else" alternative) "fi")) `(if (equal? 0 (status:exit-val ,@(transform expression))) (begin ,@(transform consequent)) (begin ,@(transform alternative))))
|
|
|
|
(('for-clause "for" ((identifier "in" lst sep) do-group)) `(for-each (lambda (,(string->symbol identifier)) (begin ,@(expand identifier (transform do-group)))) (glob ,(transform lst))))
|
2016-10-10 08:55:38 +01:00
|
|
|
(('do-group "do" (command "done")) (transform command))
|
2016-11-01 10:25:36 +00:00
|
|
|
(('pipeline command) (let* ((command (transform command))) (or (builtin command) `(pipeline #t ,command))))
|
|
|
|
(('pipeline command piped-commands) `(pipeline #t ,(transform command) ,@(transform piped-commands)))
|
2016-10-13 22:40:44 +01:00
|
|
|
(('simple-command ('word s)) `(glob ,(transform s)))
|
2016-10-11 09:44:09 +01:00
|
|
|
(('simple-command ('word s1) ('word s2)) `(append (glob ,(transform s1)) (glob ,(transform s2))))
|
|
|
|
(('simple-command ('word s1) (('word s2) ...)) `(append (glob ,(transform s1)) (append-map glob (list ,@(map transform s2)))))
|
2016-09-19 21:07:46 +01:00
|
|
|
(('literal s) (transform s))
|
|
|
|
(('singlequotes s) (string-concatenate (list "'" s "'")))
|
|
|
|
(('doublequotes s) (string-concatenate (list "\"" s "\"")))
|
|
|
|
(('backticks s) (string-concatenate (list "`" s "`")))
|
|
|
|
(('delim ('singlequotes s ...)) (string-concatenate (map transform s)))
|
|
|
|
(('delim ('doublequotes s ...)) (string-concatenate (map transform s)))
|
|
|
|
(('delim ('backticks s ...)) (string-concatenate (map transform s)))
|
2016-06-06 23:54:23 +01:00
|
|
|
((('pipe _) command ...) (map transform command))
|
|
|
|
(((('pipe _) command) ...) (map transform command))
|
2016-10-11 09:44:09 +01:00
|
|
|
((_ o) (transform o)) ;; peel the onion: (symbol (...)) -> (...)
|
2016-10-18 08:15:53 +01:00
|
|
|
(_ ast))) ;; done
|
2016-06-06 23:54:23 +01:00
|
|
|
|
2016-10-13 22:40:44 +01:00
|
|
|
(define (sh-exec ast)
|
2016-10-10 13:41:51 +01:00
|
|
|
(define (exec cmd)
|
|
|
|
(local-eval cmd (the-environment)))
|
2016-11-01 13:40:17 +00:00
|
|
|
(let* (;;(print (format (current-error-port) "parsed: ~s\n" ast))
|
2016-10-13 22:40:44 +01:00
|
|
|
(ast (transform ast))
|
2016-11-01 13:40:17 +00:00
|
|
|
;;(print (format (current-error-port) "transformed: ~s\n" ast))
|
2016-10-13 22:40:44 +01:00
|
|
|
)
|
|
|
|
(match ast
|
2016-10-18 08:15:53 +01:00
|
|
|
('script #t) ;; skip
|
|
|
|
(_ (begin (map exec ast) #t)))))
|
2016-10-13 22:40:44 +01:00
|
|
|
|
2016-06-06 23:54:23 +01:00
|
|
|
|
2016-11-01 21:33:08 +00:00
|
|
|
(define prompt
|
|
|
|
(let* ((l (string #\001))
|
|
|
|
(r (string #\002))
|
|
|
|
(e (string #\033))
|
|
|
|
(user (getenv "USER"))
|
|
|
|
(host (gethostname))
|
|
|
|
(home (getenv "HOME")))
|
|
|
|
(lambda ()
|
|
|
|
(let* ((cwd (getcwd))
|
|
|
|
(cwd (if (string-prefix? home cwd)
|
|
|
|
(string-replace cwd "~" 0 (string-length home))
|
|
|
|
cwd)))
|
|
|
|
(report-jobs)
|
|
|
|
(string-append
|
|
|
|
l e "[01;32m" r user "@" host l e "[00m" r ":"
|
|
|
|
l e "[01;34m" r cwd l e "[00m" r "$ ")))))
|
2016-06-06 23:54:23 +01:00
|
|
|
|
|
|
|
(define (redraw-current-line)
|
|
|
|
(dynamic-call (dynamic-func "rl_refresh_line"
|
|
|
|
(dynamic-link "libreadline.so"))
|
|
|
|
#f))
|
|
|
|
|
|
|
|
(define (filename-completion text state)
|
|
|
|
(if (not state)
|
2016-11-01 21:33:08 +00:00
|
|
|
(let ((completions (filter (cut string-prefix? text <>)
|
2016-10-19 13:26:06 +01:00
|
|
|
(scandir (getcwd)))))
|
2016-06-06 23:54:23 +01:00
|
|
|
(cond ((< 1 (length completions)) (begin (newline)
|
|
|
|
(display (string-join completions " ")) (newline)
|
|
|
|
(redraw-current-line)
|
|
|
|
#f))
|
|
|
|
((= 1 (length completions)) (car completions))
|
|
|
|
(#t #f)))
|
|
|
|
#f))
|
|
|
|
|
|
|
|
(define (search-binary-in-path-completion text state)
|
|
|
|
(if (not state)
|
2016-11-01 21:33:08 +00:00
|
|
|
(let ((completions (filter (cut string-prefix? text <>)
|
2016-10-19 13:26:06 +01:00
|
|
|
(scandir "/bin"))))
|
2016-06-06 23:54:23 +01:00
|
|
|
(cond ((< 1 (length completions)) (begin (newline)
|
|
|
|
(display (string-join completions " ")) (newline)
|
|
|
|
(redraw-current-line)
|
|
|
|
#f))
|
|
|
|
((= 1 (length completions)) (car completions))
|
|
|
|
(#t #f)))
|
|
|
|
#f))
|
|
|
|
|
|
|
|
(define (completion text state)
|
|
|
|
(or (filename-completion text state)
|
2016-10-19 13:26:06 +01:00
|
|
|
(search-binary-in-path-completion text state)))
|