gash/sh/anguish.scm

305 lines
12 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 rdelim)
:use-module (ice-9 readline)
:use-module (ice-9 buffered-input)
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-11-02 20:53:08 +00:00
(setenv "PATH" "/bin:/usr/bin:/sbin:/usr/sbin:.")
(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))))
(when (not (eof-object? line))
(let ((ast (string-to-ast line)))
(when ast
(if (not (string-null? line))
(add-history line))
(run ast))
(loop (let ((previous (if ast "" (string-append line "\n")))
(next (readline (if ast (prompt) "> "))))
(if (eof-object? next) next
(string-append previous next))))))))))
(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
2016-11-02 23:39:18 +00:00
(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 "$"))))
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)
2016-11-02 23:39:18 +00:00
(map (lambda (path)
(if (string-prefix? "./" path) (string-drop path 2) path))
(append-map (lambda (path)
(map (cute string-append (if (string=? "/" path) "" path) "/" <>)
(filter (conjoin (negate (cut string-prefix? "." <>))
(cute glob-match (glob2regex pattern) <>))
(or (scandir path) '()))))
paths)))
2016-10-10 22:09:58 +01:00
(if (glob? pattern)
2016-11-02 23:39:18 +00:00
(let* ((absolute? (string-prefix? "/" pattern)))
(let loop ((patterns (filter (negate string-null?) (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
2016-11-02 20:53:08 +00:00
(define (string-prefix s1 s2)
(substring/read-only s1 0 (string-prefix-length s1 s2)))
(define next->file-completion (lambda () #f))
(define next->binary-completion (lambda () #f))
(define (isdir? path)
(and (access? path F_OK) (eq? 'directory (stat:type (stat path)))))
(define (ls dir)
(map (lambda (path)
(if (isdir? (string-append dir path))
(string-append path "/")
path))
(sort (filter (negate (cut string-every #\. <>))
(scandir (if (string-null? dir) (getcwd) dir))) string<?)))
(define (complete prefix list)
(if (string-null? prefix) list
(filter (cut string-prefix? prefix <>) list)))
(define (slash dir)
(if (string-suffix? "/" dir) dir
(string-append dir "/")))
(define (after-slash path)
(let ((at (string-index-right path #\/)))
(if at (string-drop path (+ 1 at))
path)))
(define (filename-completion text continue?)
(if continue?
(next->file-completion)
(let* ((dir (slash (if (isdir? text) text (dirname text))))
(listing (ls dir))
(dir (if (string=? "./" dir) "" dir))
(completions (complete (after-slash text) listing)))
(set! next->file-completion
(lambda ()
(if (null? completions)
#f
(let ((completion (car completions)))
(set! completions (cdr completions))
(string-append dir completion)))))
(next->file-completion))))
(define (search-binary-in-path-completion text continue?)
(if (not continue?)
(let* ((paths (string-split (getenv "PATH") #\:))
(binaries (apply append (filter identity (map scandir paths))))
(completions (sort (filter (cut string-prefix? text <>) binaries) string<?)))
(set! next->binary-completion (lambda ()
(if (null? completions)
#f
(let ((completion (car completions)))
(set! completions (cdr completions))
completion))))
(next->binary-completion))
(next->binary-completion)))
(define (completion text continue?)
(or (filename-completion text continue?) (search-binary-in-path-completion text continue?)))