Contents
    Eugene Saturow Head of Flutter

    Build Yourself a Tool for Automation: A Dart Script

    A good tool makes light work. Oftentimes, the number and quality of tools available affects how fast the community evolves within the technology they were made for. But we’ll never be content with what we have, otherwise the world would stop spinning.

    Hi! I’m Eugene Saturow, and I’m a Flutter developer at Surf. I’ll tell you how to make your life easier, if you’re a programmer, and put that into practice.

    If you have a lot of routine actions — automate them

    Gradually, you start to notice that you have to repeat a lot of actions: big or small, alone or as a team. They’re not too complicated, but they wear you down and waste the resources you could have spent getting in sync with your team. A logical thing to do would be to try and get rid of them, but no one’s going to do that for you just yet. You need a helping hand, an extra tool to make your life easier.

    Options: find a tool, write a script, or write a plugin

    Let’s first try to just find a tool. If your situation isn’t unique, someone has most likely addressed it already. Otherwise, let’s do it ourselves (and don’t forget to share the result). 

    Now that we’ve made the decision, let’s choose what shape our helper will take. Depending on the time we have, we can choose between:

    • a console utility (a script, software), 
    • a plugin, 
    • full-fledged software. 

    A console utility

    The tools used in the console don’t require exceptional skills to work with and only take a little time to develop. It’s a great option for small actions. But the larger a functionality’s scope is, the more attention to architecture it requires. 

    With even a simple script at hand, a team can find common ground in terms of style, name, and structure of files and folders. Staying rigid in this aspect, helps find your way around the project much easier and saves time on reviews. Only now you don’t need a strict reviewer to keep an eye on everything: there’s a strict script!

    A plugin

    Plugins are bound to the environment they were written for; that’s a pain in the neck. Teams rarely use one environment, and supporting several plugins is not always easy. 

    On the other hand, a plugin integrates into an environment, meaning, it’s much easier to write a plugin compared to an environment or an entire piece of software. You can have much quicker access to functionality. 

    Personally, I feel that plugins show that authors of this or that technology take care of their users: such an approach makes it much easier to handle. It takes my breath away to think that I can simply tap one nice button and presto! We’re done! 

    Full-fledged software

    It’s hard to write, support compatibility, and ensure portability. This step is the last resort, and all the risks should be evaluated in advance. We won’t be looking into this option, but if you’re ready, go for it! After all, someone built Vim and VS Code…

    I’d also like to mention aliases and hotkeys. Make use of them! Assigning them is simple, but they’re a step towards automation as well. Even small changes like that affect productivity: they save seconds and make work more enjoyable.

    A CLI tool in Dart

    Suppose you have a basic task: you need to write a boilerplate generator. We’re short on time, and we want something simple: thus, we choose a console utility. This scenario has occurred in our practice (more than once), so, to make it more tangible, let’s take a look at creating one in real life. 

    A short while ago, we were putting together some tooling for a Flutter package called Elementary and decided to deliver it as an executable package in Dart. 

    • Leaning on the package to create a console app.
    • Adding “branch commands” and “leaf commands”.
    • Implement the necessary functionality. 

    Let’s create a Dart project. To make it easier to process arguments, let’s add the args package. Then, add a branch command called generate and a leaf command called module. Calling them will look like this: 

    generate module [args]
    class GenerateCommand extends Command<void> {
      static const templatesUnreachable =
          FileSystemException('Generator misses template files');
    
      @override
      String get description => 'Generates template files';
    
      @override
      String get name => 'generate';
    
      @override
      bool get takesArguments => false;
    
      GenerateCommand() {
        addSubcommand(GenerateModuleCommand());
      }
    
    }

    A “leaf command” should also override description and name, but, beside that, we need to take care of a parameter named argParser. In it, we have to specify all the arguments that the command wants to receive:

    static const pathOption = 'path';
      static const nameOption = 'name';
      static const isSubdirNeededFlag = 'create-subdirectory';
    
    @override
      ArgParser get argParser {
        return ArgParser()
          ..addOption(
            nameOption,
            abbr: 'n',
            mandatory: true,
            help: 'Name of module in snake case',
            valueHelp: 'my_cool_module',
          )
          ..addOption(
            pathOption,
            abbr: 'p',
            defaultsTo: '.',
            help: 'Path to directory where module files will be generated',
            valueHelp: 'dir1/dir2',
          )
          ..addFlag(
            isSubdirNeededFlag,
            abbr: 's',
            help: 'Should we generate subdirectory for module?',
          );
    
      }

    In this case, we clearly want to receive three parameters: 

    • name of the module, 
    • the destination folder for the generated files and the flag, 
    • is it worth creating a folder named after the module for these files. 

    Let’s make the name parameter mandatory — pass mandatory: true. The destination will be the current folder by default. Note that you can set short versions for options and flags: e.g., you can write –name as well as -n. You’ll also have an automatically generated reference where you can write descriptions for flags and options.

    And, finally, a place where it all happens—a method called run:

    @override
      Future<void> run() async {
        final parsed = argResults!;
        final pathRaw = parsed[pathOption] as String;
        final fileNameBase = parsed[nameOption] as String;
        final isSubdirNeeded = parsed[isSubdirNeededFlag] as bool;
    …………
    
    }

    As a result, we receive arguments in the field argResults, convert them into the type we need, and are able to use them as we wish! If you want to know what we wished for, check out the entire code on github

    All we’ve got left is to put a file with the main function into the folder called bin:

    import 'dart:io';
    
    import 'package:args/command_runner.dart';
    import 'package:elementary_cli/generate/generate.dart';
    
    Future<void> main(List<String> arguments) async {
    
      const commandName = 'elementary_tools';
      const commandDescription = 'CLI utilities for Elementary';
      
      final runner = CommandRunner<void>(commandName, commandDescription)
        ..addCommand(GenerateCommand());
      await runner.run(arguments);
    
    }
    

    You can now publish the package: any user will be able to activate it with the command dart pub global activate and run it with dart pub global run.

    Writing yourself a helper tool is pretty easy. What’s much harder is doing a good job of highlighting the thing you want to address, so that the solution is useful. And if the script is already popular, you should think about integrating it into your workflow.

    This was an article that belongs to a series about tooling. Next time, we’ll take a look at creating a plugin for VS Code, discuss the nuances, and see if we can use an IDE and a console utility in conjunction.