When to use -M, -A or -X with Clojure CLI?

Viewed 1298

I was always using -A with clojure tools, but some warnings were saying that I should use -M instead, I found this doc, but haven't got to a conclusion when to use each yet.

2 Answers

The Clojure CLI is still evolving so, the -X option is relatively new, and the meaning of the -M option has changed in the same timeframe.

You can see the CLI versions and short release notes here: https://clojure.org/releases/tools

Up until the middle of 2020, you used -A for everything. -M simply ran the :main-opts -- it did not respect any classpath options or resolving options.

In version 1.10.1.697 (September 25, 2020), the -X option was introduced to allow for execution of a specific Clojure function, passing a hash map of data as the single argument. That release also expanded the behavior of the -M option to respect :extra-paths and :extra-deps as well as running :main-opts -- effectively making -M equivalent to -A.

There were quite a few changes to how those options worked and how the CLI overall behaved, until things settled down about a month later with 1.10.1.727 (October 21, 2020). During this time, the -A option's behavior of running :main-opts was effectively deprecated: if you use -A to run :main-opts now, you'll get a warning that you should use -M instead.

Several community tools based on the Clojure CLI and deps.edn have notes in their README that you need to use at least version 1.10.1.727 in order to leverage their features. Version 1.10.3.814 is the current version (as of March 16, 2021). It's worth staying up to date as the CLI adds new features (and it's likely to get another round of changes soon).

The TL;DR of all this is:

  • Use -M to run clojure.main and any :main-opts -- this includes -m to identify a namespace whose -main function should be executed and also -e to evaluate an expression. Note that -main is a variadic function that is invoked with zero or more String arguments.
  • Use -X to run a specific function, taking a single hash map as its argument, passing in key/value pairs via the command-line, in addition to the :exec-args in deps.edn. Note that -X accepts EDN values, which means that strings need to be quoted carefully on the command-line: '"A string"' -- the double-quotes are for EDN strings and the single-quotes are to ensure the value is passed as-is via the shell. On Windows, via cmd.exe or Powershell, quoting is more complex than this (it is much easier to use the Clojure CLI on Linux and macOS, so for Windows it is worth considering WSL2).
  • Use -A to start a REPL. For now, that means you need to be careful about mixing :main-opts with aliases that you use when starting a REPL (because -A still runs :main-opts but at some point it will stop doing that).

Another useful option these days is -P -- prepare -- which you can add immediately after clojure (before -X, -M, or -A), which lets the CLI calculate and resolve all of the dependencies from the deps.edn files and the provided aliases (and download Maven/Clojars JARs and clone git deps), but then stops prior to actual execution of a function (the specific function for -X, clojure.main for -M, or the REPL for -A).

A final note on versions: although the CLI version number is prefixed with a Clojure version number, they are not directly tied. You can run any version of Clojure (back to 1.0.0) with any version of the CLI. The version number for the CLI is x.y.z.commits where x.y.z is the default version of Clojure you will get if you don't override it via an alias or via your project's deps.edn file.

The --help text clarifies things a bit:

> clojure --help
Version: 1.10.3.814

You use the Clojure tools ('clj' or 'clojure') to run Clojure programs
on the JVM, e.g. to start a REPL or invoke a specific function with data.
The Clojure tools will configure the JVM process by defining a classpath
(of desired libraries), an execution environment (JVM options) and
specifying a main class and args. 

Using a deps.edn file (or files), you tell Clojure where your source code
resides and what libraries you need. Clojure will then calculate the full
set of required libraries and a classpath, caching expensive parts of this
process for better performance.

The internal steps of the Clojure tools, as well as the Clojure functions
you intend to run, are parameterized by data structures, often maps. Shell
command lines are not optimized for passing nested data, so instead you
will put the data structures in your deps.edn file and refer to them on the
command line via 'aliases' - keywords that name data structures.

'clj' and 'clojure' differ in that 'clj' has extra support for use as a REPL
in a terminal, and should be preferred unless you don't want that support,
then use 'clojure'.

Usage:
  Start a REPL   clj     [clj-opt*] [-Aaliases] [init-opt*]
  Exec function  clojure [clj-opt*] -X[aliases] [a/fn] [kpath v]*
  Run main       clojure [clj-opt*] -M[aliases] [init-opt*] [main-opt] [arg*]
  Prepare        clojure [clj-opt*] -P [other exec opts]

exec-opts:
 -Aaliases      Use concatenated aliases to modify classpath
 -X[aliases]    Use concatenated aliases to modify classpath or supply exec fn/args
 -M[aliases]    Use concatenated aliases to modify classpath or supply main opts
 -P             Prepare deps - download libs, cache classpath, but don't exec

clj-opts:
 -Jopt          Pass opt through in java_opts, ex: -J-Xmx512m
 -Sdeps EDN     Deps data to use as the last deps file to be merged
 -Spath         Compute classpath and echo to stdout only
 -Spom          Generate (or update) pom.xml with deps and paths
 -Stree         Print dependency tree
 -Scp CP        Do NOT compute or cache classpath, use this one instead
 -Srepro        Ignore the ~/.clojure/deps.edn config file
 -Sforce        Force recomputation of the classpath (don't use the cache)
 -Sverbose      Print important path info to console
 -Sdescribe     Print environment and command parsing info as data
 -Sthreads      Set specific number of download threads
 -Strace        Write a trace.edn file that traces deps expansion
 --             Stop parsing dep options and pass remaining arguments to clojure.main
 --version      Print the version to stdout and exit
 -version       Print the version to stderr and exit

init-opt:
 -i, --init path     Load a file or resource
 -e, --eval string   Eval exprs in string; print non-nil values
 --report target     Report uncaught exception to "file" (default), "stderr", or "none"

main-opt:
 -m, --main ns-name  Call the -main function from namespace w/args
 -r, --repl          Run a repl
 path                Run a script from a file or resource
 -                   Run a script from standard input
 -h, -?, --help      Print this help message and exit

Programs provided by :deps alias:
 -X:deps mvn-install       Install a maven jar to the local repository cache
 -X:deps git-resolve-tags  Resolve git coord tags to shas and update deps.edn

For more info, see:
 https://clojure.org/guides/deps_and_cli
 https://clojure.org/reference/repl_and_main

So -A (for "alias") is a catch-all that is legal substitute for any of the more specialized flags. I usually use -A so I don't have to remember which other ones serve which purpose.


You should also see this guide on Deps/CLI.

Related