411 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			Scheme
		
	
	
	
			
		
		
	
	
			411 lines
		
	
	
		
			12 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 pos)
 | 
						|
  to-complete?
 | 
						|
  (prefix to-complete-prefix)
 | 
						|
  (pos to-complete-pos))
 | 
						|
 | 
						|
(define-record-discloser :to-complete
 | 
						|
  (lambda (r)
 | 
						|
    `(to-complete ,(to-complete-prefix r) ,(to-complete-pos r))))
 | 
						|
 | 
						|
(define to-complete-without-prefix?
 | 
						|
  to-complete-prefix)
 | 
						|
 | 
						|
(define (make-empty-to-complete pos)
 | 
						|
  (make-to-complete #f pos))
 | 
						|
 | 
						|
(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 '()))
 | 
						|
    (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 pos) 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) (token-start-pos 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)
 | 
						|
	(make-command-line 
 | 
						|
	 (make-command (make-to-complete #f 0) '() '())
 | 
						|
	 '() #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 '()))))
 |