123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290 |
- (library (shell)
- (export
- ;; shell logic
- shell
- call
- run-command
- with-ports
- command-pipeline
- echo-command)
- (import (except (rnrs base) error)
- (only (guile)
- lambda* λ
- ;; control flow
- when
- unless
- ;; ports
- current-input-port
- current-output-port
- current-error-port
- with-input-from-port
- with-output-to-port
- with-error-to-port
- with-input-from-string
- close-port
- pipe
- ;; other
- setvbuf
- eof-object?
- ;; string formatting
- simple-format
- ;; basic shell procedures guile provides
- getcwd
- chdir
- ;; other
- error
- ;; strings
- string-split
- )
- (ice-9 exceptions)
- ;; pipes
- (ice-9 popen)
- (ice-9 textual-ports)
- (ice-9 binary-ports)
- (ice-9 receive)
- (ice-9 match)
- ;; fibers
- (fibers)
- (fibers channels)
- ;; ftw stands for file-tree-walk
- ;; for file-system-tree
- (ice-9 ftw)
- ;; for match-lambda
- (ice-9 match)
- ;; lists
- (srfi srfi-1)
- ;; let-values
- (srfi srfi-11)
- ;; strings
- (srfi srfi-13)
- (prefix (file) file:)
- (alias)
- (list-helpers)
- (string-helpers)
- (commands))
- (define identity (λ (any) any))
- (define read-from-write-to
- (lambda* (in-port out-port #:key (bytes-count 1024))
- "Read from an IN-PORT and write to OUT-PORT, BYTES-COUNT
- bytes at a time."
- (let loop ([bv (get-bytevector-n in-port bytes-count)])
- (unless (eof-object? bv)
- (put-bytevector out-port bv)
- (loop (get-bytevector-n in-port bytes-count))))))
- (define run-command
- (lambda* (cmd
- #:key
- (cmd-out-port (current-output-port))
- (err-out-port (current-error-port)))
- "Allow the user to give output port and error port to the
- function."
- (with-output-to-port cmd-out-port
- (λ ()
- (with-error-to-port err-out-port
- (λ ()
- (let* (;; Run the actual command. If an error
- ;; happens, it should write to the
- ;; err-write port. Output of the command
- ;; should be written to an output port,
- ;; which corresponds to the input-port,
- ;; which is returned by open-input-pipe.
- [in-port (open-input-pipe cmd)]
- ;; Read in block mode.
- [_ignored (setvbuf in-port 'block)])
- ;; Write to caller given command output port.
- (read-from-write-to in-port cmd-out-port)
- ;; Get the exit code of the command.
- (close-pipe in-port))))))))
- ;; TODO: shell must use the current input port and current output
- ;; port and current error port for the shell command that is to be
- ;; run. In case of a shell command the return value is the output as
- ;; a string.
- (define shell
- (lambda* (command)
- "Run a shell COMMAND. Return 3 values: (1) exit code, (2)
- command output, (3) error output."
- ;; Construct pairs of input and outout ports using
- ;; `pipe'. Whatever is written to the output port can
- ;; be read from the input port.
- (match-let ([(cmd-in . cmd-out) (pipe)]
- [(err-in . err-out) (pipe)])
- (let ([exit-code
- (run-command command
- ;; Write command output to the
- ;; out port, so that it can be
- ;; read from in port.
- #:cmd-out-port cmd-out
- ;; Write error output to the
- ;; error out port, so that it
- ;; can be read from the error in
- ;; port.
- #:err-out-port err-out)])
- ;; Do not forget to close the out port and error
- ;; out port.
- (close-port cmd-out)
- (close-port err-out)
- ;; Read the (error) output of the command and
- ;; return it.
- (let ([output-message (get-string-all cmd-in)]
- [error-message (get-string-all err-in)])
- (values exit-code
- output-message
- error-message))))))
- (define call
- (lambda* (command
- #:key
- (display-exit-code #f)
- (exit-code-formatter
- (λ (exit-code) (string-append (number->string exit-code) "\n")))
- (cmd-out-formatter identity)
- (err-out-formatter identity))
- "Like shell, but displays the results of running the shell
- COMMAND, instead of returning them. How output is displayed
- can be optionally specified via keyword arguments
- EXIT-CODE-FORMATTER, CMD-OUT-FORMATTER,
- ERR-OUT-FORMATTER. The keyword argument DISPLAY-EXIT-CODE is
- a flag that enables or disables display of the exit code."
- (let-values ([(exit-code cmd-output err-output) (shell command)])
- (when display-exit-code
- (simple-format #t "~a" (exit-code-formatter exit-code)))
- (simple-format #t "~a" (cmd-out-formatter cmd-output))
- (simple-format #t "~a" (err-out-formatter err-output)))))
- (define with-ports
- (lambda* (proc
- #:key
- (in (current-input-port))
- (out (current-output-port))
- (err (current-error-port)))
- "Transform any procedure PROC into a shell procedure or shell command,
- by setting its IN, OUT and ERROR ports. Any procedure can use the
- current input, output and error ports internally, which are mapped to
- other ports using this WITH-PORTS function."
- (with-input-from-port in
- (λ ()
- (with-output-to-port out
- (λ ()
- (with-error-to-port err
- (λ ()
- (proc)))))))))
- ;; (define server
- ;; (λ (in out)
- ;; ;; infinite blocking loop
- ;; (let lp ()
- ;; (match (pk 'server-received #|block on get-message|# (get-message in))
- ;; ('ping! (put-message out 'pong!))
- ;; ('sup (put-message out 'not-much-u))
- ;; (msg (put-message out (cons 'wat msg))))
- ;; (lp))))
- ;; (define client
- ;; (λ (in out)
- ;; (for-each (λ (msg)
- ;; (put-message out msg)
- ;; (pk 'client-received (get-message in)))
- ;; (list '(1 2 3)
- ;; #(1 2 3)
- ;; ;; We can pass non-string data between fibers!
- ;; (make-point 1 2)))))
- (define command-pipeline
- (λ (. commands)
- (cond
- [(null? commands) '()]
- [else
- ;; Create a scheduler and run it in the main thread. Inside
- ;; this expression one can use `spawn-fiber' to spawn more
- ;; fibers.
- (run-fibers
- ;; `run-fibers' takes a procedure, which is run inside a
- ;; fiber, using the newly created scheduler.
- (λ ()
- (let iter ([commands° commands]
- [previous-output-channel (current-input-port)])
- (cond
- [(null? commands°)
- ;; Read from last command's output channel to get
- ;; the final result.
- (get-message previous-output-channel)]
- [else
- ;; Each command gets a new output channel, so that it
- ;; can write its output there.
- (let ([command-output-channel (make-channel)])
- (let ([command-fiber
- ;; Spawn the command as a fiber. Subsequent
- ;; commands need to read from its out channel
- ;; to receive input.
- (spawn-fiber
- (λ ()
- ((car commands°) previous-output-channel command-output-channel)))])
- ;; Output of this command is input of the next
- ;; command. Spawn fibers for the next commands.
- (iter (cdr commands°) command-output-channel)))])))
- #:drain? #t)])))
- (define echo-command
- (λ (in out)
- (cond
- [(channel? in)
- (let ([msg (get-message in)])
- (simple-format #t "received: ~a\n" msg)
- (put-message out msg))]
- [else
- (let ([msg "no message received"])
- (put-message out msg))])))
- ;; (run-fibers
- ;; (λ ()
- ;; (call-with-channel-input-string
- ;; "Hello!"
- ;; (λ (pipeline-input-channel)
- ;; ;; TODO: But how to get the pipeline-input-channel to be
- ;; ;; visible in the echo-command, so that echo-command makes use
- ;; ;; of it?
- ;; (command-pipeline echo-command
- ;; echo-command
- ;; echo-command
- ;; echo-command
- ;; echo-command)))))
- ;; EXAMPLE CALLS:
- #;(with-output-to-file "test-output.log"
- (λ ()
- (call "ls -al" #:display-exit-code #t)))
- #;(with-output-to-file "test-output.log"
- (λ ()
- (with-input-from-file "test-input.log"
- (λ ()
- (call "cut -d ' ' -f 1-2" #:display-exit-code #t)))))
- ;; IDEA: Write a function which works like this: (direct function #:in #:out)
- ;; TODO: IDEA: Building a pipeline of commands means, that pipes are
- ;; constructed, which an earlier command can use to write output and
- ;; a later command can use to read input.
- )
|