commander-s/scheme/cmdline.scm

410 lines
11 KiB
Scheme

;; ,open conditions extended-ports define-record-types srfi-1 srfi-6 srfi-8 srfi-13 srfi-14 silly signals handle
;; command line language:
;;
;; <cmdline> ::= (<cmd> (<sep> <cmd>)* <jobctrl>?
;; <cmd> ::= <prog> <arg>* <redir>*
;; <redir> ::= <redirop> <fname>
;; <redirop> ::= ">" | ">>" | "<<" | "<"
;; <fname> ::= <string> | <unquote>
;; <prog> ::= <string> | <unquote>
;; <string> ::= "..." | char+ with char = ASCII\{"&","|","<",">",","}
;; <jobctrl> ::= "&" | "&*"
;; <sep> ::= "|" | "&&" | "||" | ";"
;; <unquote> ::= "," <s-expr> | ",@" <s-expr>
;; lexer stuff
(define-record-type token :token
(really-make-token token type start-pos cursor-pos)
token?
(token token-token)
(type token-type)
(start-pos token-start-pos)
(cursor-pos token-cursor-pos))
;; all indices and positions are zero-based
(define-record-discloser :token
(lambda (r)
`(token ,(token-type r) ,(token-token r) ,(token-cursor-pos r))))
(define (make-token token type start-pos cursor-index port)
(really-make-token
token type start-pos
(and cursor-index
(<= start-pos cursor-index (current-column port))
(- cursor-index start-pos))))
(define (make-string-token data start-pos cursor-index port)
(make-token data 'string start-pos cursor-index port))
(define (string-token? thing)
(and (token? thing) (eq? (token-type thing) 'string)))
(define (make-s-expr-token data start-pos cursor-index port)
(make-token data 's-expr start-pos cursor-index port))
(define (s-expr-token? thing)
(and (token? thing) (eq? (token-type thing) 's-expr)))
(define (make-operator-token data start-pos cursor-index port)
(make-token data 'operator start-pos cursor-index port))
(define (operator-token? thing)
(and (token? thing) (eq? (token-type thing) 'operator)))
(define (cursor-on-token? token)
(or (to-complete? token)
(integer? (token-cursor-pos token))))
(define-record-type to-complete :to-complete
(make-to-complete prefix)
to-complete?
(prefix to-complete-prefix))
(define-record-discloser :to-complete
(lambda (r)
`(to-complete ,(to-complete-prefix r))))
(define to-complete-without-prefix?
to-complete-prefix)
(define (make-empty-to-complete)
(make-to-complete #f))
(define operator-chars (char-set #\& #\| #\< #\> #\, #\;))
(define composed-operator-derivations
(char-set #\& #\* #\| #\> #\<))
(define (valid-operator? str)
(member str '("&" "&&" "&*" "|" "||" "<" "<<" ">" ">>" "," ",@" ";")))
(define token-terminating-chars
(char-set-union operator-chars char-set:whitespace))
(define (lex-token cursor-index port)
(let ((start-pos (current-column port)))
(let lp ((c (peek-char port)) (chars '()))
(cond
((or (eof-object? c)
(char-set-contains? token-terminating-chars c))
(make-string-token (reverse-list->string chars (length chars))
start-pos cursor-index port))
(else
(read-char port)
(lp (peek-char port) (cons c chars)))))))
(define (lex-operator cursor-index port)
(let ((start-pos (current-column port))
(c-1 (read-char port))
(c-2 (peek-char port)))
(if (eof-object? c-2)
(make-operator-token (string->symbol (string c-1))
start-pos cursor-index port)
(let ((c (if (char-set-contains? composed-operator-derivations c-2)
(begin
(read-char port)
(string-append (string c-1) (string c-2)))
(string c-1))))
(cond
((valid-operator? c)
(make-operator-token (string->symbol c)
start-pos cursor-index port))
(else
(error "Invalid operator in command line" c start-pos)))))))
(define (lex-port cursor-index port)
(let lp ((c (peek-char port)) (tokens '()))
(display (list c (current-column port)))
(newline)
(cond
((eof-object? c)
(reverse tokens))
((or (char=? c #\") (char=? c #\,))
(let* ((start-pos (current-column port))
(s-expr (read port))
(token
(make-s-expr-token
s-expr start-pos cursor-index port)))
(lp (peek-char port) (cons token tokens))))
((char-set-contains? operator-chars c)
(let ((token (lex-operator cursor-index port)))
(lp (peek-char port) (cons token tokens))))
((char-set-contains? char-set:whitespace c)
(let ((pos (current-column port)))
(read-char port)
(lp (peek-char port)
(if (and cursor-index (= cursor-index pos))
(cons (make-empty-to-complete) tokens)
tokens))))
(else
(let ((token (lex-token cursor-index port)))
(lp (peek-char port) (cons token tokens)))))))
(define (lex-command-line cmd-line . args)
(let-optionals args
((cursor-index #f))
(lex-port
cursor-index
(make-tracking-input-port
(make-string-input-port cmd-line)))))
;; abstract syntax
(define-record-type command-line :command-line
(make-command-line first-cmd combinator/cmds job-ctrl)
command-line?
(first-cmd command-line-first-cmd)
(combinator/cmds command-line-combinator/cmds)
(job-ctrl command-line-job-ctrl))
(define-record-discloser :command-line
(lambda (r)
`(command-line ,(command-line-first-cmd r)
,(command-line-combinator/cmds r)
,(command-line-job-ctrl r))))
(define-record-type command :command
(make-command executable args redirections)
command?
(executable command-executable)
(args command-args)
(redirections command-redirections))
(define-record-discloser :command
(lambda (r)
`(command ,(command-executable r)
,(command-args r) ,(command-redirections r))))
(define-record-type redirection :redirection
(make-redirection op dest)
redirection?
(op redirection-op)
(dest redirection-dest))
(define-record-discloser :redirection
(lambda (r)
`(redir ,(redirection-op r) ,(redirection-dest r))))
;; condition raised during parsing
(define-condition-type 'parser-error '(error))
(define parser-error? (condition-predicate 'parser-error))
(define-condition-type 'parser-syntax-error '(parser-error))
(define parser-syntax-error?
(condition-predicate 'parser-syntax-error))
(define-condition-type 'parser-unexpected-eof '(parser-error))
(define parser-unexpected-eof?
(condition-predicate 'parser-unexpected-eof))
(define (signal-syntax-error info)
(signal 'parser-syntax-error info))
(define (signal-unexpected-eof info)
(signal 'parser-unexpected-eof info))
;; a simple recursive descent parser
(define (extract-token t)
(cond
((to-complete? t)
t)
((cursor-on-token? t)
(make-to-complete (token-token t)))
(else
(token-token t))))
(define (string-or-s-expr? t)
(or (string-token? t)
(s-expr-token? t)
(to-complete? t)))
(define (parse-string/s-expr tokens)
(if (null? tokens)
(signal-unexpected-eof 'parse-string/s-expr)
(let ((t (car tokens)))
(if (or (string-token? t) (s-expr-token? t))
(values (extract-token t) (cdr tokens))
(signal-syntax-error `(parse-string/s-expr ,t ,tokens))))))
(define (parse-many-satisfying predicate tokens)
(let lp ((tokens tokens) (found '()))
(if (or (null? tokens) (not (predicate (car tokens))))
(values (reverse found) tokens)
(lp (cdr tokens) (cons (car tokens) found)))))
(define (parse-redirection tokens)
(if (< (length tokens) 2)
(values #f tokens)
(let ((op (car tokens))
(obj (cadr tokens)))
(cond
((and (operator-token? op)
(member (token-token op) '(> < >>))
(string-or-s-expr? obj))
(values
(make-redirection (token-token op) (extract-token obj))
(cddr tokens)))
((and (operator-token? op)
(eq? (token-token op) '<<)
(s-expr-token? obj))
(values
(make-redirection (token-token op) (extract-token obj))
(cddr tokens)))
(else
(values #f tokens))))))
(define (parse-redirections tokens)
(let lp ((tokens tokens) (redirs '()))
(receive (redir tokens)
(parse-redirection tokens)
(if redir
(lp tokens (cons redir redirs))
(values (reverse redirs) tokens)))))
(define (parse-command tokens)
(receive (executable tokens)
(parse-string/s-expr tokens)
(receive (args tokens)
(parse-many-satisfying string-or-s-expr? tokens)
(receive (redirs tokens)
(parse-redirections tokens)
(values (make-command executable
(map extract-token args) redirs)
tokens)))))
(define combinator-tokens
(map string->symbol '(";" "|" "&&" "||")))
(define (combinator-token? token)
(and (operator-token? token)
(member (token-token token) combinator-tokens)))
(define (parse-combinator/command tokens)
(if (< (length tokens) 2)
(values #f tokens)
(let ((comb (car tokens))
(cmd (cdr tokens)))
(cond
((combinator-token? comb)
(receive (command tokens)
(parse-command cmd)
(values (cons (extract-token comb) command)
tokens)))
(else
(values #f tokens))))))
(define (parse-combinators/commands-list tokens)
(let lp ((tokens tokens) (combs/cmds '()))
(receive (comb/cmd tokens)
(parse-combinator/command tokens)
(if comb/cmd
(lp tokens (cons comb/cmd combs/cmds))
(values (reverse combs/cmds) tokens)))))
(define (parse-job-control tokens)
(if (null? tokens)
(values #f tokens)
(let ((t (car tokens)))
(if (and (operator-token? t)
(member (token-token t) '(&& &)))
(values (token-token t) (cdr tokens))
(values #f tokens)))))
(define (parse-command-line tokens)
(receive (command tokens)
(parse-command tokens)
(receive (combinator/command-list tokens)
(parse-combinators/commands-list tokens)
(receive (job-control tokens)
(parse-job-control tokens)
(if (null? tokens)
(make-command-line command combinator/command-list
job-control)
(signal-syntax-error `(parse-command-line ,tokens)))))))
;; for use with tab completion
(define (parse-command-line-carefully tokens)
(call-with-current-continuation
(lambda (esc)
(with-handler
(lambda (condition . more)
(if (parser-error? condition)
(esc #f)
(more)))
(lambda ()
(parse-command-line tokens))))))
(define (lex/parse-partial-command-line cmdln cursor-index)
(let ((tokens (lex-command-line cmdln cursor-index)))
(let lp ((tokens tokens))
(cond
((null? tokens)
#f)
((parse-command-line-carefully tokens)
=> (lambda (v) v))
(else
(lp (drop-right tokens 1)))))))
;; unparser
(define append*
(lambda args
(apply append
(map (lambda (arg)
(if (pair? arg)
arg
(list arg)))
(filter (lambda (v) (not (null? v)))
args)))))
(define string-join*
(lambda args
(string-join (apply append* args))))
(define (unparse-arbitrary v)
(let ((p (open-output-string)))
(display v p)
(get-output-string p)))
(define (unparse-string/s-expr v)
(cond
((string? v) v)
((and (pair? v) (eq? (car v) 'unquote))
(string-append
"," (unparse-arbitrary (cadr v))))
((and (pair? v) (eq? (car v) 'unquote-splicing))
(string-append
",@" (unparse-arbitrary (cadr v))))
(else
(unparse-arbitrary v))))
(define (unparse-redirection rd)
(string-join*
(symbol->string (redirection-op rd))
(unparse-string/s-expr (redirection-dest rd))))
(define (unparse-command cmd)
(string-join*
(unparse-string/s-expr (command-executable cmd))
(map unparse-string/s-expr (command-args cmd))
(map unparse-redirection
(command-redirections cmd))))
(define (unparse-command-line cmdln)
(string-join*
(unparse-command (command-line-first-cmd cmdln))
(map (lambda (comb.cmd)
(string-append
(symbol->string (car comb.cmd))
" "
(unparse-command (cdr comb.cmd))))
(command-line-combinator/cmds cmdln))
(cond ((command-line-job-ctrl cmdln)
=> symbol->string)
(else '()))))