This workshop session is designed to give you a complete introduction to the core features of Airline for creating powerful CLIs. This was originally written for the Tech Exeter Conference series and presented in September 2018.
The workshop is provided as a HTML slideshow embedded below, use the arrow keys to navigate through the slides. You can view this in fullscreen by hitting the icon in the top left of the slides.
Everyone builds command line applications at some point but often they are cobbled together or full of needless boiler plate. Airline takes a fully declarative approach to command line applications allowing users to create powerful and flexible command lines.
Airline takes care of lots of heavy lifting providing many features not found in similar libraries including annotation driven value validation restrictions, generating Man pages and Bash completion scripts to name just a few.
In the workshop session we’ll work through an example command line application to see just how powerful this can be.
In order to follow along with this workshop we assume the following knowledge and tools:
git installedmvn installedNB 2-4 will allow you to run the examples shown in the slides but aren’t essential.
A Linux/Mac laptop is preferred but you should be able to use Windows for most things.
You can find these slides at http://rvesse.github.io/airline/guide/practise/workshop.html
Firstly we want to define our command lines using declarative annotations.
This allows us to separate the command line definition cleanly from the runtime logic.
It also enables us to do optional build time checking of our definitions to ensure valid command line apps.
Secondly we look to avoid the typical boiler plate code associated with many command line libraries.
You shouldn’t need to write a ton of if statements to check that values for options fall in specified ranges or
meet common application constraints.
Finally we don’t want to tie you into a particular implementation approach.
We provide extensibility of almost every aspect of the parsing process yet provide a general purpose default setup that should suit many users.
So a basic CLI should just work, advanced CLIs can be configured as desired
Due to time constraints this will be more of an interactive demo, almost everything in these slides is in the GitHub repo so you can play along.
For this workshop we are going to build an example command line application called send-it for shipping of packages.
The example code in these slides is typically truncated to omit things like import declarations for brevity, the full
code is linked alongside each example.
The example code all lives inside the Airline git repository at https://github.com/rvesse/airline/tree/master/airline-examples
We use > to indicate that a command should be run at a command prompt and <input> within that to indicate some input
is needed.
To follow along you should start by checking out the code and building the examples:
> git clone https://github.com/rvesse/airline.git
> cd airline
> mvn package
Many of the examples are runnable using the runExample script in the airline-examples sub-directory e.g.
> cd airline-examples
> ./runExample SendIt <options>
Or for this specific workshop the send-it script in that same sub-directory can be used:
> ./send-it <options>
Or for Windows Users:
> send-it.bat <options>
Airline works with POJOs (Plain Old Java Objects) so firstly we need to define some classes that are going to hold our commands options.
We can define our options across multiple classes and our inheritance hierarchy i.e. you can create a BaseCommand with
your common options.
Or you can define options in standalone classes and compose them together.
We’re going to see the latter approach in this workshop, see Inheritance and Composition for more detail on the former approach.
@OptionThe @Option annotation is used to mark a field as being populated by an option.  At a
minimum it needs to define the name field to provide one/more names that your users will enter to refer to your option
e.g.
@Option(name = { "-e", "--example" })
private String example;
Here we define a simple String field and annotate it with @Option providing two possible names - -e and
--example - by which users can refer to it.
Other commonly used fields in the @Option annotation include title used to specify the title by which the value is
referred to in help and description used to provide descriptive help information for the option.
PostalAddress exampleLet’s take a look at the PostalAddress class which defines options for specifying a UK postal address.  Explanatory text is interspersed into the example:
public class PostalAddress {
    
    @Option(name = "--recipient", title = "Recipient", 
            description = "Specifies the name of the receipient")
    @Required
    public String recipient;
So we start with a fairly simply definition, this defines a --recipient option and states that it is a required option
via the @Required annotation.
    @Option(name = "--number", title = "HouseNumber", 
                   description = "Specifies the house number")
    @RequireOnlyOne(tag = "nameOrNumber")
    @IntegerRange(min = 0, minInclusive = false)
    public Integer houseNumber;
    
    @Option(name = "--name", title = "HouseName", 
                   description = "Specifies the house name")
    @RequireOnlyOne(tag = "nameOrNumber")
    @NotBlank
    public String houseName;
Now we’re starting to get more advanced, here we have two closely related options - --number and --name - which we
declare that we require only one of via the @RequireOnlyOne i.e. we’ve told
Airline that one, and only one, of these two options may be specified.
Additionally for the --number option we state that it must be greater than zero via the
@IntegerRange annotation and for the --name option we state that it must be
@NotBlank i.e. it must have a non-empty value that is not all whitespace.
Here we have an option that may be specified multiple times to provide multiple address lines.  Importantly we need
to define it with an appropriate Collection based type, in this case List<String> in order to collect all the
address lines specified.
    @Option(name = { "-a", "--address", "--line" }, title = "AddressLine", 
            description = "Specifies an address line.  Specify this multiple times to provide multiple address lines, these should be in the order they should be used.")
    @Required
    @MinOccurrences(occurrences = 1)
    public List<String> addressLines = new ArrayList<>();
Here we also use the @MinOccurences annotation to state that it must occur at
least once in addition to using the previously seen @Required
    @Option(name = "--postcode", title = "PostCode", 
                   description = "Specifies the postcode")
    @Required
    @Pattern(pattern = "^([A-Z]{1,2}([0-9]{1,2}|[0-9][A-Z])) (\\d[A-Z]{2})$", 
                    description = "Must be a valid UK postcode.", 
                    flags = java.util.regex.Pattern.CASE_INSENSITIVE)
    public String postCode;
Here is another example of a complex restriction, this time we use the @Pattern
annotation to enforce a regular expression to validate our postcodes meet the UK format.
And finally we have some regular Java code in our class. Your normal logic can co-exist happily alongside your Airline annotations, we’ll see this used later to implement our actual command logic.
    @Override
    public String toString() {
        StringBuilder builder = new StringBuilder();
        builder.append(this.recipient);
        builder.append('\n');
        if (this.houseNumber != null) {
            builder.append(Integer.toString(this.houseNumber));
            builder.append(' ');
        } else {
            builder.append(this.houseName);
            builder.append('\n');
        }
        
        for (String line : this.addressLines) {
            builder.append(line);
            builder.append('\n');
        }
        builder.append(this.postCode);
        
        return builder.toString();
    }
}
@ArgumentsThe @Arguments annotation is used to annotate a field that will receive arbitrary
inputs i.e. anything that is not recognised as an option as defined by your @Option annotations.  This is useful when
your command wants to operate on a list of things so is typically used in conjunction with a Collection typed field
e.g. List<String>.
@Arguments in useFor example let’s take a look at it in use in the CheckPostcodes command:
    @Arguments(title = "PostCode", description = "Specifies one/more postcodes to validate")
    @Required
    @MinOccurrences(occurrences = 1)
    @Pattern(pattern = "^([A-Z]{1,2}([0-9]{1,2}|[0-9][A-Z])) (\\d[A-Z]{2})$", 
             description = "Must be a valid UK postcode.", 
             flags = java.util.regex.Pattern.CASE_INSENSITIVE)
    public List<String> postCodes = new ArrayList<>();
Which we can run like so:
> ./send-it check-postcodes "BS1 4DJ" "RG19 6HS"
BS1 4DJ is a valid postcode
RG19 6HS is a valid postcode
So we’ve already seen a number of Restrictions in the above examples. This is one of the main ways Airline reduces boiler plate and prefers declarative definitions. There are lots more built-in restrictions than just those seen so far and you can define Custom Restrictions if you want to encapsulate reusable restriction logic.
Some useful common restrictions include:
@Required - For required options/arguments@NotBlank - To enforce non-blank string values@AllowedRawValues/@AllowedValues - 
To restrict options/arguments to a set of acceptable values@Path - Provides restrictions on options/arguments used to refer to files and directoriesSo now we’ve seen the basics of defining options and arguments lets use these to define a command:
@Command(name = "send", description = "Sends a package")
public class Send implements ExampleRunnable {
    @AirlineModule
    private PostalAddress address = new PostalAddress();
    
    @AirlineModule
    private Package item = new Package();
    @Option(name = { "-s",
            "--service" }, title = "Service", description = "Specifies the postal service you would like to use")
    private PostalService service = PostalService.FirstClass;
    @Override
    public int run() {
        // TODO: In a real world app actual business logic would go here...
        
        System.out.println(String.format("Sending package weighing %.3f KG sent via %s costing £%.2f", this.item.weight,
                this.service.toString(), this.service.calculateCost(this.item.weight)));
        System.out.println("Recipient:");
        System.out.println();
        System.out.println(this.address.toString());
        System.out.println();
        return 0;
    }
    
    public static void main(String[] args) {
        SingleCommand<Send> parser = SingleCommand.singleCommand(Send.class);
        try {
            Send cmd = parser.parse(args);
            System.exit(cmd.run());
        } catch (ParseException e) {
            System.err.print(e.getMessage());
            System.exit(1);
        }
    }
}
There’s quite a few new concepts introduced here, so let’s break them down piece by piece.
@CommandThe @Command annotation is used on Java classes to state that a class is a command.  Let’s see our previously introduced PostalAddress class combined into an actual command, here we see the Send:
@Command(name = "send", description = "Sends a package")
public class Send implements ExampleRunnable {
The @Command annotation is fairly simple, we simply have a name for our command and a description.  The name is
the name users will use to invoke the command, this name can be any string of non-whitespace characters and is the only
required field of the @Command annotation.
The description field provides descriptive text about the command that will be used in help output, we’ll see this
used later.
@AirlineModule for compositionOften for command line applications you want to define reusable sets of closely related options as we already saw with
the PostalAddress class.  Airline provides a composition mechanism that makes this easy to do.
    @AirlineModule
    private PostalAddress address = new PostalAddress();
    
    @AirlineModule
    private Package item = new Package();
Here we compose the previously seen PostalAddress class into our command, we use the
@AirlineModule annotation to indicate to Airline that it should find options declared by
that class.  We also have another set of options defined in a separate class, this time the Package class is used to provide options relating to the
package being sent.
As well as composing options defined in other classes we can also define options specific to a command directly in our command class:
    @Option(name = { "-s",
            "--service" }, title = "Service", description = "Specifies the postal service you would like to use")
    private PostalService service = PostalService.FirstClass;
Here the command declares an additional option -s/--service that is specific to this command.  Here the field actual
has an enum type (PostalService)
which Airline happily copes with.
For more details on how Airline supports differently typed fields see the Supported Types documentation.
    @Override
    public int run() {
        // TODO: In a real world app actual business logic would go here...
        
        System.out.println(String.format("Sending package weighing %.3f KG sent via %s costing £%.2f", 
        				   this.item.weight, this.service.toString(), this.service.calculateCost(this.item.weight)));
        System.out.println("Recipient:");
        System.out.println();
        System.out.println(this.address.toString());
        System.out.println();
        return 0;
    }
}
Finally we have the actual business logic of our class. In this example application it simply prints out some information but this serves to show that we can access the fields that have been populated by the users command line inputs.
In order to actually invoke our command we need to get a parser from Airline and invoke it on the user input.  In this
example we do this in our main(String[] args) method:
    public static void main(String[] args) {
        SingleCommand<Send> parser = SingleCommand.singleCommand(Send.class);
We call the static SingleCommand.singleCommand() method passing in the command class we want to get a parser for.
        try {
            Send cmd = parser.parse(args);
We can then invoke the parse() method passing in our users inputs.
            System.exit(cmd.run());
Assuming the parsing is successful we now have an instance of our Send class which we can invoke methods on like any
other Java object.   In this example our business logic is in the run() method so we simply call that method and use
its return value as the exit code.
        } catch (ParseException e) {
            System.err.print(e.getMessage());
            System.exit(1);
        }
    }
Finally if the parsing goes wrong we print the error message and exit with a non-zero return code.
Try this out now:
> ./send-it send --recipient You --number 123 -a "Your Street" -a "Somewhere" --postcode "AB12 3CD" -w 0.5
Sending package weighing 0.500 KG sent via FirstClass costing £0.50
Recipient:
You
123 Your Street
Somewhere
AB12 3CD
> echo $?
0
Typically real world command line interfaces (CLIs) consist of multiple commands e.g. git
Airline allows multiple commands to be composed together into a CLI to support complex applications.
@CliWe use the @Cli annotation to define a CLI, this is applied to classes similar to
@Command e.g. the SendItCli
class:
@Cli(name = "send-it", 
     description = "A demonstration CLI around shipping",
     commands = {
             CheckAddress.class,
             CheckPostcodes.class,
             Send.class,
             Price.class,
             Help.class,
             BashCompletion.class
     },
     defaultCommand = Help.class, 
     parserConfiguration = @Parser(
       useDefaultOptionParsers = true,
       defaultParsersFirst = false,
       optionParsers = { ListValueOptionParser.class },
       errorHandler = CollectAll.class
     )
)
public class SendItCli {
}
Let’s break that down a bit…
As we saw with @Command this is pretty self-explanatory:
@Cli(name = "send-it", 
     description = "A demonstration CLI around shipping",
The name is the name that you expect users to type at the command line, typically you’ll create a Shell script named
this which invokes your actual Java application.
As seen previously description is used to provide descriptive text that will get included in help output.
commands = {
             CheckAddress.class,
             CheckPostcodes.class,
             Send.class,
             Price.class,
             Help.class,
             BashCompletion.class
     },
defaultCommand = Help.class, 
The commands field of the annotation defines the classes that provide your commands.  Each of these must be
appropriately annotated with @Command.
We also see the defaultCommand field used to indicate what command is invoked if the user doesn’t invoke a command.
This can be useful to provide default behaviour and is often used to point to the help system.
NB - Generally it is useful for all your commands to have a common parent class or interface since as we’ll see in a
few slides time we’ll need to declare a type when creating a parser.  In this case all our commands implement ExampleRunnable
Here we see the parser being customised, we’re going to skip over most of this for now and come back to it later:
parserConfiguration = @Parser(
       useDefaultOptionParsers = true,
       defaultParsersFirst = false,
       optionParsers = { ListValueOptionParser.class },
       errorHandler = CollectAll.class
     )
The one important thing to point out here is we are changing the errorHandler to CollectAll which will allow us to
more intelligently handle errors later.
So you probably noticed we had zero logic in the class defining our CLI, similar to our single command example we need
to define an appropriate main() method for our CLI.  This we do in the SendIt class:
public class SendIt {
    public static void main(String[] args) {
        Cli<ExampleRunnable> parser = new Cli<ExampleRunnable>(SendItCli.class);
        try {
            // Parse with a result to allow us to inspect the results of parsing
            ParseResult<ExampleRunnable> result = parser.parseWithResult(args);
            if (result.wasSuccessful()) {
                // Parsed successfully, so just run the command and exit
                System.exit(result.getCommand().run());
            } else {
                // Parsing failed
                // Display errors and then the help information
                System.err.println(String.format("%d errors encountered:", result.getErrors().size()));
                int i = 1;
                for (ParseException e : result.getErrors()) {
                    System.err.println(String.format("Error %d: %s", i, e.getMessage()));
                    i++;
                }
                System.err.println();
                
                Help.<ExampleRunnable>help(parser.getMetadata(), Arrays.asList(args), System.err);
            }
        } catch (Exception e) {
            // Errors should be being collected so if anything is thrown it is unexpected
            System.err.println(String.format("Unexpected error: %s", e.getMessage()));
            e.printStackTrace(System.err);
        }
        
        // If we got here we are exiting abnormally
        System.exit(1);
    }
}
Once again there’s a lot going on, so let’s break it down…
Cli<ExampleRunnable> parser = new Cli<ExampleRunnable>(SendItCli.class);
So firstly we create an instance of the Cli class, not to be confused with the @Cli annotation, referring to our previously introduced class with the @Cli annotation.
As mentioned we need to define a type for the commands that will be parsed. So this is where it is helpful to have all your commands inherit from a common parent class or implement a common interface.
NB You can always use Object here as all Java objects derive from this but this will make the rest of your implementation awkward!
// Parse with a result to allow us to inspect the results of parsing
ParseResult<ExampleRunnable> result = parser.parseWithResult(args);
Here we call the parseWithResult() method passing in the user arguments received by our main() method.  This will
give us a ParseResult instance that we can inspect to see if parsing succeeded:
if (result.wasSuccessful()) {
    // Parsed successfully, so just run the command and exit
    System.exit(result.getCommand().run());
Assuming successful parsing we can simply call getCommand() on our result and then invoke its run() method since
all our commands implement a common interface.
Alternatively we could have just called parse(args) which would return either the parsed command, throw an exception
or return null depending on the user inputs and the parser configuration.
If parsing wasn’t successful then we need to do something about that. We specified a different error handler earlier that allows us to collect up all the errors:
} else {
     // Parsing failed
     // Display errors and then the help information
     System.err.println(String.format("%d errors encountered:", result.getErrors().size()));
     int i = 1;
     for (ParseException e : result.getErrors()) {
        System.err.println(String.format("Error %d: %s", i, e.getMessage()));
        i++;
     }
So we loop over all the errors printing them out
     System.err.println();
                
     Help.<ExampleRunnable>help(parser.getMetadata(), Arrays.asList(args), System.err);
  }
Followed by invoking the help system to display the help for the CLI.
As noted earlier we usually want to create an entry point shell script for our CLI that matches the name declared in our
@Cli annotation.
Here’s the contents of send-it:
#!/usr/bin/env bash
JAR_FILE="target/airline-examples.jar"
if [ ! -f "${JAR_FILE}" ]; then
  echo "Examples JAR ${JAR_FILE} does not yet exist, please run mvn package to build"
  exit 1
fi
java -cp "${JAR_FILE}" com.github.rvesse.airline.examples.sendit.SendIt "$@"
To run our CLI we just need to invoke the script i.e.
> ./send-it
send-itSince we defined our default command to be the help command we get useful output:
> ./send-it
usage: send-it <command> [ <args> ]
Commands are:
    check-address          Check if an address meets our restrictions
    check-postcodes        Checks whether postcodes are valid
    generate-completions   Generates a Bash completion script, the file can then be sourced to provide completion for this CLI
    help                   A command that provides help on other commands
    price                  Calculates the price for a parcel
    send                   Sends a package
See 'send-it help <command>' for more information on a specific command.
Why not try asking for help on the send command we saw earlier:
> ./send-it help send
So how did that last demo work?
Airline includes a help system that can generate help in a variety of formats plus prebuilt commands and options that can be added into your commands/CLIs.
We can invoke help in a number of ways:
HelpOption into our commandsHelp command into our CLIsLet’s see the difference between each.
HelpOption to our commandsThis is a pre-built class which defines a -h/--help option, therefore we can compose this using
@AirlineModule as seen earlier:
@Command(name = "simple", description = "A simple example command")
public class Simple implements ExampleRunnable {
    @AirlineModule
    private HelpOption<Simple> help;
    
    // Rest of implementation omitted for brevity
    
    @Override
    public int run() {
        if (help.showHelpIfRequested())
            return 0;
            
        System.out.println("Flag was " + (this.flag ? "set" : "not set"));
        System.out.println("Name was " + this.name);
        System.out.println("Number was " + this.number);
        if (args != null)
            System.out.println("Arguments were " + StringUtils.join(args, ","));
 
        return 0;
    }
}
We can see in the run() method we call the showHelpIfRequested() method to check if the user requested help.  If
this returns true then help was requested and has been shown so we just exit.  If this returns false then we
continue with our normal logic.
Let’s try that:
> ./runExample Simple
Flag was not set
Name was null
Number was 0
> ./runExample Simple --help
NAME
        simple - A simple example command
...
Help commandAirline includes a pre-built Help command that implements Runnable and Callable<Void>.  If either of these are the common interface for your commands you can simply add this to your commands in your @Cli declaration e.g.
@Cli(name = "send-it", 
     description = "A demonstration CLI around shipping",
     commands = {
             CheckAddress.class,
             CheckPostcodes.class,
             Send.class,
             Price.class,
             Help.class,
             BashCompletion.class
     })
public class SendItCli {
     
}
If you use a different interface then you can simply extend this class and have it implement your interface and call the run() method from the base class e.g. CustomHelp
@Command(name = "help", description = "Shows help")
public class CustomHelp extends Help<YourInterface> implements YourInterface {
    @Override
    public void execute() {
        super.run();
    }
}
If the previous two approaches don’t work then you can always invoke help manually. You can do this in two ways:
Even if you can’t incorporate the Help command directly you can call its static help methods e.g. Help
@Command(name = "help", 
                     description = "A command that provides help on other commands")
public class Help implements ExampleRunnable {
    @AirlineModule
    private GlobalMetadata<ExampleRunnable> global;
    @Arguments(description = "Provides the name of the commands you want to provide help for")
    @BashCompletion(behaviour = CompletionBehaviour.CLI_COMMANDS)
    private List<String> commandNames = new ArrayList<String>();
    
    @Option(name = "--include-hidden", description = "When set hidden commands and options are shown in help", hidden = true)
    private boolean includeHidden = false;
    @Override
    public int run() {
        try {
            com.github.rvesse.airline.help.Help.help(global, commandNames, this.includeHidden);
        } catch (IOException e) {
            System.err.println("Failed to output help: " + e.getMessage());
            e.printStackTrace(System.err);
            return 1;
        }
        return 0;
    }
}
Here we call the static help() method passing in the metadata for our CLI (which Airline populated for us) plus the command names that help was requested for.
Alternatively we can create and use a help generator directly e.g. GenerateHelp
@Command(name = "generate-help")
public class GenerateHelp {
    public static void main(String[] args) {
        Cli<ExampleRunnable> cli = new Cli<ExampleRunnable>(SendItCli.class);
        
        GlobalUsageGenerator<ExampleRunnable> helpGenerator = new CliGlobalUsageGenerator<>();
        try {
            helpGenerator.usage(cli.getMetadata(), System.out);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
Here we create a specific instance of a GlobalUsageGenerator and call the usage() method to generate the help.
So we have seen:
@Option and @Arguments
        @Command@CliThis is everything you need to make a functional CLI with Airline.
The user guide which has been linked throughout these slides covers all these topics, plus many more, in lots more detail and examples. Find it at http://rvesse.github.io/airline/
Please try it out, post questions, problems etc. at http://github.com/rvesse/airline/issues
Please feel free to ask questions now!
The remainder of the slides give a quick tour of some of the more advanced features of the library. If we have any extra time left we’ll take a look at these…
As we glossed over earlier we can optionally customise our parser to change the command line behaviour in a variety of ways.
So we saw earlier in our SendItCli example the parser being customised via the parserConfiguration field of the
@Cli annotation.  Let’s look more into that now.
@ParserThe @Parser annotation can be used in two ways:
@Command, this customises the parser for
SingleCommand based parsersparserConfiguration field of the @Cli annotation, this customises the
parser for Cli based parsersThere are lots of behaviours that can be customised with this annotation e.g.
By default Airline parses three common option styles in the following order of preference:
StandardOptionParser - Simple white space separated
option and values e.g. --name value sets the option --name to valueLongGetOptParser - Long form GNU getopt style e.g.
--name=value sets the option --name to valueClassicGetOptParser - Short form GNU getopt style e.g.
-n1 sets the option -n to 1This can be customised via several fields of the @Parser annotation e.g.
parserConfiguration = @Parser(
       useDefaultOptionParsers = true,
       defaultParsersFirst = false,
       optionParsers = { ListValueOptionParser.class }
     )
useDefaultOptionParsers indicates whether to use this default setupdefaultParsersFirst controls whether the defaults parsers are preferred in favour of any additional ones specifiedoptionParsers specifies additional option parsers to useA couple of additional styles are built-in but not enabled by default:
MaybePairValueOptionParser - Arity 2 options where the
user may specify the values as whitespace/= separated e.g. --name foo bar and --name foo=bar are both acceptable
and set the option --name to the values foo and barListValueOptionParser - Options that may be specified
multiple times can be specified in a compact comma separated list form e.g. --name foo,bar sets the option --name
to the values foo and bar.NB - Power Users can also create Custom Option Parsers if desired.
Airline allows for customising how it interprets numeric values passed to any @Option/@Arguments annotated field
that has a numeric type i.e. byte, short, int, long, float and double or their boxed equivalents.
This can be controlled either globally on the @Parser annotation with the numericTypeConverter field or on a
per-option basis by using the typeConverterProvider e.g.
@Parser(numericTypeConverter=Hexadecimal.class)
Or:
    @Option(name = { "-b", "--bytes"}, 
            description = "Quantity of bytes, optionally expressed in compact form e.g. 1g",
            typeConverterProvider = KiloAs1024.class)
    @Required
    private Long bytes;
Let’s try that:
> ./runExample ByteCalculator --bytes 4gb
4,294,967,296 Bytes
Exiting with Code 0
> ./runExample ByteCalculator --bytes 16k
16,384 Bytes
Exiting with Code 0
Manual pages are provided by using help generator as seen earlier.  This is provided in the separate
airline-help-man module.
If we use ManCommandUsageGenerator or
ManGlobalUsageGenerator our output
will be Troff plus man extensions that can be rendered by the man command.
For example the Manuals
class demonstrates this:
@Command(name = "generate-manuals", description = "Generates manual pages for this CLI that can be rendered with the man tool")
public class Manuals implements ExampleRunnable {
    @AirlineModule
    private GlobalMetadata<ExampleRunnable> global;
    @Option(name = "--include-hidden", description = "When set hidden commands and options are shown in help", hidden = true)
    private boolean includeHidden = false;
    @Override
    public int run() {
        try (FileOutputStream output = new FileOutputStream(this.global.getName() + ".1")) {
            new ManGlobalUsageGenerator<ExampleRunnable>(ManSections.GENERAL_COMMANDS).usage(this.global, output);
            System.out.println("Generated manuals to " + this.global.getName() + ".1");
        } catch (IOException e) {
            System.err.println("Error generating completion script: " + e.getMessage());
            e.printStackTrace(System.err);
        }
        return 0;
    }
}
Let’s get and view the output:
> ./send-it generate-manuals
Generated manuals to send-it.1
> man ./send-it.1
By the same mechanism we can also do Bash completion, we may need to use the additional
@BashCompletion annotation on our option/argument fields to control how we’d like Bash to complete things.
We can then create a command like BashCompletions:
@Command(name = "generate-completions", description = "Generates a Bash completion script, the file can then be sourced to provide completion for this CLI")
public class BashCompletion implements ExampleRunnable {
    @AirlineModule
    private GlobalMetadata<ExampleRunnable> global;
    
    @Option(name = "--include-hidden", description = "When set hidden commands and options are shown in help", hidden = true)
    private boolean includeHidden = false;
    @Override
    public int run() {
        try (FileOutputStream out = new FileOutputStream(this.global.getName() + "-completions.bash")) {
            new BashCompletionGenerator<ExampleRunnable>(this.includeHidden, false).usage(global, out);
            System.out.println("Generated completion script " + this.global.getName() + "-completions.bash");
        } catch (IOException e) {
            System.err.println("Error generating completion script: " + e.getMessage());
            e.printStackTrace(System.err);
        }
        return 0;
    }
}
Similar to our previous example this creates a script that we can source. Let’s try that out:
> ./send-it generate-completions
Generated completion script send-it-completions.bash
> source send-it-completions.bash
> ./send-it <tab>
check-address         check-postcodes       generate-completions  generate-manuals      help                  price                 send 
There is also a Maven Plugin that can generate the help as part of your build process e.g.
      <plugin>
        <groupId>com.github.rvesse</groupId>
        <artifactId>airline-maven-plugin</artifactId>
        <version>3.2.0</version>
        <configuration>
          <defaultOptions>
            <multiFile>true</multiFile>
          </defaultOptions>
          <formats>
            <format>BASH</format>
            <format>MAN</format>
          </formats>
          <sources>
            <source>
              <classes>
                <class>com.github.rvesse.airline.examples.sendit.SendItCli</class>
              </classes>
            </source>
          </sources>
        </configuration>
        <executions>
          <execution>
            <goals>
              <goal>generate</goal>
            </goals>
          </execution>
        </executions>
      </plugin>
Added to your pom.xml would generate both Bash Completion scripts and Manual pages as part of your build.