In the previous installment in this series we looked at how to set up an Ada development environment, and how to compile and run a simple Ada application. Building upon this foundation, we will now look at how to create more complex applications, along with how to parse and use arguments passed to Ada applications on the command line (CLI). After all, passing flags and strings to CLI applications when we launch them is a crucial part of user interaction, as well as when automating systems as is the case with system services.
The way that a program is built-up is also essential, as well-organized code eases maintenance and promotes code reusability through e.g. modularity. In Ada you can organize subprograms (i.e. functions and procedures) in a declarative fashion as stand-alone units, as well as embed subprograms in other subprograms. Another option is packages, which roughly correspond to C++ namespaces, while tagged types are the equivalent of classes. In the previous article we already saw the use of a package, when we used the Ada.Text_IO
package to output text to the CLI. In this article we’ll look at how to write our own alongside handling command line input, after a word about the role of the binding phase during the building of an Ada application.
Binding And Linking
The main task of the binder in GNAT is to perform elaboration. In most languages a linker is run immediately after the compiler, linking together the compiled code objects using defined and exported symbols into a finished binary. In contrast, the goal of elaboration is to take the output of the compiler and determine (elaborate) the way that the units fit together using information written into .ali
files (short for Ada Library Information) by the compiler. If no errors are detected, the binder generates a main program, which is compiled and linked together with the other object files into the final binary.
The main advantage of this approach is that of consistency, as in e.g. C and C++ it is possible to link incompatible objects, resulting in strange behavior and quaint crashes that make no sense until you realize that for example you were using the wrong header files to go with a particular library. When it comes to linking against libraries or object files written in another language than Ada, this elaboration process is bypassed, and issues may not crop up until the first time that the application is executed. Although Ada defines support for easy interfacing with C, COBOL and Fortran (and C++ in GNAT) via the Interfaces package, this is a trade-off to keep in mind.
We’ll look at interfacing with other languages in an upcoming article, but for now we will consider just pure Ada code.
Starting An Argument
The string of text that we pass along with the application name to the shell is parsed and made available inside the application code as a collection of text fragments, split by the spaces between them. Using this data in an efficient fashion can be somewhat tricky, especially if we wish to provide the user with the ability to specify flags and data specific to these flags. For this reason you may wish to use an existing library or similar which provides features like defining a help message and automatic parsing of flags.
As an example of this, the Sarge project that I wrote for C++ and Ada provides these features, along with an example project which we can analyze along with the library code. The Ada version comes in the form of a single package called Sarge
, which we can then use with the application: with Sarge;
Because unlike the C++ version, Ada Sarge is only contained in a package and not a tagged type, we do not create an instance. This means that to for example set a flag which the library should parse, we write the following:
Sarge.setArgument(+"h", +"help", +"Get help.", False);
This will instruct Sarge to look for the long (-h
) and short (--help
) flags. The boolean false
indicates that we are not expecting a string to follow this flag. The +
prefix on the string literals in the argument list is not a standard feature of Ada, but a renamed function to convert bounded strings to unbounded strings:
function "+"(S : in String) return Unbounded_String renames Ada.Strings.Unbounded.To_Unbounded_String;
The default string type in Ada is a bounded string, meaning that it has a fixed length. Since we do not know beforehand what the length of a flag name or CLI argument string is going to be, we use unbounded strings from the Ada.Strings.Unbounded
package. Since Ada is a strongly typed language, the conversion from a bounded string literal to an unbounded string has to be performed explicitly, yet we do not wish to type the same long function name over and over. Ergo we rename it to a simple function called +
that gets put in front of string literals, which handles this almost invisibly.
Helpful Parsing
We’re now ready to parse the CLI input, which is handled by a simple call to Sarge:
if Sarge.parseArguments /= True then put_line("Couldn't parse arguments..."); return; end if;
This also demonstrates the way that in Ada comparisons are denoted, with ‘equals’ being =
and ‘not equal’ being /=
, as compared to the assignment operator being :=
. With this if/else statement we make sure that Sarge had no problems parsing the CLI input using the parseArguments()
function. As this function is too long to list here, feel free to look at the whole file here, while I’ll point out the salient bits here.
The functions we’re interested in are in the Ada.Command_Line
package, which gives us access to whatever the shell has passed on to our application, such as the executable name:
execName := +Ada.Command_Line.command_name;
Here again we convert the bounded string into an unbounded string, before moving on to filtering for flags. Short flags start with a single single dash, while long flags start with a double dash. While not an Ada standard, this is a fairly conventional way to pass flags across platforms. This enables us to differentiate the two types of flags, as we check each CLI argument in a loop, as in this condensed version of the actual function:
for arg_i in 1..Ada.Command_Line.argument_count loop arg := +Ada.Command_Line.Argument(arg_i); if Ada.Strings.Unbounded.Slice(arg, 1, 1) = "-" then if Ada.Strings.Unbounded.Slice(arg, 1, 2) = "--" then -- Long form of the flag. -- First delete the preceding dashes. arg := Ada.Strings.Unbounded.Delete(arg, 1, 2); if not argNames.contains(arg) then Ada.Strings.Unbounded.Text_IO.put_line("Long flag " & arg & " wasn't found"); return False; end if; flag_it := argNames.find(arg); args(argNames_map.Element(flag_it)).parsed := True; end if; end if; end loop;
There’s quite a bit going on in this loop, but essentially we’re reading each of the argument values passed on the CLI starting at index 1 (because 0 is always the executable name), looping until we hit the number of total arguments, after which the loop terminates. Each argument is again converted to an unbounded string, after which we check in a comparison whether it starts with a dash. If it does, we can check if it’s a short or long flag type. In this condensed version we only look for the long form, but the short version is similar.
If it’s an unknown flag (i.e. not set via setArgument
), then we bail out and return false. In the full version we also ensure that if a flag requires a value to follow it that this is the case. The flags and values are stored in both a map and vector, which are two of the standard library containers.
Contained Arguments
When we set a flag to look for in Sarge, it is added to the argNames
map, as well as the args
vector, with the latter containing the full Record (like a struct in C). The map is used to provide an easily searchable index into the vector which contains the records. Since this topic has us diving pretty deep into containers in Ada, this will be covered in depth in the following article in this series.
We’ll finish this article by quickly covering how the Sarge API is used to access the parsed CLI arguments. As can be seen in the screenshot, we can request the total number of found flags, and query per flag whether it was found, or directly obtain its value (if any) if it was found. This then allows us to act upon any found flag, as well as a trailing text segment (e.g. a file path), making something like Sarge into a modular package that can be used with any CLI-based application without having to write all of the code again just to parse CLI arguments.
As said, in the next installment we’ll look at the use of Ada standard containers, as well as record types. Feel free to sound off in the comments about any related topics that you’d like to see covered.
“Ada.Strings.Unbounded.Text_IO.put_line(…”
Thank goodness they didn’t think of calling the language Lovelace instead of Ada. The irony of such as verbose language having such a short name!
The article kind of missed on why should the strings be converted. If they are already inside some struct/object on the library or the runtime, then the system already knows their length. So, why convert to unbound ?
RIPL please that’s the way to learn new language!
Are you telling us there’s a REPL environment for Ada, or are you asking Maya to create such a tool for us where none exists?
https://en.wikipedia.org/wiki/Read%E2%80%93eval%E2%80%93print_loop
For Sarge_Test.adb in a REPL see Compiler Explorer (CE, godbolt) here:
https://godbolt.org/z/n3aMPGGW1
Created it from the Ada template, see ‘Templates’ in the taskbar at the top of CE.