zio
Creating CLI tools leveraging ZIO and Decline using scala-cli
Here’s a quick overview and demonstration of kicking the tires on ZIO 1.x + decline (commandline argument parser) using scala-cli
(version 0.1.6) to build commandline tools.
High Level Points
scala-cli
is a new tool in active development that aims to replace the currentscala
tool.scala-cli
enables running scripts, loading REPL, compiling, testing, packaging (amongst other features) for “simple” (flat) projects.scala-cli
enables a very Python-ish workflow and integrates with your text editor well. A REPL driven approach can be used usingscala-cli repl File.scala
.scala-cli package
enables creating an executable from your main class. This is very useful. No need to deal with Python’s conda, pipenv, poetry, etc… for setting up an env. Rsync/scp your packaged tool to another server and your good to go (provided the java versions are compatible).- Building a commandline tool using ZIO was a useful exercise to kick the tires on ZIO and understand the ZIO 1.x effect system and learn how to compose computation in ZIO.
decline
is a CLI parser library usingcats
. It has a very elegant mechanism of composing options/commands.- The default
decline
interface had some unexpected behaviors with how--help
was handled and how errors were handled/mapped to exit codes.
Specifics
Using scala-cli setup-ide .
will generate the necessary files for the LSP server for your text editor.
Using directives
//>
in the top of your scala file, you can define the scala version, library versions, etc…
For example, Declined.scala
1 | //> using platform "jvm" |
Using zio.App
we can define our main
by overriding run
.
1 | object DeclinedApp extends zio.App { |
Using decline
, CLI options and arguments are defined using Opts
.
For example:
1 | import com.monovore.decline._ |
These Opts
compose in interesting ways, specifically with ZIO
effects.
1 | val versionOpt: Opts[RIO[Console, Unit]] = Opts |
You can compose options together using mapN
to define an “action” or “command” that would need multiple commandline options/args.
For example to define a mainOpt
that uses name, alpha, force
:
1 | val nameOpt = Opts.option[String]("user", help = "User name", "u") |
In addition to composing using mapN
, there’s orElse
which enables composing “actions”. For example, enabling --version
to run (if provided) or run the “main” application.
1 | val runOpt = versionOpt orElse mainOpt |
These composed Opts
can be used in a Command
that will handled --help
and be central point where Command.parse
can be called.
1 | val command: Command[RIO[Console, Unit]] = Command[RIO[Console, Unit]]( |
Bridging the ouptut of Command.parse
with ZIO requires a little glue and some special attention to deal with the error cases. Command.parse
will return an Either[Help, T]
. The left of Either
being used as “help+errors” container is a bit of friction point because --help
triggers the left of the Either
. Help.errors
will return a non-empty list of errors (if there are parse errors).
1 | def effect(arg: List[String]): ZIO[Console, Throwable, Unit] = { |
And finally, wire a call to run
and make sure errors are written to stderr and a non-zero exit code is returned during failure cases.
1 | override def run(args: List[String]): ZIO[ZEnv, Nothing, ExitCode] = |
Running can be done using scala-cli run Declined.scala
1 | $> scala-cli run Declined.scala -- --version |
Or by packaging the app and running the generated exe.
1 | scala-cli package --jvm 14 Declined.scala |
Running
1 | $> ./DeclinedApp --help |
And a few smoke tests.
1 | $> ./DeclinedApp --user Dave --alpha 3.14 |
Summary and Final Comments
scala-cli
is a very promising addition for the Scala community.scala-cli
changed my workflow. This new workflow was closer to how I would work in Python.- I really like ZIO’s core composablity ethos, however, it does have a learning curve.
Decline
‘sCommand[T]
design allows for intergrate with ZIO pretty seemlessly.- Misc “scrappy” CLI tools that I would typically write in Python, I could easily write in Scala.