Add pipeline semantics

* geesh/shell.scm (swap-and-shift-pairs): New function.
(make-pipes): New function.
(plumb): New function.
(sh:pipeline): New public function.
* tests/shell.scm: Test it.
This commit is contained in:
Timothy Sample 2018-10-16 13:48:52 -04:00
parent 553b8f2b96
commit 37f4ce6ea8
2 changed files with 133 additions and 0 deletions

View File

@ -7,6 +7,7 @@
#:use-module (srfi srfi-26)
#:export (sh:exec-let
sh:exec
sh:pipeline
sh:subshell
sh:substitute-command
sh:with-redirects))
@ -211,3 +212,58 @@ a string."
((pid . status) (waitpid pid)))
(set-var! env "?" (number->string (status:exit-val status)))
result)))
;;; Pipelines.
(define (swap-and-shift-pairs pairs)
"Swap and shift @var{pairs} over by one. For example, the list
@code{((a . b) (c . d))} becomes @code{((#f . b) (a . d) (c . #f))}"
(let ((kons (lambda (pair acc)
(match-let (((a . b) pair))
(match acc
((head . rest) `(,b (,a . ,head) ,@rest))
(() `(,b (,a . #f))))))))
(match (fold-right kons '() pairs)
((head . rest) `((#f . ,head) ,@rest))
(() '()))))
(define (make-pipes xs)
"Cons each element of @var{xs} to a pair of ports such that the first
port is an input port connected to the second port of the previous
element's pair, and the second port is an output port connected to the
first port of next element's pair. The first pair will have @code{#f}
for an input port and the last will have @code{#f} as an output port."
(match xs
(() '())
((x) `((,x . (#f . #f))))
(_ (let ((pipes (map (lambda (x) (pipe)) (cdr xs))))
(map cons xs (swap-and-shift-pairs pipes))))))
(define (plumb env in out thunk)
"Run @var{thunk} in a new process with @code{current-input-port} set
to @var{in} and @code{current-output-port} set to @var{out}. If
@var{in} or @var{out} is @code{#f}, the corresponding ``current'' port
is left unchanged."
(let* ((thunk* (lambda ()
(let ((in (or in (current-input-port)))
(out (or out (current-output-port))))
(parameterize ((current-input-port in)
(current-output-port out))
(thunk)))))
(pid (%subshell thunk*)))
(when in (close-port in))
(when out (close-port out))
pid))
(define (sh:pipeline env . thunks)
"Run each thunk in @var{thunks} in its own process with the output
of each thunk sent to the input of the next thunk."
(let ((pids (map (match-lambda
((thunk . (source . sink))
(plumb env source sink thunk)))
(make-pipes thunks))))
(unless (null? pids)
(match-let* ((pid (last pids))
((pid . status) (waitpid pid)))
(set-var! env "?" (number->string (status:exit-val status)))))))

View File

@ -442,4 +442,81 @@
(let ((env (make-environment '())))
(sh:substitute-command env noop)))
;; Pipelines.
(test-equal "Built-ins are connected by pipelines"
"foo"
(call-with-temporary-directory
(lambda (directory)
(let ((foo (string-append directory "/foo.txt"))
(env (make-environment '())))
(sh:pipeline env
(lambda ()
(display "foo\n"))
(lambda ()
(with-output-to-file foo
(lambda ()
(display (get-line (current-input-port)))))))
(call-with-input-file foo get-string-all)))))
(test-equal "External utilities are connected by pipelines"
"foo"
(call-with-temporary-directory
(lambda (directory)
(let ((utility1 (string-append directory "utility1"))
(utility2 (string-append directory "utility2"))
(foo (string-append directory "/foo.txt"))
(env (make-environment '())))
(make-script utility1
(display "foo\n"))
(make-script utility2
(use-modules (ice-9 textual-ports))
(with-output-to-file ,foo
(lambda ()
(display (get-line (current-input-port))))))
(sh:pipeline env
(lambda ()
(sh:exec env utility1))
(lambda ()
(sh:exec env utility2)))
(call-with-input-file foo get-string-all)))))
(test-equal "Externals and built-ins are connected by pipelines"
"foo"
(call-with-temporary-directory
(lambda (directory)
(let ((utility (string-append directory "/utility"))
(foo (string-append directory "/foo.txt"))
(env (make-environment '())))
(make-script utility
(display "foo\n"))
(sh:pipeline env
(lambda ()
(sh:exec env utility))
(lambda ()
(with-output-to-file foo
(lambda ()
(display (get-line (current-input-port)))))))
(call-with-input-file foo get-string-all)))))
(test-equal "Built-ins and externals are connected by pipelines"
"foo"
(call-with-temporary-directory
(lambda (directory)
(let ((utility (string-append directory "/utility"))
(foo (string-append directory "/foo.txt"))
(env (make-environment '())))
(make-script utility
(use-modules (ice-9 textual-ports))
(with-output-to-file ,foo
(lambda ()
(display (get-line (current-input-port))))))
(sh:pipeline env
(lambda ()
(display "foo\n"))
(lambda ()
(sh:exec env utility)))
(call-with-input-file foo get-string-all)))))
(test-end)