commanded 0.1.0 copy "commanded: ^0.1.0" to clipboard
commanded: ^0.1.0 copied to clipboard

A Dart package to make parsing CLI arguments easier and safer.

example/lib/example.dart

import 'dart:math';

import 'package:commanded/commanded.dart';

// Build Runner stuff.
part 'example.g.dart';

// Here, let's create a simple command-line application with a bunch of subcommands that do various things.
// We're totally not doing this just for example for the package.
//
//
// Our first utility: **Random number generator**
// This will be able to:
// - Generate a random number from 0 and 100, inclusive.
// - Have optional --min and --max options for tuning the range.
// - Have an optional --secure flag for making the RNG use Random.secure. For a bonus, we'll also let this have a -s abbreviation!
// - Have an avoid list too, so the user can select specific numbers to avoid choosing. We'll also give this an abbreviation of -a!
// - We'll also have a --timed flag for debugging.
//
//
// Our next utility: **Text Echoer**
// This will be able to:
// - Print inputted text.
// - Repeat said text over and over.
// - Capitalize the text, or make it lowercase.
// Yes, this is basic, but we're gonna use this to show off positional arguments, negatable flags, and more.
//
//
// Our next little thing: **PrintMyArguments**
// This is a very basic command to demonstrate positional arguments. We'll also do enums too!

// First, we're gonna set up a base command class. This gives us global options.
// For now, we'll just have --verbose.
// This allows of this class to be inherited by command classes that extends BaseCommand!
abstract class BaseCommand extends Command {
  @Flag("verbose", abbr: "v", help: "Enable verbose mode. This gives you extra logs.")
  bool verbose = false;
}

// We're creating this command as a base for our other subcommands.
// One important limitation is that flags defined in this command won't apply to subcommands.
// For global options, we have to create/edit classes like BaseCommand.
//
// See the very bottom of the file for why we use MainCommand here.
@MainCommand()
class ParentCommand extends Command {
  // The binary name. This appears in the usage.
  @override
  String get name => "example";

  // We only have subcommands in this command, so we error if someone runs our command without any subcommands.
  @override
  CommandSettings get settings => .new(subcommandsOnly: true);

  // Define a subcommand. We can put late here instead of creating a new command.
  // The build runner only looks at the type, which is RandomNumberCommand here.
  @Subcommand("random", help: "Generate a random number.")
  late final RandomNumberCommand randomNumberCommand;

  // Define another subcommand.
  @Subcommand("echo", help: "Echo some text.")
  late final EchoCommand echoCommand;

  // Define yet another subcommand...
  @Subcommand("args", help: "Do some positional argument stuff!")
  late final PrintMyPositionalArgumentsCommand printMyPositionalArgumentsCommand;

  // Build the usage line that shows up when --help is called.
  // This is very customizable, to fit whatever style you prefer!
  // If you don't override this, or return null, a default usage builder is used.
  @override
  UsageBuilder? buildUsage() {
    return .new()
      ..addCustom("example")
      ..addCustom("<${allSubcommands.map((x) => x.name).join(" | ")}>")
      ;
  }

  // Build the block of text that shows up when --help is called.
  // This is also very customizable!
  //
  // We don't do much here, but we'll use addSubcommands to make things easier.
  // We'll do more later.
  @override
  HelpBuilder buildHelp() {
    return .new()
      ..addSubcommands(allSubcommands)
      ;
  }

  // This will never be called, cuz we only have subcommands defined here.
  @override
  void onRun() {}
}

// Because we extend BaseCommand, we automatically get the --verbose flag passed down to us!
// Isn't that so cool?
class RandomNumberCommand extends BaseCommand {
  // This also shows up in the usage.
  @override
  String get name => "random";

  // Since it's not late, and we put a default, the option is not required, and will default to 0.
  @Option("min", help: "Min number to generate, inclusive. Defaults to 0.")
  int min = 0;

  // Since it's not late, and we put a default, the option is not required, and will default to 100.
  @Option("max", help: "Max number to generate, inclusive. Defaults to 100.")
  int max = 100;

  // Not gonna repeat that again
  @Option("count", abbr: "c", help: "Amount of numbers to generate. Defaults to 1.")
  int count = 1;

  // Since it's not late, and we put a default, the flag will default to false.
  @Flag("secure", abbr: "s", help: "Whether to make this RNG secure. Defaults to false.")
  bool secure = false;

  // Since this one is also not late, and we put a default, the flag will default to false.
  @Flag("timed", abbr: "t", help: "Whether to time how long it takes to generate the number. Defaults to false.")
  bool timed = false;

  // Whoa, a multi-option?
  // This is basically an Option, but it can be provided multiple times, or none at all, to build a list of inputs.
  // Every time someone adds --avoid <number>, this list is added to.
  // There's an option min parameter for the annotation as well, but we don't need that.
  @MultiOption("avoid", abbr: "a", help: "Numbers to avoid choosing.")
  final List<int> avoids = [];

  // This is a validator.
  // This kinda works like Flutter's text field validator, if you've ever used that.
  // If you return a string, the user will see your message, along with usage and help.
  // It's just like when the user provides invalid arguments, and the parser shows a message.
  @override
  String? validate() {
    if (min < 0 || max < 0) return "Both min and max cannot be negative.";
    if (min > max) return "min must be equal to or lesser than max.";
    if (count < 1) return "Count must be positive.";

    if (avoids.length >= max - min + 1) return "You avoided as many numbers as you can generate!";
    return null;
  }

  // Run our program! All of our properties, like min, max, secure, and even random, are available here!
  @override
  void onRun() {
    final Random random = (secure ? .secure() : .new());
    final stopwatch = Stopwatch()..start();
    bool avoided = false;

    for (int i = 0; i < count; i++) {
      int? value;

      while (value == null || avoids.contains(value)) {
        if (value != null) avoided = true;
        value = random.nextInt((max - min) + 1) + min;
      }

      print(value);
    }

    stopwatch.stop();
    if (timed) print("Time: ${avoided ? "<Timing unavailable because generation required retries.>" : stopwatch.elapsedMicroseconds}us");
    if (verbose) print("Generated number! (secure: $secure, avoided: ${avoids.join(", ")})"); // We're using verbose, from BaseCommand!
  }

  // Build our help, but this time with more customization!
  // We can even add separators and custom text! How cool is that!
  @override
  HelpBuilder buildHelp() {
    return .new()
      ..addCustom("Options:")
      ..addOption(options.min)
      ..addOption(options.max)
      ..addSeparator()
      ..addCustom("Flags:")
      ..addFlag(flags.secure)
      ..addFlag(flags.verbose)
      ;
  }
}

class EchoCommand extends BaseCommand {
  // This also shows up in the usage.
  @override
  String get name => "echo";

  // These will be explained below.
  @override
  CommandSettings get settings => .new(allowTrailingOptions: false, errorOnInvalidOptions: false);

  // An option.
  @Option("repeat", abbr: "r", help: "How many times to repeat the text. Defaults to 1.")
  int repeat = 1;

  // We've seen binary flags, but this here is one of em negatable flags.
  // This being negatable, we can now make this nullable, giving it 3 values.
  // There are 3 different cases that we have to account for now:
  // - --capitalize: This value becomes true. We'll take this as capitalize the entire string.
  // - --no-capitalize: This is autogenerated, and if this is used, this value becomes false. We'll take this as make the entire string lowercase.
  // - Not provided: This value stays null. We'll leave the text how it is.
  @Flag("capitalize", negatable: true, help: "Whether to capitalize this string, or make it all lowercase.")
  bool? capitalize;

  // Ooh, what's this Rest thing?
  // Well, this basically tells the command that if the user provides any extra positional arguments, to try to parse it and add it to this list.
  // You can only have up to 1 of these; if you need to override ones from higher up, simply make this one have the same name.
  //
  // min here is like multi-options. If there's less than min of these provided, we'll throw a parse error.
  // Also converters apply here too!
  @Rest("text", help: "Your words.", min: 1)
  final List<String> rest = [];

  // Some basic help building.
  @override
  HelpBuilder buildHelp() {
    return .new()
      ..addOption(options.repeat)
      ..addFlag(flags.capitalize)
      ;
  }

  // Let's build a custom usage line!
  // We did the same in ParentCommand, but here we'll add some extra flare just because.
  // As HelpBuilder allows extra text, so does UsageBuilder!
  @override
  UsageBuilder? buildUsage() {
    return .new()
      ..addFlag(flags.verbose)
      ..addFlag(flags.capitalize)
      ..addOption(options.repeat)
      ..addCustom("...text")
      ;
  }

  // Another validator!
  @override
  String? validate() {
    if (repeat < 1) return "repeat must be positive.";
    return null;
  }

  @override
  void onRun() {
    // Oh-ho, what's this 'rest' variable we see?
    // Well, this is in fact a list containing all the positional arguments, even if we haven't defined them.
    // Remember our settings up there, specifically allowRest? This makes it so that our parser doesn't error if it encounters a positional argument that overflows the amount we've defined.
    //
    // Because rest is a List<String>, we will combine all the arguments with spaces.
    // Yes, it might be inaccurate, but the point here is to show off the rest variable.
    // You might use this for better purposes later on.
    //
    // Remember allowTrailingOptions? Well, this makes it so that when we use a positional argument, or start inputting text, then options and flags after that get parsed as a positional argument.
    // Try it out! Use this:
    //   dart run example echo "hey there!" --repeat 3
    // You'll actually see the text:
    //   hey there! --repeat 3
    // Instead of the text being repeated 3 times.
    //
    // There's one more setting that'll come into play: errorOnInvalidOptions.
    // Because we disabled this, if the parser comes across an option or flag it doesn't know about, it'll parse it as a positional argument.
    // Try doing this! When testing this command, add a random flag that doesn't exist, like --my-flag.
    // It'll get parsed as the text!

    final text = switch (capitalize) {
      true => rest.join(" ").toUpperCase(),
      false => rest.join(" ").toLowerCase(),
      null => rest.join(" "),
    };

    // A bit anticlimactic, huh
    print(text * repeat);
  }
}

// For showing off; see below.
enum MyEnum {
  one,
  two,
}

// A very basic command to demonstrate positional arguments.
class PrintMyPositionalArgumentsCommand extends BaseCommand {
  @override
  String get name => "args";

  // Define converters for options, multi-options, arguments, and rest parameters.
  // If we don't do this, we'll get a runtime error.
  // This is because the parser doesn't know how to use MyEnum.
  // We never have to do this for flags.
  //
  // Basic types are built-in, like int, num, String, double, and bool.
  // For more info, see the readme of this package.
  @override
  List<Converter<dynamic>> get converters => [
    EnumConverter<MyEnum>(MyEnum.values),
  ];

  // Required.
  @Argument("arg1", help: "An argument!")
  late String arg1;

  // Required.
  @Argument("arg2", help: "An argument?")
  late int arg2;

  // Not required.
  // We'll also use this to show off how enums can be used, using EnumConverter (see above).
  @Argument("arg3", help: "An argument...")
  MyEnum? arg3;

  // We'll also add a rest parameter like above!
  // This'll show off how you can use more than just strings here.
  @Rest("numbers", help: "Numbers!")
  final List<int> numbers = [];

  @override
  HelpBuilder buildHelp() {
    return .new()
      ..addArguments(allArguments)
      ..addRest(numbersData)
      ;
  }

  @override
  void onRun() {
    print("arg1: $arg1 (${arg1.runtimeType})");
    print("arg2: $arg2 (${arg2.runtimeType})");
    print("arg3: $arg3 (${arg3.runtimeType})");
    print("numbers: ${numbers.join(", ")} (${numbers.runtimeType})");
  }

  // Because we defined arg1, arg2, and arg3 in that order, that's the order positional arguments will be parsed as.
  // If you run this:
  //   dart run example args test 32 true
  // You'll see arg1, arg2, and arg3 printed in that order.
  //
  // If you run:
  //   dart run example args true test 32
  // You'll see an error message, saying that you entered something invalid.
  // Now, try rearranging these arguments, and see what happens!
}

// Since we put @MainCommand() above our... main command, we get this special function called runCommands.
// This simply points to ParentCommandData.runFromList. We could call this manually, but using runCommands feels so much more professional, ya know?
// However, it's absolutely still possible to call ParentCommandData.runFromList, or any other command; this is just a shortcut.
void main(List<String> arguments) {
  runCommands(arguments);
}
0
likes
150
points
262
downloads

Documentation

API reference

Publisher

verified publishercalebh101.net

Weekly Downloads

A Dart package to make parsing CLI arguments easier and safer.

Repository (GitHub)
View/report issues

License

GPL-3.0 (license)

Dependencies

analyzer, build, collection, meta, source_gen

More

Packages that depend on commanded