diff --git a/CHANGELOG.md b/CHANGELOG.md index 7b3743851..c77f75a2d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [v0.1.1] ### Added * Added several missing functions to `basilisp.core` (#956) + * Added CLI argument parser in `basilisp.contrib.cli-tools` namespace (#535) ### Fixed * Fixed an issue where attempting to run a namespace from the CLI could fail in certain cases (#957) diff --git a/docs/api/contrib/cli-tools.rst b/docs/api/contrib/cli-tools.rst new file mode 100644 index 000000000..202d2eb3a --- /dev/null +++ b/docs/api/contrib/cli-tools.rst @@ -0,0 +1,10 @@ +basilisp.contrib.cli-tools +========================== + +.. toctree:: + :maxdepth: 2 + :caption: Contents: + +.. autonamespace:: basilisp.contrib.cli-tools + :members: + :undoc-members: \ No newline at end of file diff --git a/src/basilisp/contrib/cli_tools.lpy b/src/basilisp/contrib/cli_tools.lpy new file mode 100644 index 000000000..e6176ac82 --- /dev/null +++ b/src/basilisp/contrib/cli_tools.lpy @@ -0,0 +1,547 @@ +(ns basilisp.contrib.cli-tools + (:import + argparse + functools + os + sys)) + +(defn ^:private wrapped-namespace + "Wrap an ``argparse.Namespace`` object from the argument parser. + + ``argparse`` uses Namespace objects to collect arguments parsed from the command + line. These objects are just bags of attributes, so we wrap them with an + implementation of ``IPersistentMap`` to make them easier to integrate with idiomatic + Basilisp code." + [ns] + (reify + basilisp.lang.interfaces/IPersistentMap + (assoc [this & kvs] + (doseq [kv (partition 2 kvs) + :let [[k v] kv]] + (python/setattr ns (name k) v)) + this) + (cons [this & elems] + (doseq [elem elems] + (cond + (nil? elem) + nil + + (map? elem) + (doseq [entry (seq elem) + :let [[k v] entry]] + (python/setattr ns (name k) v)) + + (map-entry? elem) + (python/setattr ns (name (key elem)) (val elem)) + + (vector? elem) + (python/setattr ns (name (nth elem 0)) (nth elem 1)) + + (py-dict? elem) + (doseq [entry (.items elem) + :let [[k v] entry]] + (python/setattr ns (name k) v)) + + :else + (throw + (ex-info "Argument to namespace conj must be another Map or castable to MapEntry" + {:value elem + :type (python/type elem)})))) + this) + (contains [this key] + (python/hasattr ns (name key))) + (dissoc [this & ks] + (doseq [k ks] + (when (python/hasattr ns (name k)) + (python/delattr ns (name k)))) + this) + (empty [this] + (throw (python/TypeError "Cannot create empty Namespace"))) + (entry [this key] + (when (python/hasattr ns (name key)) + (map-entry key (python/getattr ns (name key))))) + (val-at [this key & args] + (let [[default] args] + (python/getattr ns (name key) default))) + (seq [this] + (->> (python/vars ns) + (.items) + (map (fn [[k v]] (map-entry (keyword k) v))))) + (__getitem__ [this key] + (python/getattr this (name key))) + (__iter__ [this] + (python/iter (python/vars this))) + (__len__ [this] + (python/len (python/vars this))))) + +(defn ^:private argument-parser-kwargs + [config] + {:prog (:command config) + :description (:description config) + :usage (:usage config) + :epilog (:epilogue config) + :add-help (:include-help config true) + :allow-abbrev (:allow-abbrev config true) + :exit-on-error (:exit-on-error config true)}) + +(defn ^:private argument-subparser-kwargs + [{:keys [aliases] :as config}] + (cond-> {:help (:help config) + :description (:description config) + :usage (:usage config) + :epilog (:epilogue config) + :add-help (:include-help config true) + :allow-abbrev (:allow-abbrev config true) + :exit-on-error (:exit-on-error config true)} + aliases (assoc :aliases aliases))) + +(declare ^:private setup-parser) + +(defn ^:private add-cli-sub-parser + "Add a new command parser to the subparsers object." + [subparsers {cmd-name :command :as command}] + (let [command-parser (->> (argument-subparser-kwargs command) + (apply-method-kw subparsers add-parser cmd-name))] + (setup-parser command-parser command) + nil)) + +(def ^:private action-default-attrs + "Actions are required to have a number of different argument defaults which are + not all provided to the action constructor. Failing to respond to these values + with a meaningful value causes ``argparse`` to throw exceptions in certain cases." + (->> (argparse/Action [] "dest") + (python/vars) + (.items) + (remove #{"dest"}) + (mapcat (fn [[k v]] [(keyword (demunge k)) v])) + (apply hash-map))) + +;; Deep inside of argparse, they create actions using the `action_class(**kwargs)` +;; syntax, so collect all arguments into keyword arguments is the safest option +;; for faking an Action class. + +(defn ^:private cli-tools-assoc-action + "Create a new CLI Tools Assoc Action. + + Add actions allow users to define actions with :lpy:fn:`assoc`-like semantics on the + parsed options map. + + The provided ``assoc-fn`` will be passed the entire options map, the relevant + destination key, and the parsed option and should return a new options map." + [assoc-fn] + (fn ^{:kwargs :collect} AssocAction [{:keys [dest] :as kwargs}] + (let [kwargs (merge action-default-attrs kwargs)] + (reify + ^:abstract argparse/Action + (^{:kwargs :collect} __call__ [self _ namespace values _ _] + (let [lisp-val (py->lisp values)] + (-> (wrapped-namespace namespace) + (assoc-fn dest lisp-val)))) + (__getattr__ [self name] + (let [k (keyword (demunge name))] + (if (contains? kwargs k) + (get kwargs k) + (throw (python/AttributeError + (str "UpdateAction does not have attribute " name)))))))))) + +(defn ^:private cli-tools-update-action + "Create a new CLI Tools Update Action. + + Update actions allow users to define actions which update the current value + stored in the options map. + + The provided ``update-fn`` will be passed the current value of the given option + (or ``nil`` if no default is provided) and should return a new value for the + option." + [update-fn] + (fn ^{:kwargs :collect} UpdateAction [{:keys [dest] :as kwargs}] + (let [kwargs (merge action-default-attrs kwargs)] + (reify + ^:abstract argparse/Action + (^{:kwargs :collect} __call__ [self _ namespace _ _ _] + (-> (wrapped-namespace namespace) + (update dest update-fn))) + (__getattr__ [self name] + (let [k (keyword (demunge name))] + (if (contains? kwargs k) + (get kwargs k) + (throw (python/AttributeError + (str "UpdateAction does not have attribute " name)))))))))) + +(defn ^:private cli-tools-action + "Return a new Action for the argument definition. + + Users may provide at most one of ``:assoc-fn`` or ``:update-fn``. If both are given, + an exception will be thrown. If neither is provided, a default assoc action will + be returned using :lpy:fn:`assoc` as the function." + [{:keys [assoc-fn update-fn]}] + (if update-fn + (cli-tools-update-action update-fn) + (cli-tools-assoc-action (or assoc-fn assoc)))) + +(defn ^:private validate-argument + [{:keys [name flags dest nargs assoc-fn update-fn] :as argument}] + (when (and name flags) + (throw + (ex-info (str "Arguments may either be positional (via :name) or " + "optional (via :flags), not both") + {:name name + :flags flags}))) + (when (and name dest) + (throw + (ex-info (str "Arguments may either provide a :name or :dest, " + "not both") + {:name name + :dest dest}))) + (when update-fn + (when assoc-fn + (throw + (ex-info (str "Arguments may only specify either an :assoc-fn or " + "an :update-fn, not both") + {:assoc-fn assoc-fn + :update-fn update-fn}))) + (when nargs + (throw + (ex-info (str "Arguments may only specify either an :update-fn or " + ":nargs, not both") + {:assoc-fn assoc-fn + :update-fn update-fn}))))) + +(defn ^:private add-argument + [parser argument] + (validate-argument argument) + (let [;; ArgumentParser has a "type" function which can convert the string value + ;; from the CLI into its final value. We decompose that into a parse and + ;; validate step in the argument config and re-compose those steps here. + validate-fn (when-let [[f msg] (:validate argument)] + (functools/update-wrapper + (fn [v] + (if-not (f v) + (throw + (argparse/ArgumentTypeError msg)) + v)) + f)) + type-fn (if-let [parse-fn (:parse-fn argument)] + (if validate-fn + (-> (comp validate-fn parse-fn) + (functools/update-wrapper parse-fn)) + parse-fn) + validate-fn) + + ;; Prepare the keyword arguments for ArgumentParser.add_argument + kwargs (cond-> {:default (or (some-> (:env argument) (os/getenv)) + (:default argument)) + :const true + :action (cli-tools-action argument) + :nargs (if-let [nargs (:nargs argument)] + (cond-> nargs (keyword? nargs) (name)) + (if (:update-fn argument) 0 "?")) + :type type-fn + :metavar (when-let [metavar (:metavar argument)] + (cond-> metavar + (vector? metavar) (python/tuple))) + :help (:help argument)} + (:dest argument) (assoc :dest (:dest argument))) + + ;; Python's ArgumentParser.add_argument method takes variadic positional + ;; arguments _and_ keyword arguments, which is a challenging combination + ;; to call via Basilisp, so we create a partial of the method with the + ;; kwargs then (apply method args) for the potentially variadic positionals. + method (partial-kw (.-add-argument parser) kwargs) + name (:name argument) + flags (:flags argument)] + (if name + (method name) + (apply method flags)) + nil)) + +(def ^:dynamic *create-default-handler* + "A function of one argument expecting an ``argparse/ArgumentParser`` instance + which returns a function of one argument which will receive the parsed arguments + from the CLI. + + Override this to control how default handlers are generated." + (fn [parser] + (fn [_] + (.print-help parser) + (sys/exit 1)))) + +(defn ^:private setup-parser + "Set up the command parser from the configuration map." + [parser config] + (when-let [commands (:commands config)] + (let [subparsers (.add-subparsers parser ** :title "subcommands")] + (doseq [command commands] + (add-cli-sub-parser subparsers command)))) + ;; Add arguments to the parser. Arguments can be grouped as exclusive groups + ;; or as display groups (without mutual exclusion). + (when-let [arguments (:arguments config)] + (let [groups (->> arguments + (map #(if (and (contains? % :exclusive-group) + (contains? % :group)) + (throw + (ex-info (str "Arguments may be in either a :group " + "or an :exlusive-group, not both.") + {:arg %})) + %)) + (group-by (fn [v] + (condp #(contains? %2 %1) v + :exclusive-group :ex-group + :group :group + :normal))))] + ;; Mutually-exclusive argument groups + (let [ex-groups (group-by :exclusive-group (:ex-group groups))] + (doseq [ex-group-pair ex-groups] + (let [[_ ex-group] ex-group-pair + ex-grouper (.add-mutually-exclusive-group parser)] + (doseq [argument ex-group] + (add-argument ex-grouper argument))))) + ;; Argument groups + (let [display-groups (group-by :group (:group groups))] + (doseq [display-group-pair display-groups] + (let [[title group] display-group-pair + grouper (.add-argument-group parser (name title))] + (doseq [argument group] + (add-argument grouper argument))))) + ;; All other arguments + (doseq [argument (:normal groups)] + (add-argument parser argument)))) + ;; Configure either the given handler or a default handler. + (let [handler (or (:handler config) + (*create-default-handler* parser))] + (.set-defaults parser ** :handler handler)) + nil) + +;; Config map spec +(comment + {;; Primary CLI entrypoint configuration + + ;; Name the command as it appears in the help text. If nil is provided, + ;; `sys.argv[0]` is used. Default nil. + :command "command" + + ;; The description which appears immediately under the usage text. Optional. + :description "This command foos the bars." + + ;; A function of one argument which receives the parsed arguments as a map + ;; and which can direct further action based on the arguments. Handlers + ;; can be defined on the top-level command and on any subcommands. If no + ;; handler is defined, a default one will be provided which prints the help + ;; text for the command or subcommand and exits the program with a non-zero + ;; error code. + :handler identity + + ;; The usage message emitted when invalid arguments are provided or when + ;; printing the help text. If nil is provided, the usage message will be + ;; calculated automatically by the arguments and commands provided. + :usage "%(prog)s [options]" + + ;; The text which appears beneath the generated help message. Optional. + :epilogue "" + + ;; If true, include the `-h/--help` commands by default. Default true. + :include-help true + + ;; If true, allow abbreviations for unambiguous partial flags. Default true. + :allow-abbrev true + + ;; Arguments which apply globally. Default nil. + :arguments [{;; The name of a positional argument. Mutually exclusive with + ;; the `:flags` key. + :name "foo" + + ;; The flags for an optional argument. Mutually exclusive with + ;; the `:name` key. + :flags ["-f" "--foo"] + + :nargs nil + + ;; A function of one string argument which returns the intended + ;; final value. Default nil. + :parse-fn int + + ;; A vector of two elements. The first element is a function + ;; which can accept the value from `:parse-fn` (if given) or the + ;; string value from the CLI (if `:parse-fn` is not given). If + ;; the function returns a falsey value, the value will be + ;; rejected. Default nil. + :validate [pos? "foo must be positive"] + + ;; A default value which will be supplied if the flag is not + ;; given. If the value is a string, it will be subject to the + ;; `:parse-fn` and `:validate` rules, if given. Non-string + ;; values will be used as-is. Default nil. + :default "yes" + + ;; The name of an environment variable from which to draw the + ;; default value if that environment variable exists. If the + ;; named environment variable is defined, its value will be + ;; subject to the same rules as `:default` above. This key + ;; always supersedes `:default` if it is defined. Default nil. + :env "SOME_ENV_VAR" + + ;; An `assoc`-like function which will be passed the current + ;; options map, the relevant destination key, and the parsed + ;; option and should return a new options map. + ;; + ;; If neither `:assoc-fn`, nor `:update-fn` are provided, then + ;; a default of `assoc` will be given for `:assoc-fn`. If + ;; `:update-fn` is provided, it will be used. If both keys are + ;; given, an exception will be thrown. + :assoc-fn assoc + + ;; A function of one argument which will receive the current + ;; value of the associated option and must return an updated + ;; value. Default nil. + :update-fn (fnil inc 0) + + ;; Either a string value or vector of strings used to refer to + ;; expected arguments generated help text message. Optional. + :metavar "FOO" + + ;; Arguments may be specified to be part of an exclusive group, + ;; in which case only one argument of the group will be permitted. + ;; Arguments may be in either an exclusive group or a display + ;; group, but not both. Default nil. + :exclusive-group :baz + + ;; Arguments may be grouped into display groups which do not enforce + ;; mutual exclusion. These groups will only be used for help text. + ;; Arguments may be in either an exclusive group or a display + ;; group, but not both. Default nil. + :group :bar + + ;; A help string which will be shown next to the argument in the + ;; CLI help text. Default nil. + :help "Add extra foos into the bars."}] + + ;; Subcommands which will be added to the parser. Default nil. + :commands [{;; The name of the command as it should appear in the CLI. Required. + :command "do-foo" + + ;; A vector of valid aliases which will be accepted for the + ;; subcommand. Default []. + :aliases ["foo" "df"] + + ;; The description which appears immediately under the usage text + ;; for this subcommand's help text. Optional. + :description "This command does the foo." + + ;; The short help text which appears beside this subcommand on + ;; the parent command's help text. Optional. + :help "Does the foo" + + ;; A function of one argument with the same semantics as described + ;; in the top-level command `:handler` key. + :handler identity + + ;; The usage message emitted when invalid arguments are provided or + ;; when printing the help text. If nil is provided, the usage + ;; message will be calculated automatically by the arguments and + ;; commands provided. + :usage "%(prog)s [options]" + + ;; The text which appears beneath the generated help message. + ;; Optional. + :epilogue "" + + ;; If true, include the `-h/--help` commands by default. Default + ;; true. + :include-help true + + ;; If true, allow abbreviations for unambiguous partial flags. + ;; Default true. + :allow-abbrev true + + ;; Arguments which apply to this specific subcommand. Default nil. + :arguments [] + + ;; Subcommands which will be added to the parser for this + ;; subcommand (which can have it's own subcommands and so on...). + ;; Default nil. + :commands []}]}) + +(defn file-type + "Return a type function for a file-typed argument. The ``mode`` must be specified + and must be one of the allowed modes for Python's ``open`` builtin. + + If the user supplies the value '-', this function intelligently opens either + :lpy:var:`*in*` for read-mode files or :lpy:var:`*out*` for write-mode files. + + This function also supports the ``bufsize``, ``encoding``, and ``errors`` options + of ``open``." + ([mode] + (argparse/FileType mode)) + ([mode & {:as opts}] + (apply-kw argparse/FileType mode opts))) + +(defn ^:private cli-parser? + [o] + (instance? argparse/ArgumentParser o)) + +(defn cli-parser + "Create a CLI argument parser from the configuration map. + + Configuration maps have the following spec" + [parser-or-config] + (if (cli-parser? parser-or-config) + parser-or-config + (doto (->> parser-or-config + (argument-parser-kwargs) + (apply-kw argparse/ArgumentParser)) + (setup-parser parser-or-config)))) + +(defn ^:private args->map + "Transform an ``argparse.Namespace`` into a map." + [args] + (->> args + (python/vars) + (.items) + (into {} (map (fn [[k v]] [(keyword (demunge k)) v]))))) + +(defn parse-args + "Parse command line arguments using the supplied parser or CLI tools configuration + map. If no ``args`` are given, parse the arguments from :lpy:var:`*command-line-args*`." + ([parser-or-config] + (parse-args parser-or-config *command-line-args*)) + ([parser-or-config args] + (-> (cli-parser parser-or-config) + (.parse-args args) + (args->map)))) + +(defn parse-known-args + "Parse known command line arguments using the supplied parser or CLI tools + configuration map. + + Returns a vector of ``[parsed-args remainder]`` where ``parsed-args`` is a map of + parsed arguments and ``remainder`` is a vector of unrecognized arguments. + + If no ``args`` are given, parse the arguments from :lpy:var:`*command-line-args*`." + ([parser-or-config] + (parse-known-args parser-or-config *command-line-args*)) + ([parser-or-config args] + (let [[parsed remainder] (-> (cli-parser parser-or-config) + (.parse-known-args args))] + [(args->map parsed) (vec remainder)]))) + +(defn ^:private execute-handler + "Execute the handler defined in the arguments. + + If no handler is defined, print the help for the parser and exit with error code 1." + [{:keys [handler] :as parsed-args}] + (handler parsed-args)) + +(defn handle-args + "Parse command line arguments using the supplied parser or CLI tools configuration + map (as by :lpy:fn:`parse-args`) and execute any command or sub-command handlers for the + parsed arguments. + + If no ``args`` are given, parse the arguments from :lpy:var:`*command-line-args*`." + ([parser-or-config] + (-> (cli-parser parser-or-config) + (parse-args) + (execute-handler))) + ([parser-or-config args] + (->(cli-parser parser-or-config) + (parse-args args) + (execute-handler)))) diff --git a/src/basilisp/lang/compiler/generator.py b/src/basilisp/lang/compiler/generator.py index fc528f4d1..ba349f848 100644 --- a/src/basilisp/lang/compiler/generator.py +++ b/src/basilisp/lang/compiler/generator.py @@ -2717,7 +2717,7 @@ def _reify_to_py_ast( ), verified_abstract=node.verified_abstract, artificially_abstract_bases=artificially_abstract_bases, - is_frozen=True, + is_frozen=False, use_slots=True, use_weakref_slot=node.use_weakref_slot, ) diff --git a/tests/basilisp/contrib/test_cli_tools.lpy b/tests/basilisp/contrib/test_cli_tools.lpy new file mode 100644 index 000000000..efeb706de --- /dev/null +++ b/tests/basilisp/contrib/test_cli_tools.lpy @@ -0,0 +1,154 @@ +(ns tests.basilisp.contrib.test-cli-tools + (:require + [basilisp.contrib.cli-tools :as cli] + [basilisp.test :refer [deftest are is testing]]) + (:import argparse)) + +(def ^:private base-parser-config + {:command "command" + :description "This command foos the bars." + :allow-abbrev true + :arguments [] + :commands []}) + +(def ^:private base-arg-config + {:name "foo" + :parse-fn int + :validate [pos? "foo must be positive"] + :default "yes" + :metavar "FOO" + :help "Add extra foos into the bars."}) + +(def ^:private base-flag-config + {:flags ["-f" "--foo"] + :parse-fn int + :validate [pos? "foo must be positive"] + :default "yes" + :metavar "FOO" + :help "Add extra foos into the bars."}) + +(deftest cli-parser-spec-validation + (testing "disallow name and flags" + (is (thrown? basilisp.lang.exception/ExceptionInfo + (-> base-arg-config + (assoc :flags ["-f" "--foo"]) + (->> (update base-parser-config :arguments conj)) + (cli/cli-parser))))) + + (testing "disallow name and dest" + (is (thrown? basilisp.lang.exception/ExceptionInfo + (-> base-arg-config + (assoc :dest "dest") + (->> (update base-parser-config :arguments conj)) + (cli/cli-parser))))) + + (testing "disallow assoc-fn and update-fn" + (is (thrown? basilisp.lang.exception/ExceptionInfo + (-> base-arg-config + (assoc :assoc-fn assoc :update-fn (fnil inc 0)) + (->> (update base-parser-config :arguments conj)) + (cli/cli-parser))))) + + (testing "disallow exclusive-group and group" + (is (thrown? basilisp.lang.exception/ExceptionInfo + (-> base-arg-config + (assoc :group :foos :exclusive-group :bars) + (->> (update base-parser-config :arguments conj)) + (cli/cli-parser)))))) + +(defn parse-args + [parser args] + (try + (cli/parse-args parser args) + (catch python/SystemExit _ nil))) + +(deftest cli-parser + (let [parser (cli/cli-parser + {:command "basilisp" + :description "Basilisp is a Lisp dialect inspired by Clojure targeting Python 3" + :exit-on-error false + :arguments [] + :commands [{:command "compile" + :description "Run multiple Basilisp scripts in parallel." + :help "run multiple Basilisp scripts in parallel" + :exit-on-error false + :handler identity + :arguments [{:dest "optimize" + :flags ["-o"] + :help "set the optimization level" + :default "standard" + :parse-fn keyword + :validate [#{:debug :standard :optimized} "must be one of: :debug, :standard, :optimized"]}]} + {:command "run" + :description "Run a Basilisp script from a file or run Basilisp code directly." + :help "run a Basilisp script or code" + :handler identity + :arguments [{:name "file-or-code" + :help "the filename or, if using -c the code, to execute"} + {:flags ["--in-ns"] + :help "namespace to run the code in" + :default "basilisp.user"} + {:flags ["-c" "--code"] + :help "if provided, treat argument as a string of code" + :default false + :update-fn (constantly true)} + {:flags ["--warn-on-shadowed-name"] + :help "if provided, emit warnings if a local name is shadowed by another local name" + :env "BASILISP_WARN_ON_SHADOWED_NAME" + :group "compiler flags"} + {:flags ["--warn-on-shadowed-var"] + :help "if provided, emit warnings if a Var name is shadowed by a local name" + :env "BASILISP_WARN_ON_SHADOWED_VAR" + :group "compiler flags"}]} + {:command "run-all" + :description "Run multiple Basilisp scripts in parallel." + :help "run multiple Basilisp scripts in parallel" + :exit-on-error false + :handler identity + :arguments [{:name "files" + :help "the filename or, if using -c the code, to execute" + :nargs "+"} + {:flags ["-p" "--parallel"] + :help "if provided, number of parallel workres" + :default 1 + :validate [pos? "must be positive"] + :parse-fn python/int} + {:flags ["-v" "--verbose"] + :help "increase verbosity level; may be specified multiple times" + :default 1 + :update-fn inc}]}]})] + (are [args] (thrown? argparse/ArgumentError (parse-args parser args)) + ["compile" "-o" "kinda"] + ["run-all" "-p" "-1" "script.lpy"] + ["run-all" "-p" "hi" "script.lpy"]) + + (are [args ret] (= ret (dissoc (parse-args parser args) :handler)) + [] {} + + ["compile" "-o" "debug"] {:optimize :debug} + + ["run" "--in-ns" "basilisp.contrib.cli-tools" "cli_tools.lpy"] {:file-or-code "cli_tools.lpy" + :code false + :in-ns "basilisp.contrib.cli-tools" + :warn-on-shadowed-var nil + :warn-on-shadowed-name nil} + ["run" "--warn-on-shadowed-name" "-c" "(identity 1)"] {:file-or-code "(identity 1)" + :code true + :in-ns "basilisp.user" + :warn-on-shadowed-var nil + :warn-on-shadowed-name true} + ["run" "--code" "(identity 1)"] {:file-or-code "(identity 1)" + :code true + :in-ns "basilisp.user" + :warn-on-shadowed-var nil + :warn-on-shadowed-name nil} + + ["run-all" "script.lpy"] {:files ["script.lpy"] + :parallel 1 + :verbose 1} + ["run-all" "-vvv" "script.lpy"] {:files ["script.lpy"] + :parallel 1 + :verbose 4} + ["run-all" "-p" "3" "script1.lpy", "script2.lpy" "script3.lpy"] {:files ["script1.lpy", "script2.lpy" "script3.lpy"] + :parallel 3 + :verbose 1})))