shell.scm 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290
  1. (library (shell)
  2. (export
  3. ;; shell logic
  4. shell
  5. call
  6. run-command
  7. with-ports
  8. command-pipeline
  9. echo-command)
  10. (import (except (rnrs base) error)
  11. (only (guile)
  12. lambda* λ
  13. ;; control flow
  14. when
  15. unless
  16. ;; ports
  17. current-input-port
  18. current-output-port
  19. current-error-port
  20. with-input-from-port
  21. with-output-to-port
  22. with-error-to-port
  23. with-input-from-string
  24. close-port
  25. pipe
  26. ;; other
  27. setvbuf
  28. eof-object?
  29. ;; string formatting
  30. simple-format
  31. ;; basic shell procedures guile provides
  32. getcwd
  33. chdir
  34. ;; other
  35. error
  36. ;; strings
  37. string-split
  38. )
  39. (ice-9 exceptions)
  40. ;; pipes
  41. (ice-9 popen)
  42. (ice-9 textual-ports)
  43. (ice-9 binary-ports)
  44. (ice-9 receive)
  45. (ice-9 match)
  46. ;; fibers
  47. (fibers)
  48. (fibers channels)
  49. ;; ftw stands for file-tree-walk
  50. ;; for file-system-tree
  51. (ice-9 ftw)
  52. ;; for match-lambda
  53. (ice-9 match)
  54. ;; lists
  55. (srfi srfi-1)
  56. ;; let-values
  57. (srfi srfi-11)
  58. ;; strings
  59. (srfi srfi-13)
  60. (prefix (file) file:)
  61. (alias)
  62. (list-helpers)
  63. (string-helpers)
  64. (commands))
  65. (define identity (λ (any) any))
  66. (define read-from-write-to
  67. (lambda* (in-port out-port #:key (bytes-count 1024))
  68. "Read from an IN-PORT and write to OUT-PORT, BYTES-COUNT
  69. bytes at a time."
  70. (let loop ([bv (get-bytevector-n in-port bytes-count)])
  71. (unless (eof-object? bv)
  72. (put-bytevector out-port bv)
  73. (loop (get-bytevector-n in-port bytes-count))))))
  74. (define run-command
  75. (lambda* (cmd
  76. #:key
  77. (cmd-out-port (current-output-port))
  78. (err-out-port (current-error-port)))
  79. "Allow the user to give output port and error port to the
  80. function."
  81. (with-output-to-port cmd-out-port
  82. (λ ()
  83. (with-error-to-port err-out-port
  84. (λ ()
  85. (let* (;; Run the actual command. If an error
  86. ;; happens, it should write to the
  87. ;; err-write port. Output of the command
  88. ;; should be written to an output port,
  89. ;; which corresponds to the input-port,
  90. ;; which is returned by open-input-pipe.
  91. [in-port (open-input-pipe cmd)]
  92. ;; Read in block mode.
  93. [_ignored (setvbuf in-port 'block)])
  94. ;; Write to caller given command output port.
  95. (read-from-write-to in-port cmd-out-port)
  96. ;; Get the exit code of the command.
  97. (close-pipe in-port))))))))
  98. ;; TODO: shell must use the current input port and current output
  99. ;; port and current error port for the shell command that is to be
  100. ;; run. In case of a shell command the return value is the output as
  101. ;; a string.
  102. (define shell
  103. (lambda* (command)
  104. "Run a shell COMMAND. Return 3 values: (1) exit code, (2)
  105. command output, (3) error output."
  106. ;; Construct pairs of input and outout ports using
  107. ;; `pipe'. Whatever is written to the output port can
  108. ;; be read from the input port.
  109. (match-let ([(cmd-in . cmd-out) (pipe)]
  110. [(err-in . err-out) (pipe)])
  111. (let ([exit-code
  112. (run-command command
  113. ;; Write command output to the
  114. ;; out port, so that it can be
  115. ;; read from in port.
  116. #:cmd-out-port cmd-out
  117. ;; Write error output to the
  118. ;; error out port, so that it
  119. ;; can be read from the error in
  120. ;; port.
  121. #:err-out-port err-out)])
  122. ;; Do not forget to close the out port and error
  123. ;; out port.
  124. (close-port cmd-out)
  125. (close-port err-out)
  126. ;; Read the (error) output of the command and
  127. ;; return it.
  128. (let ([output-message (get-string-all cmd-in)]
  129. [error-message (get-string-all err-in)])
  130. (values exit-code
  131. output-message
  132. error-message))))))
  133. (define call
  134. (lambda* (command
  135. #:key
  136. (display-exit-code #f)
  137. (exit-code-formatter
  138. (λ (exit-code) (string-append (number->string exit-code) "\n")))
  139. (cmd-out-formatter identity)
  140. (err-out-formatter identity))
  141. "Like shell, but displays the results of running the shell
  142. COMMAND, instead of returning them. How output is displayed
  143. can be optionally specified via keyword arguments
  144. EXIT-CODE-FORMATTER, CMD-OUT-FORMATTER,
  145. ERR-OUT-FORMATTER. The keyword argument DISPLAY-EXIT-CODE is
  146. a flag that enables or disables display of the exit code."
  147. (let-values ([(exit-code cmd-output err-output) (shell command)])
  148. (when display-exit-code
  149. (simple-format #t "~a" (exit-code-formatter exit-code)))
  150. (simple-format #t "~a" (cmd-out-formatter cmd-output))
  151. (simple-format #t "~a" (err-out-formatter err-output)))))
  152. (define with-ports
  153. (lambda* (proc
  154. #:key
  155. (in (current-input-port))
  156. (out (current-output-port))
  157. (err (current-error-port)))
  158. "Transform any procedure PROC into a shell procedure or shell command,
  159. by setting its IN, OUT and ERROR ports. Any procedure can use the
  160. current input, output and error ports internally, which are mapped to
  161. other ports using this WITH-PORTS function."
  162. (with-input-from-port in
  163. (λ ()
  164. (with-output-to-port out
  165. (λ ()
  166. (with-error-to-port err
  167. (λ ()
  168. (proc)))))))))
  169. ;; (define server
  170. ;; (λ (in out)
  171. ;; ;; infinite blocking loop
  172. ;; (let lp ()
  173. ;; (match (pk 'server-received #|block on get-message|# (get-message in))
  174. ;; ('ping! (put-message out 'pong!))
  175. ;; ('sup (put-message out 'not-much-u))
  176. ;; (msg (put-message out (cons 'wat msg))))
  177. ;; (lp))))
  178. ;; (define client
  179. ;; (λ (in out)
  180. ;; (for-each (λ (msg)
  181. ;; (put-message out msg)
  182. ;; (pk 'client-received (get-message in)))
  183. ;; (list '(1 2 3)
  184. ;; #(1 2 3)
  185. ;; ;; We can pass non-string data between fibers!
  186. ;; (make-point 1 2)))))
  187. (define command-pipeline
  188. (λ (. commands)
  189. (cond
  190. [(null? commands) '()]
  191. [else
  192. ;; Create a scheduler and run it in the main thread. Inside
  193. ;; this expression one can use `spawn-fiber' to spawn more
  194. ;; fibers.
  195. (run-fibers
  196. ;; `run-fibers' takes a procedure, which is run inside a
  197. ;; fiber, using the newly created scheduler.
  198. (λ ()
  199. (let iter ([commands° commands]
  200. [previous-output-channel (current-input-port)])
  201. (cond
  202. [(null? commands°)
  203. ;; Read from last command's output channel to get
  204. ;; the final result.
  205. (get-message previous-output-channel)]
  206. [else
  207. ;; Each command gets a new output channel, so that it
  208. ;; can write its output there.
  209. (let ([command-output-channel (make-channel)])
  210. (let ([command-fiber
  211. ;; Spawn the command as a fiber. Subsequent
  212. ;; commands need to read from its out channel
  213. ;; to receive input.
  214. (spawn-fiber
  215. (λ ()
  216. ((car commands°) previous-output-channel command-output-channel)))])
  217. ;; Output of this command is input of the next
  218. ;; command. Spawn fibers for the next commands.
  219. (iter (cdr commands°) command-output-channel)))])))
  220. #:drain? #t)])))
  221. (define echo-command
  222. (λ (in out)
  223. (cond
  224. [(channel? in)
  225. (let ([msg (get-message in)])
  226. (simple-format #t "received: ~a\n" msg)
  227. (put-message out msg))]
  228. [else
  229. (let ([msg "no message received"])
  230. (put-message out msg))])))
  231. ;; (run-fibers
  232. ;; (λ ()
  233. ;; (call-with-channel-input-string
  234. ;; "Hello!"
  235. ;; (λ (pipeline-input-channel)
  236. ;; ;; TODO: But how to get the pipeline-input-channel to be
  237. ;; ;; visible in the echo-command, so that echo-command makes use
  238. ;; ;; of it?
  239. ;; (command-pipeline echo-command
  240. ;; echo-command
  241. ;; echo-command
  242. ;; echo-command
  243. ;; echo-command)))))
  244. ;; EXAMPLE CALLS:
  245. #;(with-output-to-file "test-output.log"
  246. (λ ()
  247. (call "ls -al" #:display-exit-code #t)))
  248. #;(with-output-to-file "test-output.log"
  249. (λ ()
  250. (with-input-from-file "test-input.log"
  251. (λ ()
  252. (call "cut -d ' ' -f 1-2" #:display-exit-code #t)))))
  253. ;; IDEA: Write a function which works like this: (direct function #:in #:out)
  254. ;; TODO: IDEA: Building a pipeline of commands means, that pipes are
  255. ;; constructed, which an earlier command can use to write output and
  256. ;; a later command can use to read input.
  257. )