(ns clojure.java.shell
"Run a host program and capture its result: the clojure.java.shell API over
the native clojure.rust.process/run. The program is started by argv, never
through a shell.")
(def ^:dynamic *sh-dir*
"Working directory for `sh` calls made in this binding, or nil to inherit."
nil)
(def ^:dynamic *sh-env*
"Environment map for `sh` calls made in this binding, or nil to inherit."
nil)
(defmacro with-sh-dir
"Evaluate body with `sh` running programs in directory dir."
[dir & body]
`(binding [*sh-dir* ~dir] ~@body))
(defmacro with-sh-env
"Evaluate body with `sh` running programs under environment env, which
replaces the inherited environment."
[env & body]
`(binding [*sh-env* ~env] ~@body))
(defn- utf-8? [enc]
(or (nil? enc) (contains? #{"UTF-8" "utf-8" "UTF8" "utf8"} enc)))
(defn- env-map [env]
(cond
(nil? env) nil
(map? env) (into {} (map (fn [[k v]] [(if (keyword? k) (name k) (str k)) (str v)]) env))
:else (into {}
(map (fn [s]
(let [s (str s)
i (clojure.string/index-of s "=")]
(if (nil? i)
(throw (ex-info (str "sh: an :env entry must be NAME=value, got " (pr-str s)) {:entry s}))
[(subs s 0 i) (subs s (inc i))])))
env))))
(defn sh
"Run a host program and wait for it to finish.
args are the program and its arguments as strings, followed by options:
:in text for the program's standard input; a non-string is slurped
:in-enc encoding of :in (UTF-8 only)
:out-enc encoding of stdout: UTF-8 (the default) or :bytes
:dir working directory
:env environment replacing the inherited one: a map, or a seq of
\"NAME=value\" strings
Returns {:exit code :out stdout :err stderr}. A non-zero exit is returned,
not thrown."
[& args]
(let [[cmd opts] (split-with string? args)
_ (when (empty? cmd)
(throw (ex-info "sh: the program name must be given as a string" {:args args})))
_ (when (odd? (count opts))
(throw (ex-info (str "sh: options must be key/value pairs, got " (pr-str opts)) {:opts opts})))
{:keys [in in-enc out-enc dir env]
:or {dir *sh-dir* env *sh-env*}} (apply hash-map opts)]
(when-not (utf-8? in-enc)
(throw (ex-info (str "sh: unsupported :in-enc " (pr-str in-enc) "; only UTF-8 is supported") {:in-enc in-enc})))
(when-not (or (utf-8? out-enc) (= :bytes out-enc))
(throw (ex-info (str "sh: unsupported :out-enc " (pr-str out-enc) "; only UTF-8 and :bytes are supported") {:out-enc out-enc})))
(clojure.rust.process/run
(vec cmd)
(cond-> {}
(some? in) (assoc :in (if (string? in) in (slurp in)))
(some? dir) (assoc :dir (str dir))
(some? env) (assoc :env (env-map env))
(= :bytes out-enc) (assoc :out-bytes true)))))