Browse documentation

Overview

Overview

Start here

Your first CLIRunnable examplesGuide index

How do I...?

Common recipesShell completionProject generator

Learn

Commands and subcommandsOptions and validationOutput, errors, and progress

Reference

Public APICurrent limitations

Inside cli-fp

Technical design

Project

Contributing and support

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 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 ..> 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:

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.

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-help, and execution helpers. 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:

  1. Exception Classes (CLI.Errors)
type
  ECLIException = class(Exception);
  ECommandNotFoundException = class(ECLIException);
  EInvalidParameterException = class(ECLIException);
  ERequiredParameterMissingException = class(ECLIException);
  EInvalidParameterValueException = class(ECLIException);
  ECommandExecutionException = class(ECLIException);

These exception types are available to application code, but 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. Duplicate command registration currently raises a generic Exception.

  1. Parameter Validation
  1. Command 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

  1. Command Implementation
type
  TMyCommand = class(TBaseCommand)
  public
    constructor Create;
    function Execute: Integer; override;
  end;
  1. Parameter Definition
Cmd.AddParameter(
  '-p',
  '--param',
  'Parameter description',
  True,
  ptString,
  'default'
);
  1. 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;
  1. Color Usage
  1. Error Handling
try
  Result := Command.Execute;
except
  on E: ECommandExecutionException 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

Complex Types

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

  1. Command parameters are parsed from command line
  2. Each parameter is validated based on its type
  3. If any validation fails:
    • Error message is displayed
    • Command is not executed
    • Returns error code 1
  4. If all validations pass:
    • Command's Execute method is called
    • Returns command's result code

Shell Completion Script Generators

The CLI framework includes advanced completion script generators for both Bash and PowerShell, providing context-aware tab completion for your CLI.

Bash Completion Script Generator

Accessible via the --completion-file global flag, this generator outputs a Bash script that provides:

PowerShell Completion Script Generator

Accessible via the --completion-file-pwsh global flag, this generator outputs a PowerShell script that provides:

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:                                                     │
│    - CD_NOFILE (4): Don't fallback to file completion                   │
│    - CD_NOSPACE (2): Don't add space after completion                   │
│  • Set shell completion candidates (COMPREPLY / results array)          │
└─────────────────────────────────────────────────────────────────────────┘
                                    │
                                    │ Completion options ready
                                    ▼
┌─────────────────────────────────────────────────────────────────────────┐
│                         USER SEES COMPLETIONS                           │
│                                                                         │
│  $ myapp repo clone --[TAB]                                             │
│  --url  --path  --branch  --depth  --help                               │
└─────────────────────────────────────────────────────────────────────────┘

Key Points:

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. Tokens[2] = "--url"Last token is a flag
     - Check if "--url" is complete flag
     - Check parameter type
     - If String: no suggestions
     - If Boolean: return ["true", "false"]
     - If Enum: return allowed valuesReturn: suggestions + ":4" (CD_NOFILE)

---

Completion Feature Matrix

The completion system provides built-in static completion; the exposed custom callback methods are deprecated and non-functional:

FeatureStatusImplementation Details
Commands✅ Fully functionalResolved dynamically from the registered command tree
Subcommands✅ Fully functionalMulti-level hierarchy support
Flags (short/long)✅ Fully functionalContext-aware at each command level
Boolean values✅ Fully functionalAuto-completes with true/false
Enum values✅ Fully functionalAuto-completes with allowed values
Custom callbacks⚠️ DeprecatedRegistration methods are non-functional stubs and are planned for removal in v2.0.0

Implementation Approach:

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:

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:

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:

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.