Skip to content

Commit

Permalink
Add basilisp.shell namespace
Browse files Browse the repository at this point in the history
  • Loading branch information
chrisrink10 committed Mar 15, 2020
1 parent af28206 commit 77e0a4f
Show file tree
Hide file tree
Showing 2 changed files with 92 additions and 0 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
* Added line, column, and file information to reader `SyntaxError`s (#488)
* Added context information to the `CompilerException` string output (#493)
* Added Array (Python list) functions (#504, #509)
* Added shell function in `basilisp.shell` namespace (#515)

### Changed
* Change the default user namespace to `basilisp.user` (#466)
Expand Down
91 changes: 91 additions & 0 deletions src/basilisp/shell.lpy
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
(ns basilisp.shell
(:import subprocess))

(def ^:dynamic *sh-dir*
"Bind to the value of the working directory to use for calls to `sh` if the `:dir`
argument is not provided. Callers may use the `with-sh-dir` macro to bind this
value for multiple calls to `sh`.
Defaults to `nil`, which will use the current working directory of this process."
nil)

(def ^:dynamic *sh-env*
"Bind to a map of environment variables to use for calls to `sh` if the `:env`
argument is not provided. Callers may use the `with-sh-env` macro to bind this
value for multiple calls to `sh`.
Defaults to `nil`, which will use the current process's environment."
nil)

(defmacro with-sh-dir
"Convenience macro for binding `*sh-dir*` for multiple `sh` invocations."
[dir & body]
`(binding [*sh-dir* ~dir]
~@body))

(defmacro with-sh-env
"Convenience macro for binding `*sh-env*` for multiple `sh` invocations."
[env-map & body]
`(binding [*sh-env* ~env-map]
~@body))

(defn sh
"Execute a shell command as a subprocess of the current process.
Commands are specified as a series of string arguments split on whitespace:
(sh \"ls\" \"-la\")
Following the command, 0 or more keyword/value pairs may be specified to
control input and output options to the subprocess. The options are:
- :in - a string, byte string, byte array, file descriptor, or file
object
- :in-enc - a string value matching one of Python's supported encodings;
if the value of `:in` is a string, decode that string to bytes
using the encoding named here; if none is specified, `utf-8`
will be used; if the value of `:in` is not a string, this value
will be ignored
- :out-enc - a string value matching on of Python's supported encodings or
the special value `:bytes`; if specified as a string, decode the
standard out and standard error streams returned by the subprocess
using this encoding; if specified as `:bytes`, return the byte
string from the output without encoding; if none is specified,
`utf-8` will be used
- :env - a mapping of string values to string values which are used as the
subprocess's environment; if none is specified and `*sh-env` is
not set, the environment of the current process will be used
- :dir - a string indicating the working directory which is to be used for
the subprocess; if none is specified and `*sh-dir*` is not set,
the working directory of the current process will be used"
[& args]
(let [[cmd arg-seq] (split-with string? args)
sh-args (apply hash-map arg-seq)
out-enc (:out-enc sh-args "utf-8")
[input stdin] (when-let [input-val (:in sh-args)]
(cond
(string? input-val)
[(.encode input-val (:in-enc sh-args "utf-8")) nil]

(or (bytes? input-val)
(byte-string? input-val))
[input-val nil]

:else
[nil input-val]))

;; subprocess.run completely barfs if you even supply the stdin
;; kwarg at the same time as the input kwarg, so we have to do
;; this nonsense to avoid sending them both in
opts (cond-> {:cwd (:dir sh-args *sh-dir*)
:env (:env sh-args *sh-env*)
:stdout subprocess/PIPE
:stderr subprocess/PIPE}
input (assoc :input input)
stdin (assoc :stdin stdin))
result (apply-kw subprocess/run (python/list cmd) opts)]
{:exit (.-returncode result)
:out (cond-> (.-stdout result)
(not= out-enc :bytes) (.decode out-enc))
:err (cond-> (.-stderr result)
(not= out-enc :bytes) (.decode out-enc))}))

0 comments on commit 77e0a4f

Please sign in to comment.