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( , , ). 12 13:- dynamic 14 command_spec/3. 15 16:- multifile 17 prolog:message//1. 18 19%%%%%%%%%% PUBLIC PREDICATES %%%%%%%%%%%%%%%%
current_prolog_flag(argv, Arguments), execute_cli(Arguments)
23execute_cli :-
24 current_prolog_flag(argv, Arguments),
25 execute_cli(Arguments).
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 ), !.
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:
describe(Message)
- provides human readable information about the command that is printed when
the CLI command help
or `command help` is invoked.context(ContextVariable, OptionName)
- if call to context_variable_value(ContextVariable, Value)
succeeds, then Goal second argument will contain option named OptionName with the value Value.
If the context_variable is not resolved then the exception is raised and user informed that the required
option is missingoptional(context(ContextVariable, OptionName))
- similar to above, but the c ontext variable is not
required to be resolvedpositional(Nth1, OptionName, Describe)
- associates Nth1 element of positional arguments with option OptionName
when calling Goal. If there are not enough positional argumentsthen the exception is raised and user is
informed that argument is missingoptional(positional(Nth1, OptionName, Describe))
- similar to above, but the positional argument is not
required to be resolved75register_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 , 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 , 246 portray_arguments(Positionals), 247 , 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 334prologmessage(cli_option(unknown, Option)) --> 335 [ 'Unknown command line option ~w' - [Option] ]. 336 prologmessage(cli_option(unknown, Option)) --> 337 [ 'Missing required command line option \'~w\' ' - [Option] ]. 338 prologmessage(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))