CLI Framework Technical Documentation
Documentation home · How do I...? · API reference · Current limitations
This is a maintainer-level guide to parser flow, object ownership, help and completion generation, console behaviour, and tests. Application authors looking for public usage examples should start with the first-CLI guide.
All Pascal blocks on this page are source-context excerpts from the unit named by their heading or surrounding text. They describe implementation and interfaces, not standalone application snippets; use the reader guides for a defined command class, command instance, and application setup.
Architecture Overview
The Free Pascal CLI Framework is built on a modular, interface-based architecture that promotes extensibility and maintainability. The framework is organized into several key components that work together to provide a complete CLI solution.
classDiagram
class ICLIApplication {
<<interface>>
+RegisterCommand(Command: ICommand)
+Execute(): Integer
}
class ICommand {
<<interface>>
+GetName(): string
+GetDescription(): string
+GetParameters(): TArray<ICommandParameter>
+GetSubCommands(): TArray<ICommand>
+Execute(): Integer
}
class ICommandParameterReceiver {
<<interface>>
+SetParsedParams(Params: TStringList)
}
class ICommandParameter {
<<interface>>
+GetShortFlag(): string
+GetLongFlag(): string
+GetDescription(): string
+GetRequired(): Boolean
+GetParamType(): TParameterType
+GetDefaultValue(): string
+GetAllowedValues(): string
}
class IProgressIndicator {
<<interface>>
+Start()
+Stop()
+Update(Progress: Integer, Caption: string = '')
}
class TCLIApplication {
-FName: string
-FVersion: string
-FRootCommand: ICommand
-FCommands: TCommandList
-FCurrentCommand: ICommand
-FParsedParams: TStringList
-FParamStartIndex: Integer
-FDebugMode: Boolean
-FArguments: TStringArray
+RegisterCommand(Command: ICommand)
+Execute(): Integer
+RootCommand: ICommand
-ParseCommandLine()
-ShowHelp()
-ShowCommandHelp()
-ShowCompleteHelp()
}
class TBaseCommand {
-FName: string
-FDescription: string
-FParameters: array of ICommandParameter
-FSubCommands: array of ICommand
-FParsedParams: TStringList
+AddParameter(Parameter: ICommandParameter)
+AddSubCommand(Command: ICommand)
+SetParsedParams(Params: TStringList)
+Execute(): Integer
#GetParameterValue(Flag: string, out Value: string): Boolean
}
class TCLIHelpRenderer {
<<internal>>
+ShowGeneral()
+ShowCommand()
+ShowComplete()
+ShowBrief()
}
class TCLICompletionEngine {
<<internal>>
+Complete(Tokens): TStringList
}
class CLIInternalCompletionScripts {
<<internal unit>>
+RenderBashCompletionScript()
+RenderPowerShellCompletionScript()
}
class CLIInternalParameterValues {
<<internal unit>>
+TryGetParameterValue()
+RedactParameterValue()
+RedactArgument()
}
class TCommandParameter {
-FShortFlag: string
-FLongFlag: string
-FDescription: string
-FRequired: Boolean
-FParamType: TParameterType
-FDefaultValue: string
-FAllowedValues: string
+Create(ShortFlag, LongFlag, Description: string, Required: Boolean, ParamType: TParameterType, DefaultValue: string, AllowedValues: string)
}
class TProgressIndicator {
#FActive: Boolean
#FLastRenderLength: Integer
+Start()
+Stop()
+Update(Progress: Integer, Caption: string = '')*
#RenderText(Text: string)
}
class TProgressBar {
-FTotal: Integer
-FWidth: Integer
-FLastProgress: Integer
-FLastCaption: string
+Create(Total: Integer, Width: Integer)
+Update(Progress: Integer, Caption: string = '')
}
class TSpinner {
-FStyle: TSpinnerStyle
-FFrame: Integer
-FFrames: array of string
+Create(Style: TSpinnerStyle)
+Update(Progress: Integer, Caption: string = '')
}
class TConsole {
-FDefaultAttr: Word
+SetForegroundColor(Color: TConsoleColor)
+SetBackgroundColor(Color: TConsoleColor)
+ResetColors()
+Write(Text: string)
+WriteLn(Text: string)
+ClearLine()
+MoveCursorUp(Lines: Integer)
+MoveCursorDown(Lines: Integer)
+MoveCursorLeft(Columns: Integer)
+MoveCursorRight(Columns: Integer)
+SaveCursorPosition()
+RestoreCursorPosition()
}
ICLIApplication <|.. TCLIApplication
ICommand <|.. TBaseCommand
ICommandParameterReceiver <|.. TBaseCommand
ICommandParameter <|.. TCommandParameter
IProgressIndicator <|.. TProgressIndicator
TProgressIndicator <|-- TProgressBar
TProgressIndicator <|-- TSpinner
TCLIApplication --> ICommand
TCLIApplication ..> TCLIHelpRenderer
TCLIApplication ..> TCLICompletionEngine
TCLIApplication ..> CLIInternalCompletionScripts
TCLIApplication ..> CLIInternalParameterValues
TBaseCommand --> ICommandParameter
TBaseCommand --> ICommand
TBaseCommand ..> TCLIHelpRenderer
TBaseCommand ..> CLIInternalParameterValues
Core Components
1. Interfaces (CLI.Interfaces)
ICommand
ICommand = interface
function GetName: string;
function GetDescription: string;
function GetParameters: specialize TArray<ICommandParameter>;
function GetSubCommands: specialize TArray<ICommand>;
function Execute: Integer;
property Name: string read GetName;
property Description: string read GetDescription;
property Parameters: specialize TArray<ICommandParameter> read GetParameters;
property SubCommands: specialize TArray<ICommand> read GetSubCommands;
end;
ICommandParameterReceiver
ICommandParameterReceiver is an optional capability, not a requirement for all commands. Before validation and execution, TCLIApplication uses Supports() to provide its parsed parameter list when the selected command implements this interface. TBaseCommand implements it; a standalone ICommand that does not need framework-managed lookup can execute without it.
ICommandParameterReceiver = interface
procedure SetParsedParams(const Params: TStringList);
end;
ICommandParameter
ICommandParameter = interface
function GetShortFlag: string;
function GetLongFlag: string;
function GetDescription: string;
function GetRequired: Boolean;
function GetParamType: TParameterType;
function GetDefaultValue: string;
function GetAllowedValues: string;
property ShortFlag: string read GetShortFlag;
property LongFlag: string read GetLongFlag;
property Description: string read GetDescription;
property Required: Boolean read GetRequired;
property ParamType: TParameterType read GetParamType;
property DefaultValue: string read GetDefaultValue;
property AllowedValues: string read GetAllowedValues;
end;
IProgressIndicator
IProgressIndicator = interface
procedure Start;
procedure Stop;
procedure Update(const Progress: Integer; const ACaption: string = '');
end;
2. Application Core (CLI.Application)
The TCLIApplication class is the central component that:
- Manages command registration
- Holds an optional executable root command
- Coordinates application setup, root/named-command selection, parsing, and dispatch through focused stages
- Delegates help formatting to
CLI.Internal.Help - Orchestrates completion requests, delegating candidate calculation to
CLI.Internal.Completionand Bash/PowerShell script rendering toCLI.Internal.CompletionScripts
CLI.Internal.ParameterValues owns parameter lookup semantics shared by validation and command execution. These units are internal implementation boundaries; the public TCLIApplication facade and ICLIApplication contract are unchanged. Lazarus compiles the internal units as package members but does not add them to the generated package uses surface.
The facade is intentionally retained: it is the one place that knows process arguments, registered commands, test-output capture, and the public execution entry point. CLI.Internal.CompletionScripts returns ordered rendered lines with the existing output-routing marker; the facade performs the final console write. This keeps generated Bash and PowerShell text independently testable without exposing a new public renderer API. The decision is recorded in ADR-001.
Key methods:
TCLIApplication = class(TInterfacedObject, ICLIApplication)
private
FName: string;
FVersion: string;
FRootCommand: ICommand;
FCommands: TCommandList;
FCurrentCommand: ICommand;
FParsedParams: TStringList;
FParamStartIndex: Integer;
FDebugMode: Boolean;
FArguments: TStringArray;
{$IFDEF CLI_FP_TESTING}
FOutputCapture: TStrings;
{$ENDIF}
public
procedure RegisterCommand(const Command: ICommand);
function Execute: Integer;
property DebugMode: Boolean read FDebugMode write FDebugMode;
property Version: string read FVersion;
property RootCommand: ICommand read FRootCommand;
property Commands: TCommandList read GetCommands;
end;
ExecuteArguments coordinates focused global-request, command-selection, command-version, command-help, and execution helpers. Version detection uses the same ReadOption token boundaries as parsing, preserving equals-form values. Parsed occurrences are kept in order; shared lookup scans backwards across both aliases so validation and command retrieval agree. Output capture state and TestExecuteAndCapture exist only in builds compiled with CLI_FP_TESTING; normal runtime units contain neither symbol.
Root-command support is introduced through an overload rather than by changing ICLIApplication, preserving the existing public interface contract:
function CreateCLIApplication(const Name, Version: string): ICLIApplication;
function CreateCLIApplication(const Name, Version: string;
const RootCommand: ICommand): ICLIApplication;
At execution time, an empty argument list selects FRootCommand when present. A leading option also selects it after terminal global options have been handled. A leading non-option token continues through the existing named command and subcommand resolver. Both paths converge on the same parameter parsing, validation, and exception handling pipeline.
3. Base Classes
TBaseCommand (CLI.Command)
Base implementation for commands with:
TBaseCommand = class(TInterfacedObject, ICommand, ICommandParameterReceiver)
private
FName: string;
FDescription: string;
FParameters: array of ICommandParameter;
FSubCommands: array of ICommand;
FParsedParams: TStringList;
protected
function GetParameterValue(const Flag: string; out Value: string): Boolean;
public
procedure AddParameter(const Parameter: ICommandParameter);
procedure AddSubCommand(const Command: ICommand);
procedure SetParsedParams(const Params: TStringList);
function Execute: Integer; virtual; abstract;
end;
TCommandParameter (CLI.Parameter)
Base implementation for command parameters:
TCommandParameter = class(TInterfacedObject, ICommandParameter)
private
FShortFlag: string;
FLongFlag: string;
FDescription: string;
FRequired: Boolean;
FParamType: TParameterType;
FDefaultValue: string;
FAllowedValues: string;
public
constructor Create(const AShortFlag, ALongFlag, ADescription: string;
ARequired: Boolean; AParamType: TParameterType;
const ADefaultValue: string = ''; const AAllowedValues: string = '');
end;
4. Console Support (CLI.Console)
Console color and cursor control:
type
TConsoleColor = (
ccBlack, ccBlue, ccGreen, ccCyan,
ccRed, ccMagenta, ccYellow, ccWhite,
ccBrightBlack, ccBrightBlue, ccBrightGreen, ccBrightCyan,
ccBrightRed, ccBrightMagenta, ccBrightYellow, ccBrightWhite
);
TConsole = class
private
class var FDefaultAttr: Word;
class procedure InitConsole;
public
class procedure SetForegroundColor(const Color: TConsoleColor);
class procedure SetBackgroundColor(const Color: TConsoleColor);
class procedure ResetColors;
class procedure Write(const Text: string); overload;
class procedure Write(const Text: string; const FgColor: TConsoleColor); overload;
class procedure WriteLn(const Text: string); overload;
class procedure WriteLn(const Text: string; const FgColor: TConsoleColor); overload;
// Cursor control methods
class procedure ClearLine;
class procedure MoveCursorUp(const Lines: Integer = 1);
class procedure MoveCursorDown(const Lines: Integer = 1);
class procedure MoveCursorLeft(const Columns: Integer = 1);
class procedure MoveCursorRight(const Columns: Integer = 1);
class procedure SaveCursorPosition;
class procedure RestoreCursorPosition;
end;
5. Progress Indicators (CLI.Progress)
Two types of progress indicators:
Spinner
type
TSpinnerStyle = (
ssDots, // ⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏
ssLine, // -\|/
ssCircle, // ◐◓◑◒
ssSquare, // ◰◳◲◱
ssArrow, // ←↖↑↗→↘↓↙
ssBounce, // ⠁⠂⠄⠂
ssBar // ▏▎▍▌▋▊▉█▊▋▌▍▎▏
);
TSpinner = class(TProgressIndicator)
private
FStyle: TSpinnerStyle;
FFrame: Integer;
FFrames: array of string;
public
constructor Create(const AStyle: TSpinnerStyle);
procedure Update(const Progress: Integer; const ACaption: string = ''); override;
end;
Progress Bar
TProgressBar = class(TProgressIndicator)
private
FTotal: Integer;
FWidth: Integer;
FLastProgress: Integer;
FLastCaption: string;
public
constructor Create(const ATotal: Integer; const AWidth: Integer = 10);
procedure Update(const Progress: Integer; const ACaption: string = ''); override;
end;
Error Handling
The framework defines a CLI-specific exception hierarchy:
- Exception Classes (
CLI.Errors)
type
ECLIException = class(Exception);
ECommandNotFoundException = class(ECLIException);
EInvalidParameterException = class(ECLIException);
ERequiredParameterMissingException = class(ECLIException);
EInvalidParameterValueException = class(ECLIException);
ECommandExecutionException = class(ECLIException);
These public types are retained as 1.x source-compatibility types for application code that chooses to raise or catch them. They are not the general runtime-error contract: the current TCLIApplication execution path does not raise them for parser or validation failures. It writes those errors and returns exit code 1; command exceptions are caught as Exception and reported as execution errors. Definition errors, including duplicate command registration, use the standard SysUtils argument-exception types documented by CLI.Validation.
- Parameter Validation
- Required parameter checks
- Type validation
- Default value application
- Command Validation
- Command existence checks
- Subcommand validation
- Parameter format validation
Platform-Specific Considerations
Windows Console Support
Foreground/background colours and colour reset use the Windows console API:
{$IFDEF WINDOWS}
Handle := GetStdHandle(STD_OUTPUT_HANDLE);
GetConsoleScreenBufferInfo(Handle, Info);
SetConsoleTextAttribute(Handle, Attributes);
{$ENDIF}
Cursor movement and position helpers still emit ANSI control sequences, so those operations depend on ANSI-compatible terminal behaviour.
Unix-Like Console Support
On non-Windows targets, colours and cursor control use ANSI escape sequences:
{$ELSE}
System.Write(#27'[<color_code>m');
{$ENDIF}
Best Practices
- Command Implementation
type
TMyCommand = class(TBaseCommand)
public
constructor Create;
function Execute: Integer; override;
end;
- Parameter Definition
Cmd.AddParameter(
'-p',
'--param',
'Parameter description',
True,
ptString,
'default'
);
- Progress Indication
var
Progress: IProgressIndicator;
begin
Progress := CreateProgressBar(100, 20); // total=100, width=20
Progress.Start;
try
// Update progress with optional inline status text
Progress.Update(50, 'Halfway done'); // 50%
finally
Progress.Stop;
end;
end;
- Color Usage
- Use red for errors
- Use yellow for warnings
- Use green for success messages
- Use cyan for information
- Use white for normal output
- Error Handling
try
Result := Command.Execute;
except
on E: Exception do
begin
TConsole.WriteLn('Error: ' + E.Message, ccRed);
Result := 1;
end;
end;
Parameter Validation
Implementation Details
The framework implements parameter validation in TCLIApplication.ValidateParameterValue. Each parameter type has specific validation rules:
> Note: AddFlag defaults to the string 'false'. Presence without a > value produces 'true'. Overriding the default with 'true' makes an absent > flag true, which is generally surprising.
Basic Types
ptString: No validationptInteger: UsesTryStrToIntptFloat: UsesTryStrToFloatwith the process locale's decimal separatorptBoolean: Must be 'true' or 'false' (case-insensitive)
Complex Types
ptDateTime: UsesTryStrToDateTimewith local format settings: ``pascal LocalFormatSettings := DefaultFormatSettings; LocalFormatSettings.DateSeparator := '-'; LocalFormatSettings.ShortDateFormat := 'yyyy-mm-dd'; LocalFormatSettings.LongTimeFormat := 'HH:nn'; // 24-hour format TryStrToDateTime(Value, DateTimeValue, LocalFormatSettings);`This does not change process-wideFormatSettings.YYYY-MM-DD HH:MM` is the documented portable representation. The parser remains compatible with existing v1.x values containing seconds, but help and validation errors consistently advertise the canonical format.
ptEnum: Validates against pipe-separated allowed values: ``pascal AllowedValues.Delimiter := '|'; AllowedValues.StrictDelimiter := True; AllowedValues.DelimitedText := Param.AllowedValues;``
ptUrl: Validates URL protocol: ``pascal StartsStr('http://', Value) or StartsStr('https://', Value) or StartsStr('git://', Value) or StartsStr('ssh://', Value)``
Error Messages
The framework provides clear error messages for validation failures:
Format('Error: Parameter "%s" must be an integer', [Param.LongFlag])
Format('Error: Parameter "%s" must be a float', [Param.LongFlag])
Format('Error: Parameter "%s" must be "true" or "false"', [Param.LongFlag])
Format('Error: Parameter "%s" must be in format YYYY-MM-DD HH:MM', [Param.LongFlag])
Format('Error: Parameter "%s" must be one of: %s', [Param.LongFlag, Param.AllowedValues])
Format('Error: Parameter "%s" must be a valid URL starting with http://, https://, git://, or ssh://', [Param.LongFlag])
Validation Flow
Version requests are handled before this flow using parser-aware token boundaries. They return success without validating required command options.
- Command parameters are parsed from command line
- Each parameter is validated based on its type
- If any validation fails:
- Error message is displayed
- Command is not executed
- Returns error code 1
- If all validations pass:
- Command's Execute method is called
- Returns command's result code
Shell Completion Script Generators
The CLI framework provides completion script generators for both Bash and PowerShell, with context-aware tab completion for your CLI.
CLI.Internal.CompletionScripts owns deterministic Bash and PowerShell text rendering and shell quoting. TCLIApplication retains the public flags and the process/test-output boundary. FPCUnit compares full ordered renderings; required CI additionally generates a fixture script and checks Bash syntax on Linux and PowerShell parsing on Windows.
Bash Completion Script Generator
Accessible via the --completion-file global flag, this generator outputs a Bash script that provides:
- At the root level, an option prefix includes root-command parameters and all application options:
--help,-h,--help-complete,--version,-v,--completion-file, and--completion-file-pwsh. - At named command levels, an empty token offers subcommands, command parameters, help, and reserved
--version/-vrequests. Version requests display the application version without executing the selected command. - The shell function calls the executable's hidden
__completeentrypoint for live candidates. A static associative tree is still emitted for compatibility but is not read by the generated function. - Automatic value completion: Boolean parameters automatically complete with
true/false, and enum parameters complete with their allowed values.
PowerShell Completion Script Generator
Accessible via the --completion-file-pwsh global flag, this generator outputs a PowerShell script that provides:
- Command, subcommand, and parameter candidates scoped to the resolved command path, plus the built-in application flags described above.
- File fallback is suppressed.
- Automatic value completion: Boolean parameters automatically complete with
true/false, and enum parameters complete with their allowed values. - Uses
Register-ArgumentCompleter; PowerShell 7+ additionally receives native executable registration.
Completion System Flow Diagram
The completion system uses a hidden __complete entrypoint that shell scripts invoke to get completion suggestions dynamically:
┌─────────────────────────────────────────────────────────────────────────┐
│ USER INTERACTION │
└─────────────────────────────────────────────────────────────────────────┘
│
│ User presses TAB
▼
┌─────────────────────────────────────────────────────────────────────────┐
│ BASH / POWERSHELL SHELL │
│ │
│ • Detects TAB keypress │
│ • Reads current command line │
│ • Parses words and cursor position │
└─────────────────────────────────────────────────────────────────────────┘
│
│ Calls completion function
▼
┌─────────────────────────────────────────────────────────────────────────┐
│ GENERATED COMPLETION SCRIPT │
│ (myapp_completion.bash / myapp_completion.ps1) │
│ │
│ Bash: │
│ 1. Extract COMP_WORDS[] and COMP_CWORD │
│ 2. Build args array from words[1..cword-1] │
│ 3. If cursor after space, append empty token "" │
│ 4. Call: ./myapp __complete "${args[@]}" │
│ │
│ PowerShell: │
│ 1. Split command line into $words array │
│ 2. Skip first word (app name) │
│ 3. If line ends with space, append empty token │
│ 4. Call: & ./myapp __complete @argsList │
└─────────────────────────────────────────────────────────────────────────┘
│
│ Executes: myapp __complete [tokens...]
▼
┌─────────────────────────────────────────────────────────────────────────┐
│ CLI APPLICATION + COMPLETION ENGINE │
│ (src/cli.application.pas and src/cli.internal.completion.pas) │
│ │
│ TCLIApplication.Execute(): │
│ ┌──────────────────────────────────────────────────┐ │
│ │ if ParamStr(1) = '__complete' then │ │
│ │ HandleCompletion(); │ │
│ │ Exit(0); │ │
│ └──────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ HandleCompletion(): │
│ • Collect tokens from ParamStr(2..ParamCount) │
│ • Delegate through DoComplete(Tokens) to CompleteCLI() │
│ • Write suggestions to stdout (one per line) │
│ • Write directive as :<number> on last line │
└─────────────────────────────────────────────────────────────────────────┘
│
│ Wrapper delegates to CompleteCLI()
▼
┌─────────────────────────────────────────────────────────────────────────┐
│ CompleteCLI(Tokens): COMPLETION LOGIC ENGINE │
│ │
│ 1. ROOT-LEVEL FLAG CHECK │
│ ┌─────────────────────────────────────────┐ │
│ │ if Tokens[0] starts with '-' │ │
│ │ → Use root command when configured │ │
│ │ → Otherwise complete global options │ │
│ └─────────────────────────────────────────┘ │
│ │ │
│ 2. COMMAND RESOLUTION │
│ ┌─────────────────────────────────────────┐ │
│ │ Find command matching Tokens[0] │ │
│ │ If not found: │ │
│ │ → Complete top-level commands │ │
│ │ → Return: matching command names │ │
│ └─────────────────────────────────────────┘ │
│ │ │
│ 3. SUBCOMMAND WALKING │
│ ┌─────────────────────────────────────────┐ │
│ │ Walk through subcommands │ │
│ │ idx = 1 │ │
│ │ while idx < token_count: │ │
│ │ if Tokens[idx] is subcommand: │ │
│ │ Cmd = SubCmd │ │
│ │ idx++ │ │
│ └─────────────────────────────────────────┘ │
│ │ │
│ 4. CONTEXT DETERMINATION │
│ │ │
│ ┌───────────────────┴─┬──────────────────┐ │
│ ▼ ▼ ▼ │
│ FLAG NAME FLAG VALUE COMMAND POSITION │
│ │
│ │
│ Last token Previous token Not completing flag │
│ starts with '-' is a flag or flag value │
│ ├─ Prefix match ├─ Boolean? ├─ No positional entered? │
│ │ → --flag-name │ → true/false │ → Subcommands │
│ ├─ Exact match? ├─ Enum? │ → Command flags │
│ │ → Complete │ → allowed vals │ → Help flags │
│ value (bool/ └─ Other types? └─ Positional already entered?│
│ enum) → No completion → No candidates │
│ │
│ 5. RETURN SUGGESTIONS + DIRECTIVE │
│ ┌─────────────────────────────────────────┐ │
│ │ suggestions = TStringList │ │
│ │ directive = CD_NOFILE | CD_NOSPACE etc. │ │
│ │ Return: suggestions + :<directive> │ │
│ └─────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────┘
│
│ Returns list of suggestions
▼
┌─────────────────────────────────────────────────────────────────────────┐
│ STDOUT OUTPUT │
│ │
│ suggestion1 │
│ suggestion2 │
│ suggestion3 │
│ :4 ← directive (CD_NOFILE = 4) │
└─────────────────────────────────────────────────────────────────────────┘
│
│ Output captured by shell script
▼
┌─────────────────────────────────────────────────────────────────────────┐
│ COMPLETION SCRIPT PROCESSING │
│ │
│ • Parse last line for directive (:number) │
│ • Extract suggestions (all lines before directive) │
│ • Apply directive: │
│ - Bash currently parses but does not apply directive bits │
│ - PowerShell uses bit 2 to choose the completion result type │
│ • Set shell completion candidates (COMPREPLY / results array) │
└─────────────────────────────────────────────────────────────────────────┘
│
│ Completion options ready
▼
┌─────────────────────────────────────────────────────────────────────────┐
│ USER SEES COMPLETIONS │
│ │
│ $ myapp repo clone --[TAB] │
│ --url --path --branch --depth --help │
└─────────────────────────────────────────────────────────────────────────┘
Key Points:
- Hidden Entrypoint: The application checks
ParamStr(1) = '__complete'before normal command processing - Token-Based: Shell passes parsed command-line tokens to
__complete - Directive System: Return value includes completion directive flags (CD_NOFILE, CD_NOSPACE, etc.)
- Context-Aware: Completion logic walks command tree to determine current context
- Type-Aware: Boolean and Enum parameters automatically complete with their valid values
- Candidates: The engine supplies no filesystem candidates. Bash parses directives but does not currently apply their bits; PowerShell uses bit 2 to choose
ParameterNamerather thanParameterValueresult types.
Example Token Flow:
# User types: myapp repo clone --url [TAB]
# Shell calls: myapp __complete repo clone --url ""
Tokens = ["repo", "clone", "--url", ""]
↓
CompleteCLI():
1. Tokens[0] = "repo" → Find "repo" command
2. Tokens[1] = "clone" → Find "clone" subcommand
3. Empty current token follows "--url" → Complete its value
- URL/string options: no value suggestions, directive 0
- Boolean options: ["true", "false"], directive 4
- Enum options: allowed values, directive 4
↓
Return for --url: [":0"]
---
Completion Feature Matrix
The completion system provides built-in static completion; the exposed custom callback methods are deprecated and non-functional:
| Feature | Status | Implementation Details |
|---|---|---|
| Commands | ✅ Fully functional | Resolved dynamically from the registered command tree |
| Subcommands | ✅ Fully functional | Multi-level hierarchy support |
| Flags (short/long) | ✅ Fully functional | Context-aware at each command level |
| Boolean values | ✅ Fully functional | Auto-completes with true/false |
| Enum values | ✅ Fully functional | Auto-completes with allowed values |
| Custom callbacks | ⚠️ Deprecated | Registration methods are non-functional stubs and are planned for removal in v2.0.0 |
Implementation Approach:
- Built-in completion (✅ Working):
CLI.Internal.Completion.CompleteCLI()traverses the registered command tree and parameter definitions at runtime. The application retains a smallDoComplete()compatibility wrapper. Generated Bash and PowerShell functions call the hidden__completeentrypoint; no callback registry is needed for command, flag, Boolean, or enum candidates.
- Custom callbacks (⚠️ Deprecated): The concrete application class exposes
RegisterFlagValueCompletion()andRegisterPositionalCompletion()only for 1.x source compatibility. The methods are marked deprecated, remain no-op stubs, and are planned for removal in v2.0.0. The historical investigation below explains why the earlier storage approach was not enabled.
Built-in completion covers registered commands, flags, Boolean values, and enum values. It cannot currently obtain dynamic candidates from a filesystem, database, API, or other application callback.
---
Historical Investigation: Disabled Custom Callbacks
> Current authoritative status: The deprecated public methods remain on > TCLIApplication for 1.x source compatibility, but their bodies are stubs > and always perform no registration. They are planned for removal in v2.0.0. > The discussion below records the earlier implementation investigation; it > is not a general claim that all procedural values in FPC dynamic arrays are > unsupported.
The Problem
Custom callbacks would allow developers to register dynamic completion functions at runtime:
// What we WANT to support (but can't):
App.RegisterFlagValueCompletion('deploy', '--env',
function (Args: TStringArray; ToComplete: string): TStringArray
begin
Result := ['dev', 'staging', 'prod']; // Custom completion
end);
// A later internal DoComplete call would use the registered callback.
What We Tried
The implementation requires storing function pointers in dynamic structures:
type
TFlagValueCompletionFunc = function (Args: TStringArray; ToComplete: string): TStringArray;
TFlagCompletionEntry = record
Key: string; // e.g., "deploy/--env"
Callback: TFlagValueCompletionFunc; // Function pointer
end;
TFlagCompletionList = array of TFlagCompletionEntry; // Dynamic array
var
FFlagCompletions: TFlagCompletionList; // Store all registered callbacks
procedure RegisterFlagValueCompletion(const CommandPath, FlagName: string;
Func: TFlagValueCompletionFunc);
begin
SetLength(FFlagCompletions, Length(FFlagCompletions) + 1);
FFlagCompletions[High(FFlagCompletions)].Key := CommandPath + '/' + FlagName;
FFlagCompletions[High(FFlagCompletions)].Callback := Func; // ⚠️ PROBLEM
end;
// Later, during completion:
function GetRegisteredCallback(const Key: string): TFlagValueCompletionFunc;
var
i: Integer;
begin
for i := 0 to High(FFlagCompletions) do
if FFlagCompletions[i].Key = Key then
Exit(FFlagCompletions[i].Callback); // ⚠️ Returns nil or garbage!
Result := nil;
end;
Recorded Failure
The earlier experiment recorded nil or invalid callback retrieval under the project's FPC 3.2.2 build. No focused reproducer or compiler issue is linked, so this document does not attribute that result to a confirmed FPC limitation. The current source retains only the deprecated public registration no-ops; unreachable private lookup and callback branches were removed in v1.3.3.
What Works Instead
Built-in completion avoids dynamic function pointer storage entirely:
// Compatibility wrapper in CLI.Application
function TCLIApplication.DoComplete(const Tokens: TStringArray): TStringList;
begin
Result := CompleteCLI(Tokens, FRootCommand, CommandSnapshot);
end;
DoComplete is only a facade wrapper over CLI.Internal.Completion. The internal engine performs metadata-based Boolean and enum completion and contains no callback lookup path.
Why this works:
- No function pointers stored dynamically
- Completion logic is statically coded in the internal completion engine
- Parameter metadata (allowed values, types) stored as simple strings/enums
- No retrieval of function pointers from dynamic arrays
Possible Implementation Directions
Any future implementation should first add a focused lifetime/retrieval test for the project's supported compiler. Object-backed callbacks or named procedures with explicitly managed ownership are possible designs; neither is part of the current API behavior.
Current Workaround
For most use cases, built-in completion is sufficient:
- Commands/subcommands - Registered via
RegisterCommand() - Flags - Defined via
AddParameter()methods - Boolean values - Automatically complete with
true/false - Enum values - Automatically complete with allowed values from
AddEnumParameter()
Only advanced scenarios requiring runtime-dynamic completions from external sources (files, databases, APIs) are blocked.
Code Location
The deprecated public stubs remain in src/cli.application.pas:
RegisterFlagValueCompletion()RegisterPositionalCompletion()
The built-in engine is in src/cli.internal.completion.pas. There are no private callback lookup helpers or dormant callback branches.
Before enabling the callback API, re-evaluate the design against the project's supported compiler and retain regression coverage for callback lifetime and retrieval.
Bottom line: Custom callbacks are deprecated and non-functional in the current implementation. Built-in command, flag, Boolean, and enum completion does not depend on them.
Generator maintenance
This section is for maintainers of cli-fp-gen; application authors should use the generator guide.
The generator separates command-line dispatch (CliFpGen.App), project operations (CliFpGen.Generate), the in-memory model (CliFpGen.Model), JSON I/O (CliFpGen.SpecIO), validation (CliFpGen.Validate), naming (CliFpGen.Naming), rendering (CliFpGen.Renderer), managed filesystem work (CliFpGen.Filesystem), and generated-file tracking (CliFpGen.Manifest).
When adding a parameter kind, update the model mappings, semantic validation, renderer, JSON load/save paths, golden fixture and expected output, focused tests, and the application-facing supported-kind list. Run all scripts in tests/codegen/ plus tests/codegen/run_all_tests.ps1; the compile smoke test confirms that generated calls match the framework source.
TProjectSpec owns its root-command specification and named commands, and each command owns its parameters. Construct parsed objects completely before transferring ownership; free a partially constructed object in the same routine if parsing raises. Manifest cleanup must remain conservative: no stale file may be deleted through a Unix symbolic link or Windows reparse point.