Clojure CLI, tools.deps and deps.edn guide
Note for readers
This article was written for those who want to understand how to work with Clojure CLI (command line interface), and how to configure it with deps.edn files. There are 2 official articles on this topic: Deps and CLI Guide and Deps and CLI Reference. These are both very helpful, but In my opinion, the first one is too brief to gain a good enough understanding of concepts, and the second one is too long for an introduction to the topic. So I tried to write something in between that gives the reader a deep enough explanation of how clj works, but without all the nitty-gritty details.
At the time of writing this, I used Clojure CLI version 1.10.3.855 on Mac.
Video version of this article
What is Clojure CLI?
Clojure CLI provides the tools to execute Clojure programs and manage their dependencies. To understand Clojure CLI, we should cover 3 main topics:
cljandclojureare executables that you invoke to run Clojure code.tools.depsis a library that works behind the scenes to manage dependencies and to create classpaths.deps.ednconfiguration files that you create to customize work ofclj/clojureandtools.deps.
Difference between clj and clojure executables
Both clj and clojure are scripts, and clj is just a wrapper on clojure.
Let’s find where clj is located:
|
|
And examine its content:
|
|
As you can see ,clj, behind the scenes, wraps a call to $bin_dir/clojure with the rlwrap tool. rlwrap provides a better command-line editing experience.
If you run clojure without arguments, REPL will be started. Try to type something in it, and press up/down/left/right keyboard keys.
|
|
You will notice that those keys don’t work properly.
But if you run clj instead, you will be able to use left/right keys to navigate the typed text, and up/down to navigate the calls history. This is exactly the function provided by rlwrap.
I will be using only clj later in the article.
Most usable clj options
-M and -X are the most important options, and the ones you need to learn first.
-M option to work with clojure.main namespace
Invoking clj with the -M option gives you access to functionality from the clojure.main namespace. All arguments after -M will be passed to the clojure.main/main and interpreted by it. To see all available options of clojure.main, you can run:
|
|
Or take a look at official documentation.
Running (-main) function from namespace
The most common usage of clj -M is to run the entry point of your clojure code. To do this, you should pass -m namespace-name options to the clojure.main. It will find the specified namespace, and invoke its (-main) function.
For example, if you have the following project directory structure:
project-dir (project directory)
└─ src (default sources directory)
└─ core.clj
With the core.clj file:
|
|
Running core/main from the project-dir directory looks like this:
|
|
Running clojure file as a script
clojure.main also allows running Clojure file as a script. To do this via CLI, you should use the command, clj -M /path/to/script/file.clj arg1 arg2 arg3 . An arbitrary number of arguments passed after script path will be available in a script under *command-line-args* var.
If you have a script.clj file:
|
|
Calling it will give you:
|
|
-X option to run specific functions
(-main) is not the only function you can run via CLI. You can run any other one using the -X option as long as this function takes a map as an argument. The command should look like this: clj -X namespace/fn [arg-key value]*
With file core.clj
|
|
If core.clj is located in your project-dir/src, you can call (print-args) using CLI from the project-dir folder:
|
|
Key-value pairs specified after the function name will be passed to the function as a map.
deps.edn configuration files
There are a few files with the name deps.edn. One in the clj installation itself. You can also have another one in the $HOME/.clojure folder to keep the common settings for all your projects. And, of course, you can create one in your project directory with project-specific settings. All of them store configuration settings in clojure maps. When clj is invoked, it merges them all to create a final configuration map. You can read more about locations of different deps.edn files in official documentation.
Later in this article, I will mostly talk about deps.edn that you create in a project directory.
The most important keys in the configuration map are :deps, :paths, and :aliases:.
:paths key
Under the :path key, you specify the vector of directories where source code is located.
If the deps.edn file doesn’t exist in your project folder or it doesn’t contain the:path key, clj uses the src folder by default.
For example, if you have the following directory structure:
project-dir
├─ src
│ └─ core.clj
└─ test
└─ test_runner.clj
With core.clj:
|
|
And test_runner.clj:
|
|
You can run something from core.clj because it is in the src folder:
|
|
But an attempt to run test-runner/run will fail. The test-runner namespace from thetest folder isn’t available:
|
|
To fix this, add the deps.edn file at the root of your project-dir, and put a vector of all source folders under the :paths key:
|
|
Now the content of the test folder is visible to clj:
|
|
Note, that you should specify both the src and test folders under the:paths key.
:deps key
Under the :deps key , you can place a map of external libraries that your project relies on. Libraries will be downloaded along with their dependencies, and become available for use.
Dependencies can be taken from the Maven repository, git repository, or local disk.
For Maven dependencies, you should specify their version. By default, two Maven repos are used for the search:
For Git dependencies, you should specify :git/url with the repo address, and the :git/sha or :git/tag keys to specify the library version.
Let’s declare deps.edn like this:
|
|
When clj is invoked, two libraries will be available in our code: timbre logging library which artifacts taken from Maven, and test-runner, taken from GitHub.
From core.clj timbre now can be used after importing its namespace:
|
|
And test-runner main function can be invoked by clj with already known -M switch:
|
|
More details on how to use local dependencies and meaning of different keys can be found in official documentation.
:aliases key
The “alias” is the main concept in deps.edn. It is where concentrates all convenience of clj tool. Let’s explore it with examples.
Aliases for clj -M
So far, we’ve been using clj with the -M option to run the (-main) function in a specified namespace. Let’s imagine that our project has two different entry namespaces with (-main) functions. One is used for development and one for production. Our project folder looks like this:
project-dir
└─ src
└─ dev
│ └─ core.clj
└─ prod
└─ core.clj
The command line for the dev build is:
|
|
And for prod:
|
|
To minimize typing, we can declare two different aliases in the deps.edn file, and store all options after clj -M under that aliases.
Here is the content of deps.edn with two declared aliases :dev and :prod. You can use any keywords as alias names.
|
|
To invoke an alias, you add its name right after the -M option. Now, running the dev build using an alias looks like this:
|
|
It’s similar for prod:
|
|
So, :aliases is a key in the deps.edn map where you store a map with user-defined aliases.
Every alias is a key-value pair, where the key is a user-defined name of the alias, and value is a map with pre-defined keys. In the example above, we used :main-opts, a pre-defined key that keeps a vector of options to be passed to the clojure.main namespace. When clj -M is invoked with an alias, it runs clojure.main with arguments taken from :main-opts.
Aliases for clj -X
We can also create aliases to run specific functions. They look pretty much the same as aliases from the example above, but rely on other pre-defined keys.
Let’s imagine you have a function for generating reports. It is located in the db.reports namespace, named generate. The only argument is a map with two possible keys: :settings for a map of settings, and :tables with a vector of tables for which we want to get reports. If the :tables key is absent, we generate reports for all tables.
Let’s make a stub for our reports.clj:
|
|
To run reports for all tables from the command line, we can invoke:
|
|
For orders and users tables:
|
|
Since typing all arguments in the command line is quite tedious, let’s create aliases in deps.edn:
|
|
Now you can generate reports more conveniently:
|
|
|
|
As you probably noticed, we don’t use the :main-opts’s pre-defined key anymore, because it works only with clj -M. Instead, we use the :exec-fn key to specify the namespace/function to run, and :exec-args to pass arguments map.
If you will try to run one of these aliases with clj -M, you will see a REPL started instead of the invoked function. This is because clj starts clojure.core when it sees the -M option, and since there is no :main-opts key, it won’t pass any arguments to it. And clojure.core invoked without arguments will simply start REPL.
|
|
There are pre-defined keys common to the -X and -M options, but we will discuss them later.
tools.deps library
When clj runs clojure programs, it runs a JVM process and needs to pass a classpath to it. (To read more about how Clojure works on top of JVM, you can check this article) Classpath is a list of all paths where java should look for classes used in your program, including classes for your dependencies. So to build a classpath, all dependencies should be resolved first. Both these tasks, resolving dependencies and creating a classpath, is done by thetools.deps library that goes with Clojure. clj calls tools.deps internally.
Two main functions in tools.deps that resolve and build classpaths are (resolve-deps) and (make-classpath-map), respectively.
Let’s take a look at their work and arguments:

Managing dependencies
(resolve-deps) is the first one that comes into play. As a first argument, it takes a list of dependencies declared in a top-level :deps key of deps.edn. And as a second argument, map of pre-defined keys taken from an alias that you used when launched clj.
:extra-deps allows you to add dependencies only when a particular alias is invoked. For example, you don’t need to use the test-runner dependency, unless you are running a test. So you can put it in an alias under :extra-deps:
|
|
Other keys that can be used in an alias on this step are:
- :override-deps - overrides the library version chosen by the version resolution to force a particular version instead.
:default-deps- provides a set of default versions to use.:replace-deps- a map from libs to versions of dependencies that will fully replace the project:deps.
When invoked, (resolve-deps) will combine the original list of dependencies with modifications provided in aliases, resolve all transitive dependencies, download required artifacts, and will build a flat libraries map of all dependencies needed for current invokation.
Since managing dependencies step happens at any kind of clj invocation, pre-defined keys :extra-deps, :override-deps and :default-deps can be used with any clj option we described before.
Building classpath
After the libraries map is created, the classpath building function comes into play. (make-classpath-map) takes three arguments:
- libraries map that is a result of the
(resolve-deps)step. - content of
:pathskey indeps.ednmap. - map of pre-defined keys
:extra-paths,:classpath-overridesand:replace-pathstaken from executed alias.
:extra-paths allows you to add new paths when a specific alias is invoked. For example, if you have source code for all the tests in a specific test folder, you can include it in a dedicated alias and not include it in other builds. deps.edn will look similar to this:
|
|
Other pre-defined keys for this stage are:
:classpath-overridesspecifies a location to pull a dependency that overrides the path found during dependency resolution; for example, to replace a dependency with a local debug version.
|
|
:replace-paths: a collection of string paths that will replace the ones in a:pathskey.
Running REPL with clj -A
There is one more clj option that can work with aliases that we haven’t talked about yet.
clj -A runs REPL. If you invoke it with some alias, it will take into account all dependency-related and path-related predefined keys mentioned in the alias. There are no pre-defined keys that are specific only to the -A option.
Let’s say we have the following project structure:
project-dir
├─ src
│ └─ core.clj
└─ test
└─ test_runner.clj
With core.clj:
|
|
test_runner.clj:
|
|
And deps.edn:
|
|
If we start a REPL with the clj command, we will be able to run something from core, but won’t be able to reach test-runner, because test folder is not in the :paths key of deps.edn:
|
|
But if we run clj -A:test, there won’t be an error, because the:extra-paths key in the alias adds a test folder. Also, note that test-runner can use the taoensso.timbre library because that lib is listed in :extra-deps.
|
|
Real-world examples
Let’s analyze some real-world deps.edn files to understand how they work.
Cognitect test-runner setup
We already mentioned cognitect’s test-runner above. It is a library for discovering and running tests in your project.
Its documentation suggests adding the following alias to your deps.edn:
|
|
Let’s break it down:
:extra-pathssays thatcljshould consider the “test” folder to build our classpath when using the:testalias.:extra-depsspecifies that thetest-runnerlibrary can be downloaded from github.- having
:main-optsmeans that we can run tests usingclj -M:test ...args...Args description can be found on the documentation page. - having
:exec-fnmeans that we can also run testing withclj -X:test args-map. Args-map description can be found on the documentation page.
clj-new library setup
clj-new library allows you to generate new projects from templates. In contrast to the previous example, this time you suggested adding a new alias globally in ~/.clojure/deps.edn:
|
|
:extra-depssays we can getclj-newfrom Maven.:exec-fnmeans that we can run the alias viaclj -X:new.- and by defining alias in
~/.clojure/deps.ednyou make it available in any folder on your system. So you can run something likeclojure -X:new :name myname/myappto createmyappproject. Arguments:name myname/myappwill be put in a map, merged with a map under:exec-args, and passed toclj-new/createfunction.
Other clj capabilities
clj has a bunch of other functionality that you can explore by reading the output of clj --help.
clj -Sdescribe will print environment info. In the output you can find the :config-files key with a list of deps.edn files used in the current run.
clj -Spath will print you the result classpath. Try running it with different aliases to figure out the impact on the resulting classpath; for example, by running with :test alias: clj -Spath -A:test
In Deps and CLI Reference you will find a full explanation of clj capabilities.
In Deps and CLI Guide you can find a bunch of useful examples of clj and deps.edn usage, like running a socket server remote REPL.
Conclusion
In this article, we’ve covered how clj, tools.deps, and deps.edn work together. The key concept of “alias” is explained in different examples. Also, the process of building a classpath was reviewed in detail to provide a better understanding of how pre-defined keys from your aliases impact it.