A Clojure wrapper around java.lang.ProcessBuilder
.
Status: alpha.
This code may end up in babashka but is also intended as a JVM library. You can play with this code in babashka today, by either copying the code or including this library as a git dep:
$ export BABASHKA_CLASSPATH=$(clojure -Sdeps '{:deps {babashka/babashka.process {:sha "<latest-sha>" :git/url "https://github.com/babashka/babashka.process"}}}' -Spath)
user=> (require '[clojure.string :as str])
nil
user=> (require '[babashka.process :refer [process]])
nil
user=> (-> (process ["ls" "-la"]) :out slurp str/split-lines first)
"total 136776"
-
process
: takes a command (vector of strings) and optionally a map of options.Returns: a record with
:proc
: an instance ofjava.lang.Process
:in
,:err
,:out
: the process's streams. To obtain a string from:out
or:err
you will typicall useslurp
. Slurping those streams will block the current thread until the process is finished.:exit
: delay containing the exit code. Realizing the delay will block current thread until process is finished.:command
: the command that was passed to create the process.
The returned record may be passed to
deref
. Doing so will runcheck
on the record.Supported options:
:in
,:out
,:err
: objects compatible withclojure.java.io/copy
that will be copied to or from the process's corresponding stream. May be set to:inherit
for redirecting to the parent process's corresponding stream. Optional:in-enc
,:out-enc
and:err-enc
values will be passed along toclojure.java.io/copy
.:dir
: working directory.:env
: a map of environment variables.
-
check
: takes a record as produced byprocess
, checks the exit code of the underlying process (blocking until the process is finished) and throws if it was non-zero.
user=> (require '[babashka.process :refer [process check]])
Invoke ls
:
user=> (-> (process ["ls"]) :out slurp)
"LICENSE\nREADME.md\nsrc\n"
Change working directory:
user=> (-> (process ["ls"] {:dir "test/babashka"}) :out slurp)
"process_test.clj\n"
Set the process environment.
user=> (-> (process ["sh" "-c" "echo $FOO"] {:env {:FOO "BAR" }}) :out slurp)
"BAR\n"
The exit code is returned as a delay. Realizing that delay will wait until the process finishes.
user=> (-> (process ["ls" "foo"]) :exit deref)
1
The function check
takes a process, waits for it to finish and returns it. When
the exit code is non-zero, it will throw.
user=> (-> (process ["ls" "foo"]) check :out slurp)
Execution error (ExceptionInfo) at babashka.process/check (process.clj:74).
ls: foo: No such file or directory
The return value of process
implements clojure.lang.IDeref
. When
dereferenced, it will execute check
:
user=> (-> @(process ["ls" "foo"]) :out slurp)
Execution error (ExceptionInfo) at babashka.process/check (process.clj:74).
ls: foo: No such file or directory
Redirect output to stdout:
user=> (do (process ["ls"] {:out :inherit}) nil)
LICENSE README.md deps.edn src test
nil
Redirect output stream from one process to input stream of the next process:
(let [is (-> (process ["ls"]) :out)]
(process ["cat"] {:in is
:out :inherit})
nil)
LICENSE
README.md
deps.edn
src
test
nil
Both :in
and :out
may contain objects that are compatible with clojure.java.io/copy
:
user=> (with-out-str (check (process ["cat"] {:in "foo" :out *out*})))
"foo"
user=> (with-out-str (check (process ["ls"] {:out *out*})))
"LICENSE\nREADME.md\ndeps.edn\nsrc\ntest\n"
Forwarding the output of a process as the input of another process can also be done with thread-first:
(-> (process ["ls"])
(process ["grep" "README"]) :out slurp)
"README.md\n"
Demo of a cat
process to which we send input while the process is running, then close stdin and read the output of cat afterwards:
(ns cat-demo
(:require [babashka.process :refer [process]]
[clojure.java.io :as io]))
(def catp (process ["cat"]))
(.isAlive (:proc catp)) ;; true
(def stdin (io/writer (:in catp)))
(binding [*out* stdin]
(println "hello"))
(.close stdin)
(def exit @(:exit catp)) ;; 0
(.isAlive (:proc catp)) ;; false
(slurp (:out catp)) ;; "hello\n"
Because process
spawns threads for non-blocking I/O, you might have to run
(shutdown-agents)
at the end of your Clojure JVM scripts to force
termination. Babashka does this automatically.
When piping streams with infrequent output like in this example:
(ns pipes
(:require [babashka.process :refer [process]]))
;; continually write to log
(future
(loop []
(spit "log.txt" (str (rand-int 10) "\n") :append true)
(Thread/sleep 10)
(recur)))
(-> (process ["tail" "-f" "log.txt"])
(process ["cat"])
(process ["grep" "5"] {:out :inherit}))
it may take a while before you will start seeing output, due to buffering.
If this is an issue, you can copy the output of tail
to the input of cat
yourself line by line:
(def tail (process ["tail" "-f" "log.txt"] {:err :inherit}))
(def cat-and-grep
(-> (process ["cat"] {:err :inherit})
(process ["grep" "5"] {:out :inherit
:err :inherit})))
(binding [*in* (io/reader (:out tail))
*out* (io/writer (:in cat-and-grep))]
(loop []
(when-let [x (read-line)]
(println x)
(recur))))
Copyright © 2020 Michiel Borkent
Distributed under the EPL License. See LICENSE.