Skip to content

Commit d0d7cb6

Browse files
authored
Collect CLI arguments sent to basilisp run or Basilisp scripts (#781)
Fixes #779
1 parent 46777e9 commit d0d7cb6

File tree

4 files changed

+84
-7
lines changed

4 files changed

+84
-7
lines changed

CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77
## [Unreleased]
88
### Added
99
* Added support for passing through `:tag` metadata to the generated Python AST (#354)
10+
* Added support for calling symbols as functions on maps and sets (#775)
11+
* Added support for passing command line arguments to Basilisp (#779)
1012

1113
### Changed
1214
* Optimize calls to Python's `operator` module into their corresponding native operators (#754)
1315
* Allow vars to be callable to adhere to Clojure conventions (#767)
14-
* Support symbols as fns for sets and maps (#775)
1516

1617
### Fixed
1718
* Fix issue with `(count nil)` throwing an exception (#759)

src/basilisp/cli.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
from basilisp.lang import reader as reader
1414
from basilisp.lang import runtime as runtime
1515
from basilisp.lang import symbol as sym
16+
from basilisp.lang import vector as vec
1617
from basilisp.prompt import get_prompter
1718

1819
CLI_INPUT_FILE_PATH = "<CLI Input>"
@@ -417,6 +418,11 @@ def run(
417418
with runtime.ns_bindings(args.in_ns) as ns:
418419
ns.refer_all(core_ns)
419420

421+
if args.args:
422+
cli_args_var = core_ns.find(sym.symbol(runtime.COMMAND_LINE_ARGS_VAR_NAME))
423+
assert cli_args_var is not None
424+
cli_args_var.bind_root(vec.vector(args.args))
425+
420426
if args.code:
421427
eval_str(args.file_or_code, ctx, ns, eof)
422428
elif args.file_or_code == STDIN_FILE_NAME:
@@ -445,6 +451,11 @@ def _add_run_subcommand(parser: argparse.ArgumentParser):
445451
parser.add_argument(
446452
"--in-ns", default=runtime.REPL_DEFAULT_NS, help="namespace to use for the code"
447453
)
454+
parser.add_argument(
455+
"args",
456+
nargs=argparse.REMAINDER,
457+
help="command line args made accessible to the script as basilisp.core/*command-line-args*",
458+
)
448459
_add_compiler_arg_group(parser)
449460
_add_debug_arg_group(parser)
450461

@@ -490,7 +501,12 @@ def run_script():
490501
The current process is replaced as by `os.execlp`."""
491502
# os.exec* functions do not perform shell expansion, so we must do so manually.
492503
script_path = Path(sys.argv[1]).resolve()
493-
os.execlp("basilisp", "basilisp", "run", script_path)
504+
args = ["basilisp", "run", str(script_path)]
505+
# Collect arguments sent to the script and pass them onto `basilisp run`
506+
if rest := sys.argv[2:]:
507+
args.append("--")
508+
args.extend(rest)
509+
os.execvp("basilisp", args)
494510

495511

496512
def invoke_cli(args: Optional[Sequence[str]] = None) -> None:

src/basilisp/lang/runtime.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@
8282

8383
# Public basilisp.core symbol names
8484
COMPILER_OPTIONS_VAR_NAME = "*compiler-options*"
85+
COMMAND_LINE_ARGS_VAR_NAME = "*command-line-args*"
8586
DEFAULT_READER_FEATURES_VAR_NAME = "*default-reader-features*"
8687
GENERATED_PYTHON_VAR_NAME = "*generated-python*"
8788
PRINT_GENERATED_PY_VAR_NAME = "*print-generated-python*"
@@ -2083,6 +2084,26 @@ def in_ns(s: sym.Symbol):
20832084
dynamic=True,
20842085
)
20852086

2087+
# Dynamic Var containing command line arguments passed via `basilisp run`
2088+
Var.intern(
2089+
CORE_NS_SYM,
2090+
sym.symbol(COMMAND_LINE_ARGS_VAR_NAME),
2091+
None,
2092+
dynamic=True,
2093+
meta=lmap.map(
2094+
{
2095+
_DOC_META_KEY: (
2096+
"A vector of command line arguments if this process was started "
2097+
"with command line arguments as by ``basilisp run {file_or_code}`` "
2098+
"or ``nil`` otherwise.\n\n"
2099+
"Note that this value will differ from ``sys.argv`` since it will "
2100+
"not include the command line arguments consumed by Basilisp's "
2101+
"own CLI."
2102+
)
2103+
}
2104+
),
2105+
)
2106+
20862107
# Dynamic Var for introspecting the default reader featureset
20872108
Var.intern(
20882109
CORE_NS_SYM,

tests/basilisp/cli_test.py

Lines changed: 44 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
import tempfile
99
import time
1010
from threading import Thread
11-
from typing import Optional, Sequence
11+
from typing import List, Optional, Sequence
1212
from unittest.mock import patch
1313

1414
import attr
@@ -164,20 +164,50 @@ def test_other_exception(self, run_cli):
164164

165165

166166
class TestRun:
167+
cli_args_params = [
168+
([], "0\n"),
169+
(["--"], "0\n"),
170+
(["1", "2", "3"], "6\n"),
171+
(["--", "1", "2", "3"], "6\n"),
172+
]
173+
cli_args_code = "(println (apply + (map int *command-line-args*)))"
174+
167175
def test_run_code(self, run_cli):
168176
result = run_cli(["run", "-c", "(println (+ 1 2))"])
169177
assert "3\n" == result.lisp_out
170178

179+
@pytest.mark.parametrize("args,ret", cli_args_params)
180+
def test_run_code_with_args(self, run_cli, args: List[str], ret: str):
181+
result = run_cli(["run", "-c", self.cli_args_code, *args])
182+
assert ret == result.lisp_out
183+
171184
def test_run_file(self, isolated_filesystem, run_cli):
172185
with open("test.lpy", mode="w") as f:
173186
f.write("(println (+ 1 2))")
174187
result = run_cli(["run", "test.lpy"])
175188
assert "3\n" == result.lisp_out
176189

190+
@pytest.mark.parametrize("args,ret", cli_args_params)
191+
def test_run_file_with_args(
192+
self, isolated_filesystem, run_cli, args: List[str], ret: str
193+
):
194+
with open("test.lpy", mode="w") as f:
195+
f.write(self.cli_args_code)
196+
result = run_cli(["run", "test.lpy", *args])
197+
assert ret == result.lisp_out
198+
177199
def test_run_stdin(self, run_cli):
178200
result = run_cli(["run", "-"], input="(println (+ 1 2))")
179201
assert "3\n" == result.lisp_out
180202

203+
@pytest.mark.parametrize("args,ret", cli_args_params)
204+
def test_run_stdin_with_args(self, run_cli, args: List[str], ret: str):
205+
result = run_cli(
206+
["run", "-", *args],
207+
input=self.cli_args_code,
208+
)
209+
assert ret == result.lisp_out
210+
181211

182212
def test_version(run_cli):
183213
result = run_cli(["version"])
@@ -191,7 +221,14 @@ def test_version(run_cli):
191221
"so this doesn't work natively on Windows"
192222
),
193223
)
194-
def test_run_script(tmp_path: pathlib.Path):
224+
@pytest.mark.parametrize(
225+
"args,ret",
226+
[
227+
([], b"nil\n"),
228+
(["1", "hi", "yes"], b"[1 hi yes]\n"),
229+
],
230+
)
231+
def test_run_script(tmp_path: pathlib.Path, args: List[str], ret: bytes):
195232
script_path = tmp_path / "script.lpy"
196233
script_path.write_text(
197234
"\n".join(
@@ -200,10 +237,12 @@ def test_run_script(tmp_path: pathlib.Path):
200237
"(ns test-run-script-ns ",
201238
" (:import os))",
202239
"",
203-
'(println "Hello world from Basilisp!")',
240+
"(println *command-line-args*)",
204241
]
205242
)
206243
)
207244
script_path.chmod(script_path.stat().st_mode | stat.S_IEXEC)
208-
res = subprocess.run([script_path.resolve()], check=True, capture_output=True)
209-
assert res.stdout == b"Hello world from Basilisp!\n"
245+
res = subprocess.run(
246+
[script_path.resolve(), *args], check=True, capture_output=True
247+
)
248+
assert res.stdout == ret

0 commit comments

Comments
 (0)