gash/sh/anguish.scm

254 lines
10 KiB
Scheme
Raw Normal View History

2016-06-06 23:54:23 +01:00
(define-module (sh anguish)
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)
2016-10-18 08:15:53 +01:00
(job-control-init)
2016-09-19 11:37:38 +01:00
(let* ((option-spec '((debug (single-char #\d) (value #f))
(help (single-char #\h) (value #f))
2016-06-06 23:54:23 +01:00
(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 '() '()))
2016-09-17 20:30:34 +01:00
(run (lambda (ast) (and ast
(cond (parse?
(let ((ast- (transform ast)))
2016-09-21 23:21:28 +01:00
(format (current-output-port) "parsed : ~s\n\n" ast)
2016-10-30 23:19:44 +00:00
(format (current-output-port) "prepared : ~s\n\n" ast-)
2016-09-17 20:30:34 +01:00
#t))
(#t
2016-10-18 08:15:53 +01:00
(sh-exec ast)))))))
2016-06-06 23:54:23 +01:00
(cond
(help?
(display "\
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 "
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)
2016-09-17 20:30:34 +01:00
(let* ((asts (map file-to-ast files))
(status (map run asts)))
(quit (every identity status))))
2016-06-06 23:54:23 +01:00
(#t (let* ((HOME (string-append (getenv "HOME") "/.anguishistory"))
(thunk (lambda ()
(let loop ((line (readline (prompt))))
(if (not (eof-object? line))
(begin
2016-09-17 18:31:58 +01:00
(let ((ast (string-to-ast line)))
2016-10-29 14:30:39 +01:00
(if (not (string-null? line))
(add-history line))
2016-06-06 23:54:23 +01:00
(run ast))
(loop (readline (prompt)))))))))
(clear-history)
(read-history HOME)
(with-readline-completion-function completion thunk)
(write-history HOME))
(newline)))))
(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
(define (foo o)
(let ((dollar-identifier (string-append "$" identifier)))
(match o
((? symbol?) o)
((? string?) (if (string=? o dollar-identifier) (string->symbol identifier) o))
((? list?) (map foo o)))))
(map foo 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)
(let* ((pattern (regexp-substitute/global #f "\\." pattern 'pre "\\." 'post))
(pattern (regexp-substitute/global #f "\\?" pattern 'pre "." 'post))
(pattern (regexp-substitute/global #f "\\*" pattern 'pre ".*" 'post)))
(make-regexp (string-append pattern "$"))))
(define (glob-match pattern path) ;; pattern path -> bool
(regexp-match? (regexp-exec (glob2regex pattern) path)))
(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 pattern <>)
2016-10-19 00:23:03 +01:00
(filter (negate (cute 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-10-30 23:19:44 +00:00
(let* (;(print (format (current-error-port) "parsed: ~s\n" ast))
2016-10-13 22:40:44 +01:00
(ast (transform ast))
2016-10-30 23:19:44 +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
;;TODO add colors
2016-06-06 23:54:23 +01:00
(define (prompt)
(let* ((esc (string #\033))
(CWD (getcwd))
(HOME (getenv "HOME"))
(cwd (if (string-prefix? HOME CWD)
(string-replace CWD "~" 0 (string-length HOME))
CWD)))
2016-10-29 14:30:39 +01:00
(report-jobs)
(string-append cwd "$ ")))
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-10-19 13:26:06 +01:00
(let ((completions (filter (cute string-prefix? text <>)
(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-10-19 13:26:06 +01:00
(let ((completions (filter (cute string-prefix? text <>)
(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)))