Skip to content

Commit f0ec2c9

Browse files
committed
Support leftover positional arguments and bump version to 0.2.4
1 parent 171c5f5 commit f0ec2c9

File tree

10 files changed

+106
-24
lines changed

10 files changed

+106
-24
lines changed

Project.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
name = "ArgMacros"
22
uuid = "dbc42088-9de8-42a0-8ec8-2cd114e1ea3e"
33
authors = ["zachmatson"]
4-
version = "0.2.3"
4+
version = "0.2.4"
55

66
[deps]
77
TextWrap = "b718987f-49a8-5099-9789-dcd902bef87d"

docs/src/index.md

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ The types of arguments supported are broken down into two categories:
6363
- [`@positionalrequired`](@ref)
6464
- [`@positionaldefault`](@ref)
6565
- [`@positionaloptional`](@ref)
66+
- [`@positionalleftover`](@ref)
6667

6768
The arguments are either required, optional, or have default values, as is evident
6869
in their names. Additionally, [`@argumentflag`](@ref) checks the presence of a flag,
@@ -76,7 +77,8 @@ For this reason, ALL options must be declared before ANY positionals, required
7677
positionals must be declared before default/optional ones, and positional arguments
7778
must be declared in the order the user is expected to enter them.
7879

79-
You should make your argument types `Symbol`, `String`, or subtypes of `Number`.
80+
You should make your argument types `Symbol`, `String`, subtypes of `Number`,
81+
or some type with a sensible direct conversion from `String`.
8082

8183
Here is an example with some arguments:
8284
```julia
@@ -100,7 +102,7 @@ of the name of local variable they will be stored in.
100102
but making sure to follow them is essential to your code running properly.
101103
Additionally, make sure not to declare multiple arguments using the same flags.
102104
Other than the reserved `-h` and `--help` flags, this will not be detected automatically
103-
at compile time, and could lead to undefined behavior.
105+
during compilation, and could lead to undefined behavior.
104106

105107
## Using Argument Values
106108

@@ -267,10 +269,19 @@ end
267269
## Leftover Arguments
268270

269271
By default, the program will exit and print a warning if more arguments are given than the program declares.
270-
If you don't want this to happen, include the [`@allowextraarguments`](@ref) macro.
272+
If you don't want this to happen, include the [`@allowextraarguments`](@ref) macro, or use [`@positionalleftover`](@ref)
273+
to store the extra arguments into a variable.
271274

272-
This can occur anywhere inside the `@...arguments` block, but the recommended placement is at the end,
273-
after all other help, test, and argument declarations.
275+
[`@allowextraarguments`](@ref) can occur anywhere inside the `@...arguments` block, but the recommended placement is at the end,
276+
after all other help, test, and argument declarations. [`@positionalleftover`](@ref) must be the last argument declared in the block.
277+
278+
```julia
279+
@inlinearguments begin
280+
...
281+
@positionalleftover String file_names "files"
282+
...
283+
end
284+
```
274285

275286
```julia
276287
@inlinearguments begin

docs/src/macros.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,12 +29,14 @@ Additionally, it is recommended to specify the short flag first when using multi
2929

3030
These arguments are specified by their position in the command.
3131
You must specify these in your code in the same order that users are expected to enter them.
32-
It is important to put these after all option arguments, and specify the required positional arguments first.
32+
It is important to put these after all option arguments, and specify the required positional arguments first.
33+
If you want to specify leftover positional arguments, they must come after all other arguments.
3334

3435
```@docs
3536
@positionalrequired
3637
@positionaldefault
3738
@positionaloptional
39+
@positionalleftover
3840
```
3941

4042
## Help Options

src/ArgMacros.jl

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@ export @beginarguments, @inlinearguments, @structarguments,
1717
export @helpusage, @helpdescription, @helpepilog
1818
export @argumentrequired, @argumentdefault, @argumentoptional,
1919
@argumentflag, @argumentcount
20-
export @positionalrequired, @positionaldefault, @positionaloptional
20+
export @positionalrequired, @positionaldefault, @positionaloptional,
21+
@positionalleftover
2122
export @arghelp, @argtest, @allowextraarguments
2223

2324
include("constants.jl")
@@ -26,12 +27,6 @@ include("help.jl")
2627
include("argsparsing.jl")
2728
include("macros.jl")
2829

29-
#=
30-
TODO
31-
multi-value arguments
32-
escaping behavior
33-
=#
34-
3530
include("precompile.jl")
3631
_precompile_()
3732

src/argsparsing.jl

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,11 @@ function _converttype!(::Type{T}, s::String, name::String)::T where T
121121
end
122122
end
123123

124+
"Convert a vector of strings to Number type with parse or other types with direct conversions"
125+
function _converttype!(::Type{T}, values::Vector{String}, name::String)::Vector{T} where T
126+
map(s -> _converttype!(T, s, name), values)
127+
end
128+
124129
"Quit program if the value is nothing"
125130
function _converttype!(::Type{T}, ::Nothing, name::String)::Nothing where T
126131
_quit_try_help("Argument $name missing")

src/constants.jl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,3 +18,4 @@ const POSITIONAL_REQUIRED_SYMBOL = Symbol("@positionalrequired")
1818
const POSITIONAL_DEFAULT_SYMBOL = Symbol("@positionaldefault")
1919
const POSITIONAL_OPTIONAL_SYMBOL = Symbol("@positionaloptional")
2020
const POSITIONAL_OPTIONAL_SYMBOLS = [POSITIONAL_DEFAULT_SYMBOL, POSITIONAL_OPTIONAL_SYMBOL]
21+
const POSITIONAL_LEFTOVER_SYMBOL = Symbol("@positionalleftover")

src/handleast.jl

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,14 @@ end
1515

1616
"""
1717
Enforce argument declaration ordering:
18-
Flagged → Required Positional → Optional Positional
18+
Flagged → Required Positional → Optional Positional → Leftover Positional
1919
2020
Throw `ArgumentError` if ordering violates this rule.
2121
"""
2222
function _validateorder(block::Expr)
2323
encountered_positional = false
2424
encoundered_optional_positional = false
25+
encountered_leftover_positional = false
2526

2627
for arg in _getmacrocalls(block)
2728
# Fix namespace issues
@@ -44,6 +45,16 @@ function _validateorder(block::Expr)
4445
elseif macroname in POSITIONAL_OPTIONAL_SYMBOLS
4546
encountered_positional = true
4647
encoundered_optional_positional = true
48+
49+
if encountered_leftover_positional
50+
throw(ArgumentError(
51+
"Leftover arguments must be declared after all other arguments.\nFrom: $arg"
52+
))
53+
end
54+
elseif macroname == POSITIONAL_LEFTOVER_SYMBOL
55+
encountered_positional = true
56+
encoundered_optional_positional = true
57+
encountered_leftover_positional = true
4758
end
4859
end
4960
end
@@ -52,12 +63,14 @@ end
5263
function _getargumentpair(arg::Expr)::Union{Expr, Nothing}
5364
macroname::Symbol = _get_macroname(arg)
5465

55-
if macroname in (ARGUMENT_REQUIRED_SYMBOL, POSITIONAL_REQUIRED_SYMBOL)
56-
:($(arg.args[4])::$(arg.args[3]))
57-
elseif macroname in (ARGUMENT_DEFAULT_SYMBOL, POSITIONAL_DEFAULT_SYMBOL)
66+
if macroname in (ARGUMENT_DEFAULT_SYMBOL, POSITIONAL_DEFAULT_SYMBOL)
5867
:($(arg.args[5])::$(arg.args[3]))
68+
elseif macroname in (ARGUMENT_REQUIRED_SYMBOL, POSITIONAL_REQUIRED_SYMBOL)
69+
:($(arg.args[4])::$(arg.args[3]))
5970
elseif macroname in (ARGUMENT_OPTIONAL_SYMBOL, POSITIONAL_OPTIONAL_SYMBOL)
6071
:($(arg.args[4])::Union{$(arg.args[3]), Nothing})
72+
elseif macroname == POSITIONAL_LEFTOVER_SYMBOL
73+
:($(arg.args[4])::Vector{$(arg.args[3])})
6174
elseif macroname == ARGUMENT_FLAG_SYMBOL
6275
:($(arg.args[3])::Bool)
6376
elseif macroname == ARGUMENT_COUNT_SYMBOL

src/macros.jl

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -383,6 +383,39 @@ macro positionaloptional(type::Symbol, local_name::Symbol, help_name::Union{Stri
383383
end
384384
end
385385

386+
"""
387+
@positionalleftover type local_name [help_name]
388+
389+
Get any leftover positional arguments after all other arguments have been parsed, and store in
390+
variable `local_name` with the type `Vector{type}`. This vector will be empty if there are no
391+
leftover arguments.
392+
393+
This macro must be used after all other arguments have been declared, as positional arguments are
394+
read in order after all flag/option arguments have been read.
395+
Using this macro means all input will be captured by this argument if not by another, so
396+
`@allowextraarguments` is not necesesary when this macro is used.
397+
`help_name` used instead of `local_name` in messages to user if specified.
398+
399+
Must be used in `@beginarguments begin ... end` block
400+
401+
# Example
402+
```julia
403+
@beginarguments begin
404+
...
405+
@positionalleftover String file_names "files"
406+
...
407+
end
408+
```
409+
"""
410+
macro positionalleftover(type::Symbol, local_name::Symbol, help_name::Union{String, Nothing}=nothing)
411+
help_name_str::String = something(help_name, String(local_name))
412+
return quote
413+
local $(esc(esc(local_name)))::$(esc(esc(Vector))){$(esc(esc(type)))} =
414+
_converttype!($type, splitargs, $help_name_str)
415+
empty!(splitargs)
416+
end
417+
end
418+
386419
"""
387420
@argtest argname func [desc]
388421
@@ -419,6 +452,10 @@ Disables the default behavior of printing a message and exiting
419452
the program when not all values in `ARGS` could be assigned to
420453
specified arguments.
421454
455+
This will not capture any arguments, use `@positionalleftover`
456+
if you want to capture extra arguments that could not otherwise
457+
be assigned.
458+
422459
Must be used in `@beginarguments begin ... end` block
423460
424461
# Example

src/precompile.jl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ function _precompile_()
88
arg_types = (Float64, Float32, Int64, Int32, String, Symbol)
99
for T in arg_types
1010
Base.precompile(Tuple{typeof(ArgMacros._converttype!),Type{T},String,String})
11+
Base.precompile(Tuple{typeof(ArgMacros._converttype!),Type{T},Vector{String},String})
1112
Base.precompile(Tuple{typeof(ArgMacros._converttype!),Type{T},Nothing,String})
1213
for T2 in arg_types
1314
Base.precompile(Tuple{typeof(ArgMacros._converttype!),Type{T},T2,String})

test/runtests.jl

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
1-
println("Times for 8 argument example using all macros")
1+
println("Times for 11 argument example using all macros")
22
println("Load Time (using ArgMacros)")
33
@time using ArgMacros
44
using Test
55

66
# Commands Set 1
77
let
88
empty!(ARGS)
9-
append!(ARGS, ["TEST STRING F", "-deeee", "30", "3.14", "-b=6.28", "--cc", "ArgMacros", "-a", "2"])
9+
append!(ARGS, ["TEST STRING F", "-deeee", "30", "3.14", "-b=6.28", "--cc", "ArgMacros", "-a", "2", "4", "5", "6"])
1010

1111
println("Inline Arguments Time with Precompile")
1212
let
@@ -22,6 +22,7 @@ let
2222
@positionaldefault Int 20 g
2323
@argtest g (x -> x % 10 == 0)
2424
@positionaloptional Float64 h
25+
@positionalleftover Int64 i
2526
end
2627
end
2728

@@ -38,6 +39,7 @@ let
3839
@positionaldefault Int 20 g
3940
@argtest g (x -> x % 10 == 0)
4041
@positionaloptional Float64 h
42+
@positionalleftover Int64 i
4143
end
4244

4345
println("Struct Arguments Time")
@@ -54,6 +56,7 @@ let
5456
@positionaldefault Int 20 g
5557
@argtest g (x -> x % 10 == 0)
5658
@positionaloptional Float64 h
59+
@positionalleftover Int64 i
5760
end
5861

5962
argsstruct = ArgsStruct()
@@ -72,6 +75,7 @@ let
7275
@positionaldefault Int 20 g
7376
@argtest g (x -> x % 10 == 0)
7477
@positionaloptional Float64 h
78+
@positionalleftover Int64 i
7579
end
7680

7781
println("Dict Arguments Time")
@@ -87,6 +91,7 @@ let
8791
@positionaldefault Int 20 g
8892
@argtest g (x -> x % 10 == 0)
8993
@positionaloptional Float64 h
94+
@positionalleftover Int64 i
9095
end
9196

9297
@testset "Correct, All Arguments" begin
@@ -98,7 +103,8 @@ let
98103
@test e == 4
99104
@test f == "TEST STRING F"
100105
@test g == 30
101-
@test h == 3.14
106+
@test h == 3.14
107+
@test i == [4, 5, 6]
102108
end
103109

104110
@testset "Struct" begin
@@ -109,7 +115,8 @@ let
109115
@test argsstruct.e == 4
110116
@test argsstruct.f == "TEST STRING F"
111117
@test argsstruct.g == 30
112-
@test argsstruct.h == 3.14
118+
@test argsstruct.h == 3.14
119+
@test argsstruct.i == [4, 5, 6]
113120
end
114121

115122
@testset "Tuple" begin
@@ -120,7 +127,8 @@ let
120127
@test argstuple.e == 4
121128
@test argstuple.f == "TEST STRING F"
122129
@test argstuple.g == 30
123-
@test argstuple.h == 3.14
130+
@test argstuple.h == 3.14
131+
@test argstuple.i == [4, 5, 6]
124132
end
125133

126134
@testset "Dict" begin
@@ -131,7 +139,8 @@ let
131139
@test argsdict[:e] == 4
132140
@test argsdict[:f] == "TEST STRING F"
133141
@test argsdict[:g] == 30
134-
@test argsdict[:h] == 3.14
142+
@test argsdict[:h] == 3.14
143+
@test argsdict[:i] == [4, 5, 6]
135144
end
136145
end
137146
end
@@ -153,6 +162,7 @@ let
153162
@positionaldefault Int 20 g
154163
@argtest g (x -> x % 10 == 0)
155164
@positionaloptional Float64 h
165+
@positionalleftover Int64 i
156166
end
157167

158168
@structarguments false ArgsStruct begin
@@ -167,6 +177,7 @@ let
167177
@positionaldefault Int 20 g
168178
@argtest g (x -> x % 10 == 0)
169179
@positionaloptional Float64 h
180+
@positionalleftover Int64 i
170181
end
171182

172183
argsstruct = ArgsStruct()
@@ -183,6 +194,7 @@ let
183194
@positionaldefault Int 20 g
184195
@argtest g (x -> x % 10 == 0)
185196
@positionaloptional Float64 h
197+
@positionalleftover Int64 i
186198
end
187199

188200
argsdict = @dictarguments begin
@@ -197,6 +209,7 @@ let
197209
@positionaldefault Int 20 g
198210
@argtest g (x -> x % 10 == 0)
199211
@positionaloptional Float64 h
212+
@positionalleftover Int64 i
200213
end
201214

202215
@testset "Correct, Minimal Arguments" begin
@@ -209,6 +222,7 @@ let
209222
@test f == "OTHER TEST STRING F"
210223
@test g == 20
211224
@test isnothing(h)
225+
@test isempty(i)
212226
end
213227

214228
@testset "Struct" begin
@@ -220,6 +234,7 @@ let
220234
@test argsstruct.f == "OTHER TEST STRING F"
221235
@test argsstruct.g == 20
222236
@test isnothing(argsstruct.h)
237+
@test isempty(argsstruct.i)
223238
end
224239

225240
@testset "Tuple" begin
@@ -231,6 +246,7 @@ let
231246
@test argstuple.f == "OTHER TEST STRING F"
232247
@test argstuple.g == 20
233248
@test isnothing(argstuple.h)
249+
@test isempty(argstuple.i)
234250
end
235251

236252
@testset "Dict" begin
@@ -242,6 +258,7 @@ let
242258
@test argsdict[:f] == "OTHER TEST STRING F"
243259
@test argsdict[:g] == 20
244260
@test isnothing(argsdict[:h])
261+
@test isempty(argsdict[:i])
245262
end
246263
end
247264
end

0 commit comments

Comments
 (0)