rlm@10: ; Copyright (c) Chris Houser, Jan 2009. All rights reserved. rlm@10: ; The use and distribution terms for this software are covered by the rlm@10: ; Eclipse Public License 1.0 (http://opensource.org/licenses/eclipse-1.0.php) rlm@10: ; which can be found in the file epl-v10.html at the root of this distribution. rlm@10: ; By using this software in any fashion, you are agreeing to be bound by rlm@10: ; the terms of this license. rlm@10: ; You must not remove this notice, or any other, from this software. rlm@10: rlm@10: ; :dir and :env options added by Stuart Halloway rlm@10: rlm@10: ; Conveniently launch a sub-process providing to its stdin and rlm@10: ; collecting its stdout rlm@10: rlm@10: ;; DEPRECATED in 1.2: Promoted to clojure.java.shell rlm@10: rlm@10: (ns rlm@10: ^{:author "Chris Houser", rlm@10: :deprecated "1.2" rlm@10: :doc "Conveniently launch a sub-process providing to its stdin and rlm@10: collecting its stdout"} rlm@10: clojure.contrib.shell rlm@10: (:import (java.io InputStreamReader OutputStreamWriter))) rlm@10: rlm@10: (def *sh-dir* nil) rlm@10: (def *sh-env* nil) rlm@10: rlm@10: (defmacro with-sh-dir [dir & forms] rlm@10: "Sets the directory for use with sh, see sh for details." rlm@10: `(binding [*sh-dir* ~dir] rlm@10: ~@forms)) rlm@10: rlm@10: (defmacro with-sh-env [env & forms] rlm@10: "Sets the environment for use with sh, see sh for details." rlm@10: `(binding [*sh-env* ~env] rlm@10: ~@forms)) rlm@10: rlm@10: (defn- stream-seq rlm@10: "Takes an InputStream and returns a lazy seq of integers from the stream." rlm@10: [stream] rlm@10: (take-while #(>= % 0) (repeatedly #(.read stream)))) rlm@10: rlm@10: (defn- aconcat rlm@10: "Concatenates arrays of given type." rlm@10: [type & xs] rlm@10: (let [target (make-array type (apply + (map count xs)))] rlm@10: (loop [i 0 idx 0] rlm@10: (when-let [a (nth xs i nil)] rlm@10: (System/arraycopy a 0 target idx (count a)) rlm@10: (recur (inc i) (+ idx (count a))))) rlm@10: target)) rlm@10: rlm@10: (defn- parse-args rlm@10: "Takes a seq of 'sh' arguments and returns a map of option keywords rlm@10: to option values." rlm@10: [args] rlm@10: (loop [[arg :as args] args opts {:cmd [] :out "UTF-8" :dir *sh-dir* :env *sh-env*}] rlm@10: (if-not args rlm@10: opts rlm@10: (if (keyword? arg) rlm@10: (recur (nnext args) (assoc opts arg (second args))) rlm@10: (recur (next args) (update-in opts [:cmd] conj arg)))))) rlm@10: rlm@10: (defn- as-env-key [arg] rlm@10: "Helper so that callers can use symbols, keywords, or strings rlm@10: when building an environment map." rlm@10: (cond rlm@10: (symbol? arg) (name arg) rlm@10: (keyword? arg) (name arg) rlm@10: (string? arg) arg)) rlm@10: rlm@10: (defn- as-file [arg] rlm@10: "Helper so that callers can pass a String for the :dir to sh." rlm@10: (cond rlm@10: (string? arg) (java.io.File. arg) rlm@10: (nil? arg) nil rlm@10: (instance? java.io.File arg) arg)) rlm@10: rlm@10: (defn- as-env-string [arg] rlm@10: "Helper so that callers can pass a Clojure map for the :env to sh." rlm@10: (cond rlm@10: (nil? arg) nil rlm@10: (map? arg) (into-array String (map (fn [[k v]] (str (as-env-key k) "=" v)) arg)) rlm@10: true arg)) rlm@10: rlm@10: rlm@10: (defn sh rlm@10: "Passes the given strings to Runtime.exec() to launch a sub-process. rlm@10: rlm@10: Options are rlm@10: rlm@10: :in may be given followed by a String specifying text to be fed to the rlm@10: sub-process's stdin. rlm@10: :out option may be given followed by :bytes or a String. If a String rlm@10: is given, it will be used as a character encoding name (for rlm@10: example \"UTF-8\" or \"ISO-8859-1\") to convert the rlm@10: sub-process's stdout to a String which is returned. rlm@10: If :bytes is given, the sub-process's stdout will be stored in rlm@10: a byte array and returned. Defaults to UTF-8. rlm@10: :return-map rlm@10: when followed by boolean true, sh returns a map of rlm@10: :exit => sub-process's exit code rlm@10: :out => sub-process's stdout (as byte[] or String) rlm@10: :err => sub-process's stderr (as byte[] or String) rlm@10: when not given or followed by false, sh returns a single rlm@10: array or String of the sub-process's stdout followed by its rlm@10: stderr rlm@10: :env override the process env with a map (or the underlying Java rlm@10: String[] if you are a masochist). rlm@10: :dir override the process dir with a String or java.io.File. rlm@10: rlm@10: You can bind :env or :dir for multiple operations using with-sh-env rlm@10: and with-sh-dir." rlm@10: [& args] rlm@10: (let [opts (parse-args args) rlm@10: proc (.exec (Runtime/getRuntime) rlm@10: (into-array (:cmd opts)) rlm@10: (as-env-string (:env opts)) rlm@10: (as-file (:dir opts)))] rlm@10: (if (:in opts) rlm@10: (with-open [osw (OutputStreamWriter. (.getOutputStream proc))] rlm@10: (.write osw (:in opts))) rlm@10: (.close (.getOutputStream proc))) rlm@10: (with-open [stdout (.getInputStream proc) rlm@10: stderr (.getErrorStream proc)] rlm@10: (let [[[out err] combine-fn] rlm@10: (if (= (:out opts) :bytes) rlm@10: [(for [strm [stdout stderr]] rlm@10: (into-array Byte/TYPE (map byte (stream-seq strm)))) rlm@10: #(aconcat Byte/TYPE %1 %2)] rlm@10: [(for [strm [stdout stderr]] rlm@10: (apply str (map char (stream-seq rlm@10: (InputStreamReader. strm (:out opts)))))) rlm@10: str]) rlm@10: exit-code (.waitFor proc)] rlm@10: (if (:return-map opts) rlm@10: {:exit exit-code :out out :err err} rlm@10: (combine-fn out err)))))) rlm@10: rlm@10: (comment rlm@10: rlm@10: (println (sh "ls" "-l")) rlm@10: (println (sh "ls" "-l" "/no-such-thing")) rlm@10: (println (sh "sed" "s/[aeiou]/oo/g" :in "hello there\n")) rlm@10: (println (sh "cat" :in "x\u25bax\n")) rlm@10: (println (sh "echo" "x\u25bax")) rlm@10: (println (sh "echo" "x\u25bax" :out "ISO-8859-1")) ; reads 4 single-byte chars rlm@10: (println (sh "cat" "myimage.png" :out :bytes)) ; reads binary file into bytes[] rlm@10: rlm@10: )