/
settings.jl
1498 lines (1325 loc) · 57.3 KB
/
settings.jl
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
## All types, functions and constants related to the specification of the arguments
# actions
const all_actions = [:store_arg, :store_true, :store_false, :store_const,
:append_arg, :append_const, :count_invocations,
:command, :show_help, :show_version]
const internal_actions = [:store_arg, :store_true, :store_false, :store_const,
:append_arg, :append_const, :count_invocations,
:command_arg, :command_flag,
:show_help, :show_version]
const nonflag_actions = [:store_arg, :append_arg, :command_arg]
is_flag_action(a::Symbol) = a ∉ nonflag_actions
const multi_actions = [:append_arg, :append_const]
is_multi_action(a::Symbol) = a ∈ multi_actions
const command_actions = [:command_arg, :command_flag]
is_command_action(a::Symbol) = a ∈ command_actions
# ArgParseSettingsError
struct ArgParseSettingsError <: Exception
text::AbstractString
end
serror(x...) = throw(ArgParseSettingsError(string(x...)))
# ArgConsumerType
struct ArgConsumerType
desc::Union{Int,Symbol}
function ArgConsumerType(n::Integer)
n ≥ 0 || serror("nargs can't be negative")
new(n)
end
function ArgConsumerType(s::Symbol)
s ∈ [:A, :?, :*, :+, :R] ||
serror("nargs must be an integer or one of 'A', '?', '*', '+', 'R'")
new(s)
end
end
ArgConsumerType(c::Char) = ArgConsumerType(Symbol(c))
ArgConsumerType() = ArgConsumerType(:A)
function show(io::IO, nargs::ArgConsumerType)
print(io, nargs.desc isa Int ? nargs.desc : "'" * string(nargs.desc) * "'")
end
is_multi_nargs(nargs::ArgConsumerType) = nargs.desc ∉ (0, :A, :?)
default_action(nargs::Integer) = nargs == 0 ? :store_true : :store_arg
default_action(nargs::Char) = :store_arg
default_action(nargs::Symbol) = :store_arg
default_action(nargs::ArgConsumerType) = default_action(nargs.desc)
# ArgParseGroup
mutable struct ArgParseGroup
name::AbstractString
desc::AbstractString
exclusive::Bool
required::Bool
function ArgParseGroup(name::AbstractString,
desc::AbstractString,
exclusive::Bool = false,
required::Bool = false
)
new(name, desc, exclusive, required)
end
end
const cmd_group = ArgParseGroup("commands", "commands")
const pos_group = ArgParseGroup("positional", "positional arguments")
const opt_group = ArgParseGroup("optional", "optional arguments")
const std_groups = [cmd_group, pos_group, opt_group]
# ArgParseField
mutable struct ArgParseField
dest_name::AbstractString
long_opt_name::Vector{AbstractString}
short_opt_name::Vector{AbstractString}
arg_type::Type
action::Symbol
nargs::ArgConsumerType
default
constant
range_tester::Function
required::Bool
eval_arg::Bool
help::AbstractString
metavar::Union{AbstractString,Vector{<:AbstractString}}
cmd_aliases::Vector{AbstractString}
group::AbstractString
fake::Bool
ArgParseField() = new("", AbstractString[], AbstractString[], Any, :store_true,
ArgConsumerType(), nothing, nothing, _->true, false, false, "", "",
AbstractString[], "", false)
end
is_flag(arg::ArgParseField) = is_flag_action(arg.action)
is_arg(arg::ArgParseField) = isempty(arg.long_opt_name) && isempty(arg.short_opt_name)
is_cmd(arg::ArgParseField) = is_command_action(arg.action)
const cmd_dest_name = "%COMMAND%"
const scmd_dest_name = :_COMMAND_
function show(io::IO, s::ArgParseField)
println(io, "ArgParseField(")
for f in fieldnames(ArgParseField)
println(io, " ", f, "=", getfield(s, f))
end
print(io, " )")
end
# ArgParseTable
mutable struct ArgParseTable
fields::Vector{ArgParseField}
subsettings::Dict{AbstractString,Any} # will actually be a Dict{AbstractString,ArgParseSettings}
ArgParseTable() = new(ArgParseField[], Dict{AbstractString,Any}())
end
# disallow alphanumeric, -
function check_prefix_chars(chars)
result = Set{Char}()
for c in chars
if isletter(c) || isnumeric(c) || c == '-'
throw(ArgParseError("‘$(c)’ is not allowed as prefix character"))
end
push!(result, c)
end
result
end
# ArgParseSettings
"""
ArgParseSettings
The `ArgParseSettings` object contains all the settings to be used during argument parsing. Settings
are divided in two groups: general settings and argument-table-related settings.
While the argument table requires specialized functions such as [`@add_arg_table!`](@ref) to be
defined and manipulated, general settings are simply object fields (most of them are `Bool` or
`String`) and can be passed to the constructor as keyword arguments, or directly set at any time.
This is the list of general settings currently available:
* `prog` (default = `""`): the name of the program, as displayed in the auto-generated help and
usage screens. If left empty, the source file name will be used.
* `description` (default = `""`): a description of what the program does, to be displayed in the
auto-generated help-screen, between the usage lines and the arguments description. If
`preformatted_description` is `false` (see below), it will be automatically formatted, but you can
still force newlines by using two consecutive newlines in the string, and manually control spaces
by using non-breakable spaces (the character `'\\ua0'`).
* `preformatted_description` (default = `false`): disable automatic formatting of `description`.
* `epilog` (default = `""`): like `description`, but will be displayed at the end of the
help-screen, after the arguments description. The same formatting rules also apply.
* `preformatted_epilog` (default = `false`): disable automatic formatting of `epilog`.
* `usage` (default = `""`): the usage line(s) to be displayed in the help screen and when an error
is found during parsing. If left empty, it will be auto-generated.
* `version` (default = `"Unknown version"`): version information. It's used by the `:show_version`
action.
* `add_help` (default = `true`): if `true`, a `--help, -h` option (triggering the `:show_help`
action) is added to the argument table.
* `add_version` (default = `false`): if `true`, a `--version` option (triggering the `:show_version`
action) is added to the argument table.
* `fromfile_prefix_chars` (default = `Set{Char}()`): an argument beginning with one of these
characters will specify a file from which arguments will be read, one argument read per line.
Alphanumeric characters and the hyphen-minus (`'-'`) are prohibited.
* `autofix_names` (default = `false`): if `true`, will try to automatically fix the uses of dashes
(`'-'`) and underscores (`'_'`) in option names and destinations: all underscores will be
converted to dashes in long option names; also, associated destination names, if auto-generated
(see the [Argument names](@ref) section), will have dashes replaced with underscores, both for
long options and for positional arguments. For example, an option declared as `"--my-opt"` will be
associated with the key `"my_opt"` by default. It is especially advisable to turn this option on
then parsing with the `as_symbols=true` argument to `parse_args`.
* `error_on_conflict` (default = `true`): if `true`, throw an error in case conflicting entries are
added to the argument table; if `false`, later entries will silently take precedence. See the
[Conflicts and overrides](@ref) srction for a detailed description of what conflicts are and what
is the exact behavior when this setting is `false`.
* `suppress_warnings` (default = `false`): if `true`, all warnings will be suppressed.
* `allow_ambiguous_opts` (default = `false`): if `true`, ambiguous options such as `-1` will be
accepted.
* `commands_are_required` (default = `true`): if `true`, commands will be mandatory. See the
[Commands](@ref) section.
* `exc_handler` (default = `ArgParse.default_handler`): this is a function which is invoked when an
error is detected during parsing (e.g. an option is not recognized, a required argument is not
passed etc.). It takes two arguments: the `settings::ArgParseSettings` object and the
`err::ArgParseError` exception. The default handler behaves differently depending on whether it's
invoked from a script or in an interactive environment (e.g. REPL/IJulia). In non-interactive
(script) mode, it calls `ArgParse.cmdline_handler`, which prints the error text and the usage
screen on standard error and exits Julia with error code 1:
```julia
function cmdline_handler(settings::ArgParseSettings, err, err_code::Int = 1)
println(stderr, err.text)
println(stderr, usage_string(settings))
exit(err_code)
end
```
In interactive mode instead it calls the function `ArgParse.debug_handler`, which just rethrows
the error.
* `exit_after_help` (default = `!isinteractive()`): exit Julia (with error code `0`) when the
`:show_help` or `:show_version` actions are triggered. If `false`, those actions will just stop
the parsing and make `parse_args` return `nothing`.
Here is a usage example:
```julia
settings = ArgParseSettings(description = "This program does something",
commands_are_required = false,
version = "1.0",
add_version = true)
```
which is also equivalent to:
```julia
settings = ArgParseSettings()
settings.description = "This program does something."
settings.commands_are_required = false
settings.version = "1.0"
settings.add_version = true
```
As a shorthand, the `description` field can be passed without keyword, which makes this equivalent
to the above:
```julia
settings = ArgParseSettings("This program does something",
commands_are_required = false,
version = "1.0",
add_version = true)
```
Most settings won't take effect until `parse_args` is invoked, but a few will have immediate
effects: `autofix_names`, `error_on_conflict`, `suppress_warnings`, `allow_ambiguous_opts`.
"""
mutable struct ArgParseSettings
prog::AbstractString
description::AbstractString
epilog::AbstractString
usage::AbstractString
version::AbstractString
add_help::Bool
add_version::Bool
fromfile_prefix_chars::Set{Char}
autofix_names::Bool
error_on_conflict::Bool
suppress_warnings::Bool
allow_ambiguous_opts::Bool
commands_are_required::Bool
args_groups::Vector{ArgParseGroup}
default_group::AbstractString
args_table::ArgParseTable
exc_handler::Function
preformatted_description::Bool
preformatted_epilog::Bool
exit_after_help::Bool
function ArgParseSettings(;prog::AbstractString = Base.source_path() ≢ nothing ?
basename(Base.source_path()) :
"",
description::AbstractString = "",
epilog::AbstractString = "",
usage::AbstractString = "",
version::AbstractString = "Unspecified version",
add_help::Bool = true,
add_version::Bool = false,
fromfile_prefix_chars = Set{Char}(),
autofix_names::Bool = false,
error_on_conflict::Bool = true,
suppress_warnings::Bool = false,
allow_ambiguous_opts::Bool = false,
commands_are_required::Bool = true,
exc_handler::Function = default_handler,
preformatted_description::Bool = false,
preformatted_epilog::Bool = false,
exit_after_help::Bool = !isinteractive()
)
fromfile_prefix_chars = check_prefix_chars(fromfile_prefix_chars)
return new(
prog, description, epilog, usage, version, add_help, add_version,
fromfile_prefix_chars, autofix_names, error_on_conflict,
suppress_warnings, allow_ambiguous_opts, commands_are_required,
copy(std_groups), "", ArgParseTable(), exc_handler,
preformatted_description, preformatted_epilog,
exit_after_help
)
end
end
ArgParseSettings(desc::AbstractString; kw...) = ArgParseSettings(; description = desc, kw...)
function show(io::IO, s::ArgParseSettings)
println(io, "ArgParseSettings(")
for f in fieldnames(ArgParseSettings)
f ∈ (:args_groups, :args_table) && continue
println(io, " ", f, "=", getfield(s, f))
end
println(io, " >> ", usage_string(s))
print(io, " )")
end
ArgName{T<:AbstractString} = Union{T, Vector{T}}
getindex(s::ArgParseSettings, c::AbstractString) = s.args_table.subsettings[c]
haskey(s::ArgParseSettings, c::AbstractString) = haskey(s.args_table.subsettings, c)
setindex!(s::ArgParseSettings, x::ArgParseSettings, c::AbstractString) =
setindex!(s.args_table.subsettings, x, c)
# fields declarations sanity checks
function check_name_format(name::ArgName)
isempty(name) && serror("empty name")
name isa Vector || return true
allopts = true
allargs = true
for n in name
isempty(n) && serror("empty name")
if startswith(n, '-')
allargs = false
else
allopts = false
end
end
!(allargs || allopts) && serror("multiple names must be either all options or all non-options")
for i1 = 1:length(name), i2 = i1+1:length(name)
name[i1] == name[i2] && serror("duplicate name $(name[i1])")
end
return true
end
function check_type(opt, T::Type, message::AbstractString)
opt isa T || serror(message)
return true
end
function check_eltype(opt, T::Type, message::AbstractString)
eltype(opt) <: T || serror(message)
return true
end
function warn_extra_opts(opts, valid_keys::Vector{Symbol})
for k in opts
k ∈ valid_keys || @warn "ignored option: $k"
end
return true
end
function check_action_is_valid(action::Symbol)
action ∈ all_actions || serror("invalid action: $action")
end
function check_nargs_and_action(nargs::ArgConsumerType, action::Symbol)
is_flag_action(action) && nargs.desc ≠ 0 && nargs.desc ≠ :A &&
serror("incompatible nargs and action (flag-action $action, nargs=$nargs)")
is_command_action(action) && nargs.desc ≠ :A &&
serror("incompatible nargs and action (command action, nargs=$nargs)")
!is_flag_action(action) && nargs.desc == 0 &&
serror("incompatible nargs and action (non-flag-action $action, nargs=$nargs)")
return true
end
function check_long_opt_name(name::AbstractString, settings::ArgParseSettings)
'=' ∈ name && serror("illegal option name: $name (contains '=')")
occursin(r"\s", name) && serror("illegal option name: $name (contains whitespace)")
settings.add_help &&
name == "help" && serror("option --help is reserved in the current settings")
settings.add_version &&
name == "version" && serror("option --version is reserved in the current settings")
return true
end
function check_short_opt_name(name::AbstractString, settings::ArgParseSettings)
length(name) ≠ 1 && serror("short options must use a single character")
name == "=" && serror("illegal short option name: $name")
occursin(r"\s", name) && serror("illegal option name: $name (contains whitespace)")
!settings.allow_ambiguous_opts && occursin(r"[0-9.(]", name) &&
serror("ambiguous option name: $name (disabled in current settings)")
settings.add_help && name == "h" &&
serror("option -h is reserved for help in the current settings")
return true
end
function check_arg_name(name::AbstractString)
occursin(r"^%[A-Z]*%$", name) && serror("invalid positional arg name: $name (is reserved)")
return true
end
function check_cmd_name(name::AbstractString)
isempty(name) && found_a_bug()
startswith(name, '-') && found_a_bug()
occursin(r"\s", name) && serror("invalid command name: $name (contains whitespace)")
occursin(r"^%[A-Z]*%$", name) && serror("invalid command name: $name (is reserved)")
return true
end
function check_dest_name(name::AbstractString)
occursin(r"^%[A-Z]*%$", name) && serror("invalid dest_name: $name (is reserved)")
return true
end
function idstring(arg::ArgParseField)
if is_arg(arg)
return "argument $(arg.metavar)"
elseif !isempty(arg.long_opt_name)
return "option --$(arg.long_opt_name[1])"
else
return "option -$(arg.short_opt_name[1])"
end
end
# TODO improve (test more nonsensical cases)
function check_arg_makes_sense(settings::ArgParseSettings, arg::ArgParseField)
is_arg(arg) || return true
is_command_action(arg.action) && return true
for f in settings.args_table.fields
is_arg(f) || continue
is_command_action(f.action) && serror("non-command $(idstring(arg)) can't follow commands")
!f.required && arg.required &&
serror("required $(idstring(arg)) can't follow non-required arguments")
end
return true
end
function check_conflicts_with_commands(settings::ArgParseSettings,
new_arg::ArgParseField,
allow_future_merge::Bool)
for cmd in keys(settings.args_table.subsettings)
cmd == new_arg.dest_name &&
serror("$(idstring(new_arg)) has the same destination of a command: $cmd")
end
for a in settings.args_table.fields
if is_cmd(a) && !is_cmd(new_arg)
for l1 in a.long_opt_name, l2 in new_arg.long_opt_name
# TODO be less strict here and below, and allow partial override?
l1 == l2 && serror("long opt name --$(l1) already in use by command $(a.constant)")
end
for s1 in a.short_opt_name, s2 in new_arg.short_opt_name
s1 == s2 && serror("short opt name -$(s1) already in use by command $(a.constant)")
end
elseif is_cmd(a) && is_cmd(new_arg)
if a.constant == new_arg.constant
allow_future_merge || serror("command $(a.constant) already in use")
is_arg(a) ≠ is_arg(new_arg) &&
serror("$(idstring(a)) and $(idstring(new_arg)) are incompatible")
else
for al in new_arg.cmd_aliases
al == a.constant && serror("invalid alias $al, command already in use")
end
end
end
end
return true
end
function check_conflicts_with_commands(settings::ArgParseSettings, new_cmd::AbstractString)
for a in settings.args_table.fields
new_cmd == a.dest_name &&
serror("command $new_cmd has the same destination of $(idstring(a))")
end
return true
end
function check_for_duplicates(args::Vector{ArgParseField}, new_arg::ArgParseField)
for a in args
for l1 in a.long_opt_name, l2 in new_arg.long_opt_name
l1 == l2 && serror("duplicate long opt name $l1")
end
for s1 in a.short_opt_name, s2 in new_arg.short_opt_name
s1 == s2 && serror("duplicate short opt name $s1")
end
if is_arg(a) && is_arg(new_arg) && a.metavar == new_arg.metavar
serror("two arguments have the same metavar: $(a.metavar)")
end
if is_cmd(a) && is_cmd(new_arg)
for al1 in a.cmd_aliases, al2 in new_arg.cmd_aliases
al1 == al2 &&
serror("both commands $(a.constant) and $(new_arg.constant) use the same alias $al1")
end
for al1 in a.cmd_aliases
al1 == new_arg.constant &&
serror("$al1 already in use as an alias command $(a.constant)")
end
for al2 in new_arg.cmd_aliases
al2 == a.constant && serror("invalid alias $al2, command already in use")
end
end
if a.dest_name == new_arg.dest_name
a.arg_type == new_arg.arg_type ||
serror("$(idstring(a)) and $(idstring(new_arg)) have the same destination but different arg types")
if (is_multi_action(a.action) && !is_multi_action(new_arg.action)) ||
(!is_multi_action(a.action) && is_multi_action(new_arg.action))
serror("$(idstring(a)) and $(idstring(new_arg)) have the same destination but incompatible actions")
end
end
end
return true
end
check_default_type(default::Nothing, arg_type::Type) = true
function check_default_type(default::D, arg_type::Type) where D
D <: arg_type || serror("typeof(default)=$D is incompatible with arg_type=$arg_type)")
return true
end
check_default_type_multi_action(default::Nothing, arg_type::Type) = true
function check_default_type_multi_action(default::Vector{D}, arg_type::Type) where D
arg_type <: D || serror("typeof(default)=Vector{$D} can't hold arguments of type arg_type=$arg_type)")
all(x->(x isa arg_type), default) || serror("all elements of the default value must be of type $arg_type)")
return true
end
check_default_type_multi_action(default::D, arg_type::Type) where D =
serror("typeof(default)=$D is incompatible with the action, it should be a Vector")
check_default_type_multi_nargs(default::Nothing, arg_type::Type) = true
function check_default_type_multi_nargs(default::Vector, arg_type::Type)
all(x->(x isa arg_type), default) || serror("all elements of the default value must be of type $arg_type")
return true
end
check_default_type_multi_nargs(default::D, arg_type::Type) where D =
serror("typeof(default)=$D is incompatible with nargs, it should be a Vector")
check_default_type_multi2(default::Nothing, arg_type::Type) = true
function check_default_type_multi2(default::Vector{D}, arg_type::Type) where D
Vector{arg_type} <: D || serror("typeof(default)=Vector{$D} can't hold Vectors of arguments " *
"of type arg_type=$arg_type)")
all(y->(y isa Vector), default) ||
serror("the default $(default) is incompatible with the action and nargs, it should be a Vector of Vectors")
all(y->all(x->(x isa arg_type), y), default) || serror("all elements of the default value must be of type $arg_type")
return true
end
check_default_type_multi2(default::D, arg_type::Type) where D =
serror("the default $(default) is incompatible with the action and nargs, it should be a Vector of Vectors")
check_range_default(default::Nothing, range_tester::Function) = true
function check_range_default(default, range_tester::Function)
local res::Bool
try
res = range_tester(default)
catch err
serror("the range_tester function must be defined for the default value, and return a Bool")
end
res || serror("the default value must pass the range_tester function")
return true
end
check_range_default_multi(default::Nothing, range_tester::Function) = true
function check_range_default_multi(default::Vector, range_tester::Function)
for d in default
local res::Bool
try
res = range_tester(d)
catch err
serror("the range_tester function must be defined for all the default values, and return a Bool")
end
res || serror("all of the default values must pass the range_tester function")
end
return true
end
check_range_default_multi2(default::Nothing, range_tester::Function) = true
function check_range_default_multi2(default::Vector, range_tester::Function)
for dl in default, d in dl
local res::Bool
try
res = range_tester(d)
catch err
serror("the range_tester function must be defined for all the default values, and return a Bool")
end
res || serror("all of the default values must pass the range_tester function")
end
return true
end
function check_metavar(metavar::AbstractString)
isempty(metavar) && serror("empty metavar")
startswith(metavar, '-') && serror("metavars cannot begin with -")
occursin(r"\s", metavar) && serror("illegal metavar name: $metavar (contains whitespace)")
return true
end
function check_metavar(metavar::Vector{<:AbstractString})
foreach(check_metavar, metavar)
return true
end
function check_group_name(name::AbstractString)
isempty(name) && serror("empty group name")
startswith(name, '#') && serror("invalid group name (starts with #)")
return true
end
# add_arg_table! and related
function name_to_fieldnames!(settings::ArgParseSettings, name::ArgName)
pos_arg = ""
long_opts = AbstractString[]
short_opts = AbstractString[]
aliases = AbstractString[]
r(n) = settings.autofix_names ? replace(n, '_' => '-') : n
function do_one(n, cmd_check = true)
if startswith(n, "--")
n == "--" && serror("illegal option name: --")
long_opt_name = r(n[3:end])
check_long_opt_name(long_opt_name, settings)
push!(long_opts, long_opt_name)
elseif startswith(n, '-')
n == "-" && serror("illegal option name: -")
short_opt_name = n[2:end]
check_short_opt_name(short_opt_name, settings)
push!(short_opts, short_opt_name)
else
if cmd_check
check_cmd_name(n)
else
check_arg_name(n)
end
if isempty(pos_arg)
pos_arg = n
else
push!(aliases, n)
end
end
end
if name isa Vector
foreach(do_one, name)
else
do_one(name, false)
end
return pos_arg, long_opts, short_opts, aliases
end
function auto_dest_name(pos_arg::AbstractString,
long_opts::Vector{AbstractString},
short_opts::Vector{AbstractString},
autofix_names::Bool)
r(n) = autofix_names ? replace(n, '-' => '_') : n
isempty(pos_arg) || return r(pos_arg)
isempty(long_opts) || return r(long_opts[1])
@assert !isempty(short_opts)
return short_opts[1]
end
function auto_metavar(dest_name::AbstractString, is_opt::Bool)
is_opt || return dest_name
prefix = occursin(r"^[[:alpha:]_]", dest_name) ? "" : "_"
return prefix * uppercase(dest_name)
end
function get_cmd_prog_hint(arg::ArgParseField)
isempty(arg.short_opt_name) || return "-" * arg.short_opt_name[1]
isempty(arg.long_opt_name) || return "--" * arg.long_opt_name[1]
return arg.constant
end
"""
add_arg_table!(settings, [arg_name [,arg_options]]...)
This function is very similar to the macro version [`@add_arg_table!`](@ref). Its syntax is stricter:
tuples and blocks are not allowed and argument options are explicitly specified as `Dict` objects.
However, since it doesn't involve macros, it offers more flexibility in other respects, e.g. the
`arg_name` entries need not be explicit, they can be anything which evaluates to a `String` or a
`Vector{String}`.
Example:
```julia
add_arg_table!(settings,
["--opt1", "-o"],
Dict(
:help => "an option with an argument"
),
"--opt2",
"arg1",
Dict(
:help => "a positional argument"
:required => true
))
```
"""
function add_arg_table!(settings::ArgParseSettings, table::Union{ArgName,Vector,Dict}...)
has_name = false
for i = 1:length(table)
!has_name && !(table[i] isa ArgName) &&
serror("option field must be preceded by the arg name")
has_name = true
end
i = 1
while i ≤ length(table)
if i+1 ≤ length(table) && !(table[i+1] isa ArgName)
add_arg_field!(settings, table[i]; table[i+1]...)
i += 2
else
add_arg_field!(settings, table[i])
i += 1
end
end
return settings
end
"""
@add_arg_table!(settings, table...)
This macro adds a table of arguments and options to the given `settings`. It can be invoked multiple
times. The arguments groups are determined automatically, or the current default group is used if
specified (see the [Argument groups](@ref) section for more details).
The `table` is a list in which each element can be either `String`, or a tuple or a vector of
`String`, or an assigmment expression, or a block:
* a `String`, a tuple or a vector introduces a new positional argument or option. Tuples and vectors
are only allowed for options or commands, and provide alternative names (e.g. `["--opt", "-o"]` or
`["checkout", "co"]`)
* assignment expressions (i.e. expressions using `=`, `:=` or `=>`) describe the previous argument
behavior (e.g. `help = "an option"` or `required => false`). See the
[Argument entry settings](@ref) section for a complete description
* blocks (`begin...end` or lists of expressions in parentheses separated by semicolons) are useful
to group entries and span multiple lines.
These rules allow for a variety usage styles, which are discussed in the
[Argument table styles](@ref) section. In the rest of the documentation, we will mostly use this
style:
```julia
@add_arg_table! settings begin
"--opt1", "-o"
help = "an option with an argument"
"--opt2"
"arg1"
help = "a positional argument"
required = true
end
```
In the above example, the `table` is put in a single `begin...end` block and the line
`"--opt1", "-o"` is parsed as a tuple; indentation is used to help readability.
See also the function [`add_arg_table!`](@ref).
"""
macro add_arg_table!(s, x...)
_add_arg_table!(s, x...)
end
# Moved all the code to a function just to make the deprecation work
function _add_arg_table!(s, x...)
# transform the tuple into a vector, so that
# we can manipulate it
x = Any[x...]
# escape the ArgParseSettings
s = esc(s)
z = esc(gensym())
# start building the return expression
exret = quote
$z = $s
$z isa ArgParseSettings ||
serror("first argument to @add_arg_table! must be of type ArgParseSettings")
end
# initialize the name and the options expression
name = nothing
exopt = Any[:Dict]
# iterate over the arguments
i = 1
while i ≤ length(x)
y = x[i]
if Meta.isexpr(y, :block)
# found a begin..end block: expand its contents
# in-place and restart from the same position
splice!(x, i, y.args)
continue
elseif Meta.isexpr(y, :macrocall) &&
((y.args[1] == GlobalRef(Core, Symbol("@doc"))) ||
(Meta.isexpr(y.args[1], :core) && y.args[1].args[1] == Symbol("@doc")))
# Was parsed as doc syntax. Split into components
splice!(x, i, y.args[2:end])
continue
elseif (y isa AbstractString) || Meta.isexpr(y, (:vect, :tuple))
Meta.isexpr(y, :tuple) && (y.head = :vect) # transform tuples into vectors
if Meta.isexpr(y, :vect) && (isempty(y.args) || !all(x->x isa AbstractString, y.args))
# heterogeneous elements: splice it in place, just like blocks
splice!(x, i, y.args)
continue
end
# found a string, or a vector/tuple of strings:
# this must be the option name
if name ≢ nothing
# there was a previous arg field on hold
# first, concretely build the options
opt = Expr(:call, exopt...)
kopts = Expr(:parameters, Expr(:(...), opt))
# then, call add_arg_field!
aaf = Expr(:call, :add_arg_field!, kopts, z, name)
# store it in the output expression
exret = quote
$exret
$aaf
end
end
# put the name on hold, reinitialize the options expression
name = y
exopt = Any[:Dict]
i += 1
elseif Meta.isexpr(y, (:(=), :(:=), :kw))
# found an assignment: add it to the current options expression
name ≢ nothing ||
serror("malformed table: description fields must be preceded by the arg name")
push!(exopt, Expr(:call, :(=>), Expr(:quote, y.args[1]), esc(y.args[2])))
i += 1
elseif Meta.isexpr(y, :call) && y.args[1] == :(=>)
# found an assignment: add it to the current options expression
name ≢ nothing ||
serror("malformed table: description fields must be preceded by the arg name")
push!(exopt, Expr(:call, :(=>), Expr(:quote, y.args[2]), esc(y.args[3])))
i += 1
elseif (y isa LineNumberNode) || Meta.isexpr(y, :line)
# a line number node, ignore
i += 1
continue
else
# anything else: ignore, but issue a warning
@warn "@add_arg_table!: ignoring expression $y"
i += 1
end
end
if name ≢ nothing
# there is an arg field on hold
# same as above
opt = Expr(:call, exopt...)
kopts = Expr(:parameters, Expr(:(...), opt))
aaf = Expr(:call, :add_arg_field!, kopts, z, name)
exret = quote
$exret
$aaf
end
end
# the return value when invoking the macro
# will be the ArgParseSettings object
exret = quote
$exret
$z
end
# return the resulting expression
exret
end
function get_group(group::AbstractString, arg::ArgParseField, settings::ArgParseSettings)
if isempty(group)
is_cmd(arg) && return cmd_group
is_arg(arg) && return pos_group
return opt_group
else
for ag in settings.args_groups
group == ag.name && return ag
end
serror("group $group not found, use add_arg_group! to add it")
end
found_a_bug()
end
function add_arg_field!(settings::ArgParseSettings, name::ArgName; desc...)
check_name_format(name)
supplied_opts = keys(desc)
@defaults desc begin
nargs = ArgConsumerType()
action = default_action(nargs)
arg_type = Any
default = nothing
constant = nothing
required = false
range_tester = x->true
eval_arg = false
dest_name = ""
help = ""
metavar = ""
force_override = !settings.error_on_conflict
group = settings.default_group
end
check_type(nargs, Union{ArgConsumerType,Int,Char}, "nargs must be an Int or a Char")
check_type(action, Union{AbstractString,Symbol}, "action must be an AbstractString or a Symbol")
check_type(arg_type, Type, "invalid arg_type")
check_type(required, Bool, "required must be a Bool")
check_type(range_tester, Function, "range_tester must be a Function")
check_type(dest_name, AbstractString, "dest_name must be an AbstractString")
check_type(help, AbstractString, "help must be an AbstractString")
# Check metavar's type to be either an AbstractString or a
# Vector{T<:AbstractString}
metavar_error = "metavar must be an AbstractString or a Vector{<:AbstractString}"
if !(metavar isa AbstractString)
check_type(metavar, Vector, metavar_error)
check_eltype(metavar, AbstractString, metavar_error)
check_type(nargs, Integer, "nargs must be an integer for multiple metavars")
length(metavar) == nargs || serror("metavar array must have length of nargs")
end
check_type(force_override, Bool, "force_override must be a Bool")
check_type(group, Union{AbstractString,Symbol}, "group must be an AbstractString or a Symbol")
nargs isa ArgConsumerType || (nargs = ArgConsumerType(nargs))
action isa Symbol || (action = Symbol(action))
is_opt = name isa Vector ?
startswith(first(name), '-') :
startswith(name, '-')
check_action_is_valid(action)
action == :command && (action = is_opt ? :command_flag : :command_arg)
check_nargs_and_action(nargs, action)
new_arg = ArgParseField()
is_flag = is_flag_action(action)
if !is_opt
is_flag && serror("invalid action for positional argument: $action")
nargs.desc == :? && serror("invalid 'nargs' for positional argument: '?'")
metavar isa Vector && serror("multiple metavars only supported for optional arguments")
end
pos_arg, long_opts, short_opts, cmd_aliases = name_to_fieldnames!(settings, name)
if !isempty(cmd_aliases)
is_command_action(action) || serror("only command arguments can have multiple names (aliases)")
end
new_arg.dest_name = auto_dest_name(pos_arg, long_opts, short_opts, settings.autofix_names)
new_arg.long_opt_name = long_opts
new_arg.short_opt_name = short_opts
new_arg.cmd_aliases = cmd_aliases
new_arg.nargs = nargs
new_arg.action = action
group = string(group)
if :group ∈ supplied_opts && !isempty(group)
check_group_name(group)
end
arg_group = get_group(group, new_arg, settings)
new_arg.group = arg_group.name
if arg_group.exclusive && (!is_opt || is_command_action(action))
serror("group $(new_arg.group) is mutually-exclusive, actions and commands are not allowed")
end
if action ∈ (:store_const, :append_const) && :constant ∉ supplied_opts
serror("action $action requires the 'constant' field")
end
valid_keys = [:nargs, :action, :help, :force_override, :group]
if is_flag
if action ∈ (:store_const, :append_const)
append!(valid_keys, [:default, :constant, :arg_type, :dest_name])
elseif action ∈ (:store_true, :store_false, :count_invocations, :command_flag)
push!(valid_keys, :dest_name)
else
action ∈ (:show_help, :show_version) || found_a_bug()
end
elseif is_opt
append!(valid_keys,
[:arg_type, :default, :range_tester, :dest_name, :required, :metavar, :eval_arg])
nargs.desc == :? && push!(valid_keys, :constant)
elseif action ≠ :command_arg
append!(valid_keys, [:arg_type, :default, :range_tester, :required, :metavar])
end
settings.suppress_warnings || warn_extra_opts(supplied_opts, valid_keys)
if is_command_action(action)
if (:dest_name ∈ supplied_opts) && (:dest_name ∈ valid_keys)
cmd_name = dest_name
else
cmd_name = new_arg.dest_name
end
end
if (:dest_name ∈ supplied_opts) && (:dest_name ∈ valid_keys) && (action ≠ :command_flag)
new_arg.dest_name = dest_name
end
check_dest_name(dest_name)
set_if_valid(k, x) = k ∈ valid_keys && setfield!(new_arg, k, x)
set_if_valid(:arg_type, arg_type)
set_if_valid(:default, deepcopy(default))
set_if_valid(:constant, deepcopy(constant))
set_if_valid(:range_tester, range_tester)
set_if_valid(:required, required)
set_if_valid(:help, help)
set_if_valid(:metavar, metavar)
set_if_valid(:eval_arg, eval_arg)
if !is_flag