Have you ever wondered how a CLI app is implemented?
Out there is not all frontend or backend, mobile or web. Tooling is also a thing. A major one 💁🏼♂️
This week we use Functional Programming with fpdart to implement our own CLI.
Here is how it works 👇
Tech stack
- Dart: the app is written in plain dart, no framework or anything else needed
- fpdart: the app uses fpdart for Functional Programming, which makes it safe, readable, and super convenient when it comes to error handling
Setup
Since the app is plain dart you can just execute dart create
. We use the console template:
dart create -t console <name>
This creates a simple setup:
lib
: where we are going to implement the CLIbin
: contains themain
function, entry point where the CLI is executedtest
Get started
A CLI (Command-Line Interface) allows to run a command on the terminal and execute some code. As simple as that.
You can do things like:
- Read and write files
- Make http requests
- Scan files and apply formatting or linting
- Create a project from a template
We are going to implement a CLI that scans all the folders and dart files and finds all unused files (no imports)
This requires the following steps:
- Read all the files
- Extract imports
- Find which files are unused
- Report the result
Dart provides many resources to get started:
There is more 🤩
Timeless coding principles, practices, and tools that make a difference, regardless of your language or framework, delivered in your inbox every week.
Implementation
The main
function in dart accepts a List<String>
parameter (arguments
):
arguments
represents the arguments provided when running the command on the terminal.
The args
package allows to easily parse arguments and extract flags and options. The app accepts a -o
option to specify the location of the configuration file:
dart run bin/dart_cli_with_fpdart.dart -o cli_options.yaml
The implementation is based on 3 principles:
- Pattern matching with sealed class for error handling: this allows to define all errors in a single file and make sure to handle them all using patterns
- Dependency injection: all the modules are defined as abstract classes, and then implemented and injected
- Functional programming: the implementation relies on fpdart to compose modules, handle errors, and execute sync and async operations
The resulting main
function is safe, easy to read and maintain:
void main(List<String> arguments) async =>
program(arguments) /// Execute the `program` [ReaderTaskEither]
.match<void>((cliError) {
exitCode = 2;
/// Handle [CliError] here ⛔️
}, (result) {
exitCode = 0;
/// Report unused files here ✅
}).run(
/// Provide all dependencies (Dependency injection) 💉
AppMainLayer(
argumentsParser: ArgumentsParserImpl(ArgParser()),
configReader: ConfigReaderImpl(YamlLoaderImpl()),
fileReader: FileReaderImpl(),
),
);
👉 For all the details and code snippets you can read the full article containing all the details of the implementation.
Example of running the final CLI app: all the used and unused files are reported. You can go ahead and remove all used files!
Takeaways
- I would recommend considering dart for CLI development, the setup is super easy and the tools work great
- Combining pattern matching, fpdart, and dependency injection makes everything safe and easy to read and maintain
- CLI development allows to focus 100% on the implementation: no need to handle multiple screen sizes, no UI, no versioning. Just plain dart. This makes it great if your focus is learning the language without worrying about any extra configuration
- It's easier than you think to end up with unused file 😅
I strongly suggest you to explore and try implementing your own CLI app. It's definitely a great way to learn any language.
That's it for this week project. Looking forward to start another one for next week!
See you next 👋