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
18.104.22.1685 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:
clojureare 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 of
Difference between clj and clojure executables
clojure are scripts, and
clj is just a wrapper on
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 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
I will be using only
clj later in the article.
-X are the most important options, and the ones you need to learn first.
-M option to work with
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
For example, if you have the following project directory structure:
project-dir (project directory) └─ src (default sources directory) └─ core.clj
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
If you have a
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]*
core.clj is located in your
project-dir/src, you can call
(print-args) using CLI from the
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
:path key, you specify the vector of directories where source code is located.
deps.edn file doesn’t exist in your project folder or it doesn’t contain the
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
You can run something from
core.clj because it is in the
But an attempt to run
test-runner/run will fail. The
test-runner namespace from the
test 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
Now the content of the
test folder is visible to
Note, that you should specify both the
test folders 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/tag keys to specify the library version.
deps.edn like this:
timbre now can be used after importing its namespace:
test-runner main function can be invoked by
clj with already known
More details on how to use local dependencies and meaning of different keys can be found in official documentation.
The “alias” is the main concept in
deps.edn. It is where concentrates all convenience of
clj tool. Let’s explore it with examples.
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
: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:
: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
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
To run reports for all tables from the command line, we can invoke:
Since typing all arguments in the command line is quite tedious, let’s create aliases in
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
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
-M options, but we will discuss them later.
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 the
tools.deps library that goes with Clojure.
Two main functions in
tools.deps that resolve and build classpaths are
Let’s take a look at their work and arguments:
(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
: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
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
(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
:default-deps can be used with any
clj option we described before.
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
- content of
- map of pre-defined keys
: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
Running REPL with
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
Let’s say we have the following project structure:
project-dir ├─ src │ └─ core.clj └─ test └─ test_runner.clj
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 folder is not in the
:paths key of
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
Let’s analyze some real-world
deps.edn files to understand how they work.
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
Let’s break it down:
cljshould consider the “test” folder to build our classpath when using the
:extra-depsspecifies that the
test-runnerlibrary can be downloaded from github.
:main-optsmeans that we can run tests using
clj -M:test ...args...Args description can be found on the documentation page.
:exec-fnmeans that we can also run testing with
clj -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
:extra-depssays we can get
:exec-fnmeans that we can run the alias via
- and by defining alias in
~/.clojure/deps.ednyou make it available in any folder on your system. So you can run something like
clojure -X:new :name myname/myappto create
:name myname/myappwill be put in a map, merged with a map under
:exec-args, and passed to
clj has a bunch of other functionality that you can explore by reading the output of
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
clj -Spath -A:test
In Deps and CLI Reference you will find a full explanation of
In Deps and CLI Guide you can find a bunch of useful examples of
deps.edn usage, like running a socket server remote REPL.
In this article, we’ve covered how
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.