gash/sh/anguish.scm

265 lines
11 KiB
Scheme
Raw Normal View History

2016-06-06 23:54:23 +01:00
(define-module (sh anguish)
:use-module (statprof)
2016-09-17 20:30:34 +01:00
:use-module (srfi srfi-1)
: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
:use-module (sh pipe)
:use-module (sh peg)
2016-06-06 23:54:23 +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)
((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)
(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
"))
(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.
"))
((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"))
(define (expand identifier o) ;;identifier-string -> symbol
2016-11-01 13:40:17 +00:00
(define (expand- o)
(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 22:09:58 +01:00
2016-11-01 10:25:36 +00:00
;; TODO: add braces
2016-10-10 22:09:58 +01:00
(define (glob pattern) ;; pattern -> list of path
(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
(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)))
(map (lambda (extension) (if empty? extension (string-join (list path "/" extension) "")))
(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))
(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
(define (builtin ast)
(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))
(('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))))
(('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)))
(('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))
((_ 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)
(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
(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)
(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)
(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)))