GNU GetOpt
Table of Contents
| Package | Purpose |
|---|---|
| AdaCL.Command_Line.GetOpt | GNU GetOpt style command line parser with wide character support |
From AdaCL 8.0 this package lives in the adacl_desktop crate. Applications that still depend on adacl for
command-line parsing must switch that dependency to adacl_desktop.
A modern object-oriented version of GetOpt made for Ada — that is, without the C-style global optind / optarg state.
Unlike GNAT.Command_Line this package is re-entrant. All parser state is kept inside the tagged instance, so two tasks
can parse the command line independently. AdaCL.Trace relies on that: it runs its own parser during elaboration
without disturbing the application’s parser.
The parser accepts Wide_Wide_Character short options, Wide_Wide_String long options, and Wide_Wide_String file
names, so option letters and path names may contain characters outside Latin-1.
Architecture #
Most Ada command-line parsers are a loop and a large case. AdaCL.Command_Line.GetOpt inverts that: you derive a
tagged type and override the operations that correspond to the kinds of token the scanner can return.
type Object is new AdaCL.Command_Line.GetOpt.Object with private;
overriding procedure Parse (This : in out Object);
overriding procedure Write_Help (This : in Object);
overriding procedure Analyze_Without_Argument (This : in out Object);
overriding procedure Analyze_With_Argument (This : in out Object);
overriding procedure Analyze_GNU (This : in out Object);
overriding procedure Analyze_File (This : in out Object);
private
package Inherited renames AdaCL.Command_Line.GetOpt;
type Object is new Inherited.Object with record
Operation : Operation_Type := None;
In_File : Ada.Strings.Unbounded.Unbounded_String := Ada.Strings.Unbounded.Null_Unbounded_String;
Out_File : Ada.Strings.Unbounded.Unbounded_String := Ada.Strings.Unbounded.Null_Unbounded_String;
With_Code : Boolean := True;
Quotes : Quotes_Type := HP;
end record;
Parse is the public entry point. Its inherited implementation walks the command line with Next and dispatches:
Found_Flag from Next |
Override that is called |
|---|---|
Without_Argument |
Analyze_Without_Argument |
With_Argument |
Analyze_With_Argument |
GNU_Style |
Analyze_GNU |
No_Option |
Analyze_File |
End_Of_Options |
scanning stops |
Error |
Option_Parse_Error when Exception_On_Error is true |
Each override handles the application’s own options and then calls the inherited operation for the options that the base
type already understands (--help / -?).
Pattern string #
Override Parse to install the short-option pattern before calling the inherited Parse. The pattern is the POSIX
optstring: every short option character, with Option_Argument (:) immediately after those that take a value.
Decode_Long : constant Wide_Wide_String := "decode";
Decode_Short : constant Wide_Wide_Character := 'd';
Encode_Long : constant Wide_Wide_String := "encode";
Encode_Short : constant Wide_Wide_Character := 'e';
Quotes_Long : constant Wide_Wide_String := "quotes";
Quotes_Short : constant Wide_Wide_Character := 'q';
Quotes_HP : constant Wide_Wide_String := "hp";
Quotes_Single : constant Wide_Wide_String := "single";
Quotes_Double : constant Wide_Wide_String := "double";
Quotes_Back : constant Wide_Wide_String := "back";
Pattern : constant Wide_Wide_String :=
[GetOpt.Option_Error,
Decode_Short,
Encode_Short,
Quotes_Short,
GetOpt.Option_Argument];
-- Equivalent to "?deq:" : help, decode, encode, and quotes with argument.
overriding procedure Parse (This : in out Object) is
Super : Inherited.Object renames Inherited.Object (This);
begin
This.Set_Pattern (Pattern);
This.Set_Exception_On_Error (True);
This.Set_Extract_GNU (True);
Super.Parse;
end Parse;
Useful constants from the spec:
| Constant | Value | Role |
|---|---|---|
Option_Marker |
'-' |
Introduces a short option (-d) and, doubled, a GNU long option (--decode) |
Option_Argument |
':' |
Marks the preceding short option as requiring a value |
Option_Error / Help_Short |
'?' |
Unknown-option marker and the built-in short help option |
Help_GNU |
"help" |
Built-in long help option --help |
Call Set_Extract_GNU (True) when the program should accept --name and --name=value. Long option names themselves
are not listed in the pattern string; they are recognised in Analyze_GNU.
Write_Help #
Override Write_Help for -? and --help. The inherited operation prints the built-in help option. Call it last so
the common lines appear after the application-specific ones.
Write_Help takes This as in Object. Help text must not mutate parser state; Help_Shown is set by the scanner
when the help option is seen.
overriding procedure Write_Help (This : in Object) is
pragma Debug (AdaCL.Trace.Entering (AdaCL.Trace.Parameter & This'Image));
use Ada.Text_IO;
use GetOpt;
Super : Inherited.Object renames Inherited.Object (This);
begin
New_Line;
Put_Line ("hp41cx_tools-main:");
New_Line;
Put_Line (" PX-41CX memory dump tool");
New_Line;
Put_Line ("Usage:");
New_Line;
Put_Line (" hp41cx_tools-main [command] in_file out_file");
New_Line;
Put_Line ("Commands:");
New_Line;
Put_Help_Line (Decode_Short, Decode_Long, "Decode PX-41CX memory dump to source code.");
Put_Help_Line (Encode_Short, Encode_Long, "Encode PX-41CX source code to memory dump.");
New_Line;
Put_Line ("General options:");
New_Line;
Put_Help_Line (Quotes_Short, Quotes_Long, Quotes_HP, "Use HP-41CX style quotes (⊤) for alpha strings.");
Put_Help_Line (Quotes_Short, Quotes_Long, Quotes_Single, "Use single quote (') for alpha strings.");
Put_Help_Line (Quotes_Short, Quotes_Long, Quotes_Double, "Use double quote ("") for alpha strings.");
Put_Help_Line (Quotes_Short, Quotes_Long, Quotes_Back, "Use backticks (`) for alpha strings.");
New_Line;
Put_Line ("Other options:");
New_Line;
Put_Line
("Version " & Hp41cx_Tools_Config.Crate_Version &
" (https://calculator-scripts.sourceforge.io/hp41cx-tools/).");
New_Line;
Super.Write_Help;
pragma Debug (AdaCL.Trace.Exiting);
end Write_Help;
Put_Help_Line is overloaded so a line can show a long option only, a short plus long option, or either of those with a
parameter name. The preconditions cap the option columns so the description stays aligned; keep long names and parameter
labels short.
Analyze_Without_Argument #
Called for a short option that is not followed by : in the pattern (-d, -e, -?).
overriding procedure Analyze_Without_Argument (This : in out Object) is
Super : Inherited.Object renames Inherited.Object (This);
Option : constant Wide_Wide_Character := This.Get_Option;
begin
if Option = Decode_Short then
This.Check_Command_None;
This.Operation := Decode;
elsif Option = Encode_Short then
This.Check_Command_None;
This.Operation := Encode;
else
Super.Analyze_Without_Argument;
end if;
end Analyze_Without_Argument;
The inherited call handles the built-in short help option. Unrecognised short options become Option_Parse_Error when
exception-on-error is enabled.
Analyze_With_Argument #
Called for a short option that is followed by : in the pattern. Get_Argument is the option value, not the next
positional file name.
overriding procedure Analyze_With_Argument (This : in out Object) is
Super : Inherited.Object renames Inherited.Object (This);
Option : constant Wide_Wide_Character := This.Get_Option;
Argument : constant Wide_Wide_String := This.Get_Argument;
begin
if Option = Quotes_Short then
This.Set_Quotes (Argument);
else
Super.Analyze_With_Argument;
end if;
end Analyze_With_Argument;
Analyze_GNU #
Called for --name and --name=value when GNU extraction is enabled. Get_GNU_Option is the name without the leading
dashes; Get_Argument is empty when the user omitted =value.
overriding procedure Analyze_GNU (This : in out Object) is
Super : Inherited.Object renames Inherited.Object (This);
Option : constant Wide_Wide_String := This.Get_GNU_Option;
Argument : constant Wide_Wide_String := This.Get_Argument;
begin
if Option = Decode_Long then
This.Check_Command_None;
This.Operation := Decode;
elsif Option = Encode_Long then
This.Check_Command_None;
This.Operation := Encode;
elsif Option = Quotes_Long then
This.Set_Quotes (Argument);
else
Super.Analyze_GNU;
end if;
end Analyze_GNU;
The inherited call handles --help.
Analyze_File #
Called for every token that is not an option: file names, subcommands written without a dash, and anything after a bare
-- if the scanner treats that as the end of options. The name is historical; the token need not be a file.
Parsing file names #
overriding procedure Analyze_File (This : in out Object) is
Super : Inherited.Object renames Inherited.Object (This);
Argument : constant Wide_Wide_String := This.Get_Argument;
begin
Super.Analyze_File;
if This.In_File = Ada.Strings.Unbounded.Null_Unbounded_String then
This.In_File := AdaCL.Wide_Wide_Strings.To_Unbounded_String (Argument);
elsif This.Out_File = Ada.Strings.Unbounded.Null_Unbounded_String then
This.Out_File := AdaCL.Wide_Wide_Strings.To_Unbounded_String (Argument);
else
raise Option_Wrong_Error
with "Only two file arguments expected";
end if;
end Analyze_File;
The first positional argument is stored as the input file, the second as the output file. A third positional is rejected
with Option_Wrong_Error: the tokens are well-formed, they just do not belong together.
An application that accepts any number of files can append Argument to an Ada.Containers.Indefinite_Vectors vector
of Wide_Wide_String. That container already holds unconstrained strings, so there is no need to go through
Unbounded_String. Convert to UTF-8 only when the file name is handed to an API that expects String.
Parsing other data #
Analyze_File is also the hook for positional data that is not a path. The Swiss Micros tools accept a single time
stamp YYYYMMDD HHMMSS:
package GetOpt renames AdaCL.Command_Line.GetOpt;
package SB_P renames AdaCL.Wide_Wide_Strings.Spitbol.Patterns;
package S_MC renames Ada.Strings.Wide_Wide_Maps.Wide_Wide_Constants;
Digit_Pattern : constant SB_P.Pattern := SB_P.Any (S_MC.Decimal_Digit_Set);
Date_Pattern : constant SB_P.Pattern := SB_P.Replicate (Digit_Pattern, 8);
Time_Pattern : constant SB_P.Pattern := SB_P.Replicate (Digit_Pattern, 6);
Date_Time_Pattern : constant SB_P.Pattern :=
SB_P.Pos (0) & Date_Pattern & " " & Time_Pattern;
overriding procedure Analyze_File (This : in out Object) is
pragma Debug (AdaCL.Trace.Entering (In_Parameter => This'Image));
Super : Inherited.Object renames Inherited.Object (This);
Argument : constant Wide_Wide_String := This.Get_Argument;
begin
Super.Analyze_File;
if This.Time (1) /= ' ' then
raise GetOpt.Option_Wrong_Error
with "Time already set. Only one time can be specified.";
end if;
if Argument'Length /= 15
or else not SB_P.Match (Argument, Date_Time_Pattern)
then
raise GetOpt.Option_Parse_Error
with "Invalid time format. Expected: YYYYMMDD HHMMSS";
end if;
This.Time := AdaCL.Wide_Wide_Strings.To_String (Argument);
pragma Debug (AdaCL.Trace.Exiting (Out_Parameter => This'Image));
end Analyze_File;
This.Time is a space-filled string of length 15. A non-space in the first position means a time stamp has already been
stored, so a second positional token is Option_Wrong_Error. A token that is the wrong length, or that fails the
SPITBOL pattern, is Option_Parse_Error: the text itself is not a time stamp.
The pattern is anchored at the start with Pos (0). Combined with the length check of 15 (eight digits, one space, six
digits) that is enough; the pattern does not need an end anchor. Conversion to String is safe here because a
successful match contains only decimal digits and a space.
Running the parser #
Try :
declare
Options : Command_Line.Object;
begin
Options.Parse;
-- Options now holds the parsed flags and file names.
-- Stop if the user only asked for help.
if not Options.Help_Shown then
Options.Run;
end if;
exception
when An_Exception : GetOpt.Option_Parse_Error =>
-- malformed option or missing option value
null;
when An_Exception : GetOpt.Option_Missing_Error =>
-- a required option was omitted
null;
when An_Exception : GetOpt.Option_Wrong_Error =>
-- legal options that do not make sense together
null;
when An_Exception : others =>
null;
end Try;
| Exception | Typical cause |
|---|---|
Option_Parse_Error |
Unknown option, or a required option value is missing |
Option_Missing_Error |
A required option was never given |
Option_Wrong_Error |
A combination the application rejects (Check_Command_None in the examples) |
After a successful Parse, inspect the fields you added to the derived type. Get_Optind is the 1-based index of the
last argument consumed, matching POSIX optind for readers who know the C API.
Re-entrancy #
Create one instance per parsing context. Nothing in the package is stored at library level, so:
- the application can parse
Ada.Command_Lineinmain; AdaCL.Trace.Initializecan parse the same command line for--traceoptions at the same time;- a test harness can feed several instances without resetting hidden package state.
Next is the primitive scanner if a derived type needs a custom dispatch loop. New applications should override Parse
and the Analyze_* operations instead.
Examples #
- AdaCL.Trace
- Small parser used from
Initialize. Shows that the parser is re-entrant: Trace and the application each have their own instance. - HP41CX_Tools.Commandline
- Full parser for the HP-41CX emulator tools. The fragments above are taken from this program.
- DM_Set_Time.Command_Line
- Small parser who for a date time string. One of the fragments above are taken from this program.
- Atr_Tools.CommandLine
- Full parser for Atari / SIO2PC ATR-file tools.
- Check_EXE.CommandLine
- Full parser for Atari EXE-file tools.
Name of the package #
The child name GetOpt is a deliberate exception to Ada mixed-case-with-underscores. POSIX, GNU libc, and
GNAT.Command_Line.Getopt all treat getopt as a single well-known token. Renaming the package to Get_Opt would be
style-guide-correct and would match the 8.0 identifier cleanup, but it would force every existing with clause and file
name to change on top of the crate split. The recommended 8.0 spelling is therefore to keep the child as GetOpt and to
use underscored names for every new identifier inside the package.