Skip to content

Commit 4674364

Browse files
authored
Add basilisp.shell namespace (#515)
1 parent af28206 commit 4674364

File tree

2 files changed

+92
-0
lines changed

2 files changed

+92
-0
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1515
* Added line, column, and file information to reader `SyntaxError`s (#488)
1616
* Added context information to the `CompilerException` string output (#493)
1717
* Added Array (Python list) functions (#504, #509)
18+
* Added shell function in `basilisp.shell` namespace (#515)
1819

1920
### Changed
2021
* Change the default user namespace to `basilisp.user` (#466)

src/basilisp/shell.lpy

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
(ns basilisp.shell
2+
(:import subprocess))
3+
4+
(def ^:dynamic *sh-dir*
5+
"Bind to the value of the working directory to use for calls to `sh` if the `:dir`
6+
argument is not provided. Callers may use the `with-sh-dir` macro to bind this
7+
value for multiple calls to `sh`.
8+
9+
Defaults to `nil`, which will use the current working directory of this process."
10+
nil)
11+
12+
(def ^:dynamic *sh-env*
13+
"Bind to a map of environment variables to use for calls to `sh` if the `:env`
14+
argument is not provided. Callers may use the `with-sh-env` macro to bind this
15+
value for multiple calls to `sh`.
16+
17+
Defaults to `nil`, which will use the current process's environment."
18+
nil)
19+
20+
(defmacro with-sh-dir
21+
"Convenience macro for binding `*sh-dir*` for multiple `sh` invocations."
22+
[dir & body]
23+
`(binding [*sh-dir* ~dir]
24+
~@body))
25+
26+
(defmacro with-sh-env
27+
"Convenience macro for binding `*sh-env*` for multiple `sh` invocations."
28+
[env-map & body]
29+
`(binding [*sh-env* ~env-map]
30+
~@body))
31+
32+
(defn sh
33+
"Execute a shell command as a subprocess of the current process.
34+
35+
Commands are specified as a series of string arguments split on whitespace:
36+
37+
(sh \"ls\" \"-la\")
38+
39+
Following the command, 0 or more keyword/value pairs may be specified to
40+
control input and output options to the subprocess. The options are:
41+
42+
- :in - a string, byte string, byte array, file descriptor, or file
43+
object
44+
- :in-enc - a string value matching one of Python's supported encodings;
45+
if the value of `:in` is a string, decode that string to bytes
46+
using the encoding named here; if none is specified, `utf-8`
47+
will be used; if the value of `:in` is not a string, this value
48+
will be ignored
49+
- :out-enc - a string value matching on of Python's supported encodings or
50+
the special value `:bytes`; if specified as a string, decode the
51+
standard out and standard error streams returned by the subprocess
52+
using this encoding; if specified as `:bytes`, return the byte
53+
string from the output without encoding; if none is specified,
54+
`utf-8` will be used
55+
- :env - a mapping of string values to string values which are used as the
56+
subprocess's environment; if none is specified and `*sh-env` is
57+
not set, the environment of the current process will be used
58+
- :dir - a string indicating the working directory which is to be used for
59+
the subprocess; if none is specified and `*sh-dir*` is not set,
60+
the working directory of the current process will be used"
61+
[& args]
62+
(let [[cmd arg-seq] (split-with string? args)
63+
sh-args (apply hash-map arg-seq)
64+
out-enc (:out-enc sh-args "utf-8")
65+
[input stdin] (when-let [input-val (:in sh-args)]
66+
(cond
67+
(string? input-val)
68+
[(.encode input-val (:in-enc sh-args "utf-8")) nil]
69+
70+
(or (bytes? input-val)
71+
(byte-string? input-val))
72+
[input-val nil]
73+
74+
:else
75+
[nil input-val]))
76+
77+
;; subprocess.run completely barfs if you even supply the stdin
78+
;; kwarg at the same time as the input kwarg, so we have to do
79+
;; this nonsense to avoid sending them both in
80+
opts (cond-> {:cwd (:dir sh-args *sh-dir*)
81+
:env (:env sh-args *sh-env*)
82+
:stdout subprocess/PIPE
83+
:stderr subprocess/PIPE}
84+
input (assoc :input input)
85+
stdin (assoc :stdin stdin))
86+
result (apply-kw subprocess/run (python/list cmd) opts)]
87+
{:exit (.-returncode result)
88+
:out (cond-> (.-stdout result)
89+
(not= out-enc :bytes) (.decode out-enc))
90+
:err (cond-> (.-stderr result)
91+
(not= out-enc :bytes) (.decode out-enc))}))

0 commit comments

Comments
 (0)