example-02-using-popen-write-log-file.scm 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197
  1. ;;; ABOUT
  2. ;; The examples in this document were posted on the Guile
  3. ;; user mailing list and are not originally written by me
  4. ;; zelphirkaltstahl@posteo.de.
  5. ;; Comments, some formatting and editing by me
  6. ;; (zelphirkaltstahl@posteo.de).
  7. ;; The following code is adapted from a post on the Guile
  8. ;; user mailing list, posted by post@thomasdanckaert.be and
  9. ;; is part of
  10. ;; https://github.com/tdanckaert/jobview/blob/master/jobtools.scm#L38.
  11. (import (ice-9 popen)
  12. (ice-9 textual-ports)
  13. (ice-9 exceptions)
  14. (ice-9 receive)
  15. (ice-9 match))
  16. (define process-output
  17. (λ (proc cmd)
  18. "Runs CMD as an external process, with an input port from which
  19. the process' stdout may be read, and runs the procedure PROC. PROC
  20. takes as input the input port, from which is can read the output of
  21. the command, as a single argument and processes it in any way.
  22. Throws an exception 'cmd-failed if CMD's exit-code is non-zero."
  23. ;; Bind some ports for error messaging and handling.
  24. (match-let (;; Create a pipe, which consists of 2 ports,
  25. ;; an input and an output port. These are
  26. ;; meant for error messages. Extract input
  27. ;; port and output port via pattern matching
  28. ;; from the pair which (pipe)
  29. ;; returns. err-read is an input port
  30. ;; err-write is an output port.
  31. [(err-read . err-write) (pipe)]
  32. ;; Alias the current error port to use
  33. ;; later.
  34. [stderr (current-error-port)])
  35. ;; Create a context or scope, in which the error port
  36. ;; is set to the output port of the pipe. This will
  37. ;; enable to read the errors from the input port, once
  38. ;; they have been written to the output port.
  39. (with-error-to-port err-write
  40. (λ ()
  41. (let* (;; open-input-pipe is the same as open-pipe
  42. ;; with mode OPEN_READ. open-pipe runs a
  43. ;; command in a child process. More
  44. ;; precisely it runs the command as
  45. ;; argument to '/bin/sh -c'. The OPEN_READ
  46. ;; mode makes it so that the return value
  47. ;; is an input port, from which one can
  48. ;; read the output of the command. The call
  49. ;; to open-input-pipe is the actual call
  50. ;; running the command.
  51. [port (open-input-pipe cmd)]
  52. ;; Set the buffer behavior of the port to
  53. ;; block buffered. For more detail read:
  54. ;; https://www.gnu.org/software/guile/manual/html_node/Buffering.html. Line
  55. ;; buffered might also make sense for
  56. ;; running commands and getting their
  57. ;; output. However, a line can also be very
  58. ;; long, so maybe block buffered is more
  59. ;; generally applicable.
  60. [_ignored (setvbuf port 'block)]
  61. ;; Apply the given procedure PROC to the
  62. ;; port. The procedure must expect an input
  63. ;; port as an argument, from which it
  64. ;; reads.
  65. [processed-output
  66. ;; Error handling using Guile's catch
  67. ;; procedure. catch is given 2 lambda
  68. ;; expressions. One to evaluate in any
  69. ;; case, and a second one to handle
  70. ;; errors, if any happen, during the
  71. ;; evaluation of the first lambda
  72. ;; expression.
  73. ;; The argument #t specifies, that
  74. ;; whatever the key of any raised
  75. ;; exception is, it will be handled by the
  76. ;; second lambda expression.
  77. ;; This exception handling deals with
  78. ;; errors, that originate from applying
  79. ;; the output processor, not with
  80. ;; exceptions from running the command
  81. ;; itself.
  82. (catch #t
  83. ;; Catch any exception thrown by applying PROC to
  84. ;; the output of CMD: if CMD fails, we check the
  85. ;; exit-code below; if CMD succeeds, PROC must be
  86. ;; able to deal with its output.
  87. (λ () (proc port))
  88. ;; Exception handling procedure. It
  89. ;; takes the key of the exception, which
  90. ;; is a symbol, and an arbitrary number
  91. ;; of other arguments.
  92. (λ (key . args)
  93. ;; To handle any error, output to the
  94. ;; stderr of the outer context, as
  95. ;; stderr was bound earlier. This
  96. ;; might not be actually handling an
  97. ;; exception at all, but at least
  98. ;; tells us, that something has gone
  99. ;; wrong.
  100. (format stderr "Caught exception ~a from ~y~%" key proc)))]
  101. ;; Finally, close the port and retrieve the
  102. ;; exit-code, which is the exit code of the
  103. ;; command, which was run with '/bin/sh
  104. ;; -c'.
  105. [exit-code (close-pipe port)])
  106. ;; Close the port, to which the child process
  107. ;; was to write errors, as the child process has
  108. ;; finished (either successfully or
  109. ;; unsuccessfully, but definitely finished).
  110. (close-port err-write)
  111. ;; If the exit code was non-zero, get the output
  112. ;; by reading from the err-read port, which is
  113. ;; the corresponding input port of the initially
  114. ;; created pipe.
  115. (display (simple-format #f "~a\n" "checking the exit code"))
  116. (cond
  117. [(zero? exit-code)
  118. (values exit-code processed-output)]
  119. [else
  120. (let ([error-message (get-string-all err-read)])
  121. (values exit-code error-message))])))))))
  122. ;; Example for write output to file.
  123. (define get-string-from-file
  124. (lambda* (file-path #:key (encoding "UTF-8"))
  125. (call-with-input-file file-path
  126. (λ (port)
  127. (set-port-encoding! port encoding)
  128. (get-string-all port)))))
  129. (define make-output-to-file-processor
  130. (lambda* (filename #:key (encoding "UTF-8") (mode 'replace))
  131. ;; Return a lambda, which takes the input port to work
  132. ;; with the input port, from which is can read a
  133. ;; command's output.
  134. (λ (in-port)
  135. ;; Get the output from the input port, which will
  136. ;; later be written to a file.
  137. (let ([output (get-string-all in-port)])
  138. ;; Write output to a file specified by filename.
  139. (call-with-output-file filename
  140. (λ (output-port)
  141. (set-port-encoding! output-port encoding)
  142. ;; Depending on the mode of file writing, write
  143. ;; the output or previous file content and
  144. ;; output.
  145. (cond
  146. [(eq? mode 'append)
  147. (let* ([current-file-content
  148. (get-string-from-file filename #:encoding encoding)]
  149. [complete-content
  150. (string-append current-file-content "\n" output)])
  151. (put-string output-port complete-content))]
  152. [(eq? mode 'replace)
  153. (put-string output-port output)]
  154. [else
  155. (raise-exception
  156. (make-exception
  157. (make-non-continuable-error)
  158. (make-exception-with-message "unrecognized file writing mode")
  159. (make-exception-with-irritants (list mode))
  160. (make-exception-with-origin 'make-output-to-file-processor)))])))))))
  161. (define log-file-writer
  162. (make-output-to-file-processor "command.log" #:mode 'replace))
  163. (receive (exit-code output)
  164. (process-output log-file-writer "echo 'my file content'")
  165. (display (simple-format #f "exit code: ~a\n" exit-code))
  166. (display (simple-format #f "output: ~a\n" output)))
  167. (receive (exit-code output)
  168. (let ([command
  169. (string-join
  170. ;; Construct a command, which
  171. ;; redirects stdout to stderr.
  172. (list "bash" "-c" "echo bong 1>&2")
  173. " ")])
  174. (process-output log-file-writer command))
  175. (display (simple-format #f "exit code: ~a\n" exit-code))
  176. (display (simple-format #f "output: ~a\n" output)))