1:- module(execution_cli, [
    2  execute_cli/0,
    3  execute_cli/1,            % +Arguments:list(atom)
    4  register_cli_command/3    % +Command:atom, :Goal, +ArgumentsSpec:list
    5]). 
    6
    7:- use_module(library(dcg/basics)).    8:- use_module(execution_context).    9
   10:- meta_predicate 
   11  register_cli_command(+, 2, +).   12
   13:- dynamic
   14    command_spec/3.   15
   16:- multifile
   17  prolog:message//1.   18
   19%%%%%%%%%% PUBLIC PREDICATES %%%%%%%%%%%%%%%%
 execute_cli is det
Same as current_prolog_flag(argv, Arguments), execute_cli(Arguments)
   23execute_cli :-
   24  current_prolog_flag(argv, Arguments),
   25  execute_cli(Arguments).
 execute_cli(+Arguments:list(atom)) is det
Interprets Arguments which are assumed to be unified by current_prolog_flag(argv, Arguments) call and calls goal registered by cli_command/2 directive. The name of the command is determined by the first positional argument. Options are removed from the Arguments list by checking against registered context variables - see context_variable/3 predicate.

Throws cli_option(Type, OptionName) exception if required arguments are not provided (see register_cli_command/3), or when the option that cannot be associated with any context variable is found on command line arguments. The exception can be used for print_message call.

   36execute_cli(Arguments) :-  
   37  (   phrase(positional_args([ Command | Positional]), Arguments)
   38  ->  true
   39  ;   Command = help
   40  ),
   41  (   Command == help 
   42  ->  portray_command_help(help, [])
   43  ;   ( command_spec(Command, Goal, ArgumentSpec)
   44      ->  true
   45      ;   print_message(error, format('Unknown command `~w`', [Command])),
   46          halt(42)
   47      ),
   48      (   Positional = [ help | _ ]
   49      ->  portray_command_help(Command, ArgumentSpec)
   50      ;   verify_arguments(ArgumentSpec, Positional, Options),
   51          retractall(execution_context:variable_cache),
   52          call(Goal, Positional, Options)
   53      )
   54  ), !.
 register_cli_command(+Command:atom, :Goal, +ArgumentsSpec:list) is det
Registers new command for CLI. The Goal will be invoked when the first positional argument unifies with Command. The Goal is invoked as call(Goal, PositionalArguments, Options) where PositionalArguments are all arguments that are not an command line option, and Options maps some of the context variables to the goal options. ArgumentSpec can contain some of the following terms:
   75register_cli_command(Command, Goal, ArgumentsSpec) :-
   76  retractall(command_spec(Command, _,  _)),
   77  assert(command_spec(Command, Goal, ArgumentsSpec) ).
   78
   79
   80%%%%%%%%%% PRIVATE PREDICATES %%%%%%%%%%%%%%%% 
   81is_variable(Option, Type) :-
   82  execution_context:context_variable_def(_, _, Spec),
   83  memberchk(Option, Spec),
   84  ( Type == bool
   85  ->  memberchk(is_flag(true), Spec)
   86  ;   true
   87  ).
   88is_variable(long(Negated), bool) :-
   89  atom_concat('no-', OptionName, Negated),
   90  execution_context:context_variable_def(_, _, Spec),
   91  memberchk(long(OptionName), Spec),
   92  memberchk(is_flag(true), Spec).
   93is_variable(long(OptionName), Type) :-
   94  Type \= true,
   95  execution_context:context_variable_def(Var, _, Spec),
   96  execution_context:default_name(Var, '-', DefaultName ),
   97  (   Type == bool
   98  ->  memberchk(is_flag(true), Spec),
   99      ( OptionName == DefaultName
  100      ; atom_concat('no-', DefaultName, OptionName)
  101      )
  102  ;   OptionName == DefaultName
  103  ).
  104
  105portray_arguments([]) --> [], !.
  106  portray_arguments([Name-Description|Args]) -->
  107    {   split_long_description(Description, 60, [Line|Lines]),
  108        format(atom(OutLine), '  ~w~20|~w', [Name, Line]),
  109        maplist([Descr, Out] >>format(atom(Out), '~20|~w', [Descr]), Lines, OutLines)
  110    },
  111    [OutLine| OutLines ],
  112    !,
  113    portray_arguments(Args).
  114 
  115portray_command_help(Command, Spec) :-
  116  phrase(portray_command_help(Command, Spec), Lines),
  117  atomic_list_concat(Lines, '\n', Message),
  118  print_message(help, format(Message, [])),
  119  !.
  120
  121portray_command_help(help, _) -->
  122  {   program_name(File),  
  123      format(atom(Line0), 'Usage: ~w <command> [common options] [command options]', [File]), 
  124      findall(Command-Spec, command_spec(Command, _, Spec), Commands0),
  125      sort(Commands0, Commands)
  126  },
  127  [Line0, 'Where <command> is one of:'],
  128  portray_command_infos(Commands),
  129  portray_command_options([], [ '', 'Options common for all commands:']),
  130  !.
  131 portray_command_help(Command, Spec) --> 
  132  {   program_name(File)   
  133  },    
  134  portray_command_usage(File, Command, Spec),
  135  portray_command_info(Command, Spec),
  136  portray_positional_arguments(Spec),
  137  portray_command_options(Command,['', 'Options:' ]),
  138  portray_command_options([],[ '', 'Options common for all commands:']),
  139  ['', 'Use "<command> help" for additional details' ],
  140  !.
  141
  142portray_command_info(Command, Spec) -->
  143    {   memberchk(describe(Description), Spec),
  144        split_long_description(Description, 60, [Line|Lines]),
  145        format(atom(OutLine), '  ~w~20|~w', [Command, Line]),
  146        maplist([Descr, Out] >>format(atom(Out), '~20|~w', [Descr]), Lines, OutLines)
  147    },
  148    [OutLine| OutLines],
  149    !.
  150 portray_command_info(_, _) --> [].
  151
  152portray_command_infos([]) --> [].
  153 portray_command_infos([Command-Spec| Commands]) -->
  154    portray_command_info(Command, Spec),
  155    portray_command_infos(Commands).
  156  
  157
  158portray_command_options(Command, Header) -->
  159  {   findall(
  160          Long-Short-Spec-Type, 
  161          (   execution_context:context_variable_def(_, Type, Spec),
  162              (
  163                  memberchk(cli_command(Command), Spec)
  164              ->  true
  165              ;   memberchk(cli_command(Commands), Spec),
  166                  memberchk(Command, Commands)
  167              ),
  168              (   memberchk(long(Long0), Spec)
  169              ->  atom_concat('--', Long0, Long)                
  170              ;   Long = ''
  171              ), 
  172              (   memberchk(short(Short0), Spec)
  173              ->  atom_concat('-', Short0, Short)              
  174              ;   Short = '' 
  175              ),
  176              \+ (Short == '', Long == '')
  177          ),
  178          Specs),
  179      sort(Specs, Sorted),
  180      \+ length(Sorted,0)
  181  },
  182  Header,
  183  portray_options(Sorted).
  184portray_command_options(_, _) --> [].
  185
  186portray_command_usage(File, Command, Spec) -->
  187  {
  188    findall(Index-Name, member(positional(Index, Name, _), Spec), Positionals),    
  189    findall(Index-Name, member(optional(positional(Index, Name, _)), Spec), Optionals0),
  190    (   length(Optionals0, 0)
  191    ->  Arguments0 = Positionals
  192    ;   maplist([Index-Name, Index-OptName] >> atomic_list_concat(['[', Name, ']'], OptName), Optionals ),
  193        append(Positionals, Optionals, Arguments0)
  194    ),
  195    sort(Arguments0, Arguments1),
  196    pairs_values(Arguments1, Arguments2),
  197    atomic_list_concat(Arguments2, ' ', Arguments),        
  198    format(atom(Usage), 'Usage: ~w ~w <Common options...> <Options...> ~w', [File, Command, Arguments])
  199  },
  200  [Usage].
  201
  202portray_option(Long, Short, Type, Spec) -->
  203  {   memberchk(describe(Description0), Spec),   
  204      (   memberchk(default(Default), Spec)
  205      ->  format(atom(DefaultText), 'Defaults to `~w`. ', [ Default])
  206      ;   DefaultText = ''
  207      ),   
  208      (   portray_type_text(Type, Text)
  209      ->  atomic_list_concat(['(', Text, ') '], TypeText)
  210      ;   TypeText = ''
  211      ),      
  212      (   memberchk(env(Env), Spec)
  213      ->  atomic_list_concat(['. Can be set by environment variable ', Env, '. '], EnvText)      
  214      ;   EnvText = ''
  215      ),      
  216      format(atom(Description), '~w~w~w~w', [TypeText, DefaultText, Description0, EnvText]),
  217      split_long_description(Description, 56, [Line|Lines]),      
  218      format(atom(OutLine), '  ~w ~20|~w ~24|~w', [Long, Short, Line ] ),
  219      maplist([Descr, Out] >>format(atom(Out), '~24|~w', [Descr]), Lines, OutLines)
  220  },
  221  [OutLine| OutLines],
  222  !.
  223 portray_option(_, _, _, _) --> [].
  224
  225portray_options([]) --> [], !.
  226 portray_options([Long-Short-Spec-Type|Specs]) -->
  227    portray_option(Long, Short, Type, Spec), 
  228    portray_options(Specs).
  229
  230portray_positional_arguments(Spec) -->
  231  {
  232    findall(Name-Description, member(positional(_, Name, Description), Spec), Positionals0),
  233    sort(Positionals0, Positionals),
  234    (   length(Positionals, 0)
  235    ->  PositionalsHeader = []
  236    ;    PositionalsHeader = ['', 'Mandatory arguments:']
  237    ),
  238    findall(Name-Description, member(optional(positional(_, Name, Description)), Spec), Optionals0),
  239    sort(Optionals0, Optionals),
  240    (   length(Optionals, 0)
  241    ->  OptionalsHeader = []
  242    ;    OptionalsHeader = ['', 'Optional arguments:']
  243    ) 
  244  },
  245  PositionalsHeader,
  246  portray_arguments(Positionals),
  247  OptionalsHeader,
  248  portray_arguments(Optionals).
  249
  250portray_type_text(bool, flag).
  251 portray_type_text(number, number).
  252 portray_type_text(atom, value).
  253 portray_type_text(list, 'comma separated list of values').
  254 portray_type_text(list(number), 'comma separated list of numbers').
  255 portray_type_text(list(_), 'comma separated list of values').
  256 portray_type_text(Type, Type).
  257
  258
  259
  260positional_args([]) --> [].
  261 positional_args(Positional) -->
  262   [Option],
  263   { atom_concat('--', Long, Option),
  264     atomic_list_concat([_, _], '=', Long)
  265   },
  266   !,
  267   positional_args(Positional).
  268 positional_args(Positional) -->
  269   [Option],
  270   { atom_concat('--', Long, Option),
  271     is_variable(long(Long), bool)
  272   },
  273   !,
  274   positional_args(Positional).
  275 positional_args(Positional) -->
  276   [Option],
  277   { atom_concat('--', Long, Option),
  278     is_variable(long(Long), _)
  279   },
  280   [_],
  281   !,
  282   positional_args(Positional).
  283 positional_args(_)  -->
  284   [Option],
  285   {  atom_concat('--', _, Option),
  286      throw(cli_option(unknown, Option))
  287   }.
  288 positional_args(Positional) -->
  289   [Option],
  290   {  atom_concat('-', Short, Option),
  291      atom_length(Short, 1),
  292      is_variable(short(Short), bool)
  293   },
  294   !,
  295   positional_args(Positional).
  296 positional_args(Positional) -->
  297   [Option],
  298   {  atom_concat('-', Short, Option),
  299      atom_length(Short, 1),
  300      is_variable(short(Short), _)
  301   },
  302   [_],
  303   !,
  304   positional_args(Positional).
  305 positional_args(Positional) -->
  306    positional_mixed_flags,
  307    !,
  308    positional_args(Positional).
  309 positional_args(_)  -->
  310   [Option],
  311   {  atom_concat('-', _, Option),
  312      throw(cli_option(unknown, Option))
  313   }.
  314 positional_args([Arg | Positional])  -->
  315   [Arg],
  316   positional_args( Positional).
  317
  318positional_mixed_flags, Flags -->
  319    [Option],
  320    {  atom_concat('-', Short, Option),
  321       atom_length(Short, L),
  322       L > 1,
  323       atom_codes(Short, Codes),
  324       maplist([C, F] >> atom_codes(F, [0'-, C]), Codes, Flags)
  325    },
  326    !.
  327program_name(Name) :-
  328  current_prolog_flag(os_argv, [Exe | _]),
  329  atomic_list_concat(Segments, '\\', Exe),
  330  atomic_list_concat(Segments, '/', Path),
  331  directory_file_path(_, File, Path),
  332  file_name_extension(Name, _, File).
  333
  334prolog:message(cli_option(unknown, Option)) -->
  335    [ 'Unknown command line option ~w' - [Option] ].
  336 prolog:message(cli_option(unknown, Option)) -->
  337    [ 'Missing required command line option \'~w\' ' - [Option] ].
  338 prolog:message(cli_option(positional, Option)) -->
  339    [ 'Required argument \'~w\' is missing ' - [Option] ].
  340
  341split_long_description(Long, Length, Lines) :-
  342  atom_codes(Long, Codes),
  343  phrase(split_to_lines(Length, Lines), Codes).
  344
  345split_take_word(L, L1, []) -->
  346    [C],
  347    {   is_white(C),
  348        L1 is L - 1
  349    },
  350    !.
  351 split_take_word( L, _,  _) -->
  352    { L =< 0, !,  fail}.
  353 split_take_word( L, L2, [C|Codes]) -->     
  354    [C],
  355    { L1 is  L -1 },
  356    !,
  357    split_take_word(L1, L2, Codes).
  358 split_take_word(L, L, []) --> [], !.
  359
  360split_take_line(_, []) --> eos, !.
  361 split_take_line(Length, [Word|Words]) -->
  362    split_take_word(Length, Remaining, WordCodes),
  363    { atom_codes(Word, WordCodes) },
  364    !,
  365    split_take_line(Remaining, Words),
  366    !.
  367 split_take_line(_, []) --> [], !.
  368
  369split_to_lines(_, []) --> eos, !.
  370 split_to_lines(Length, [Line|Lines]) --> 
  371    split_take_line(Length, LineWords), 
  372    { atomic_list_concat(LineWords, ' ', Line)},
  373    !,
  374    split_to_lines(Length, Lines).
  375
  376verify_arguments([], _, []).
  377 verify_arguments([optional(Element) | Spec], Argv, Options ) :-
  378    !,
  379    catch(  
  380        (   verify_arguments([Element | Spec], Argv, Options ) 
  381        ;   verify_arguments( Spec, Argv, Options )
  382        ),
  383        cli_option(_, _),
  384        verify_arguments( Spec, Argv, Options )
  385    ).
  386 verify_arguments([describe(_) | Spec], Argv, Options ) :-
  387    !,
  388    verify_arguments( Spec, Argv, Options ).
  389 verify_arguments([context(ContextVariable, OptionName)| Spec], Argv, [Option|Options] ) :-
  390    context_variable_value(ContextVariable, Value),
  391    Option =.. [OptionName, Value],
  392    !,
  393    verify_arguments( Spec, Argv, Options ).
  394 verify_arguments([context(ContextVariable, _) | _], _, _ ) :-
  395    execution_context:context_variable_def(ContextVariable, _, Spec),
  396    (   memberchk(long(OptionName), Spec)
  397    ->  true
  398    ;   execution_context:default_name(ContextVariable, '-', OptionName )
  399    ),
  400    throw(cli_option(required, OptionName)).
  401 verify_arguments([positional(Index, OptionName, _)| Spec], Positional, [Option|Options] ) :-
  402      nth1(Index, Positional, Value),
  403      Option =.. [OptionName, Value],
  404      !,
  405      verify_arguments( Spec, Positional, Options ).
  406 verify_arguments([positional(_, OptionName, _) | _], _, _ ) :-    
  407    throw(cli_option(positional, OptionName))