From 1d07e544dbf4c325f904d97094159f8935ba4fbd Mon Sep 17 00:00:00 2001 From: zachmatson <49772082+zachmatson@users.noreply.github.com> Date: Sun, 13 Sep 2020 18:11:12 -0400 Subject: [PATCH] V0.2 Changes for version 0.2.0 Testing, new documentation, new output formats, deprecate @beginarguments --- LICENSE | 2 +- Project.toml | 2 +- docs/make.jl | 2 +- docs/src/index.md | 153 ++++++++++++++--- docs/src/macros.md | 11 +- src/ArgMacros.jl | 5 +- src/constants.jl | 30 ++-- src/handleast.jl | 70 +++++--- src/help.jl | 114 +++++++------ src/macros.jl | 135 +++++++++++++-- test/Manifest.toml | 33 ++++ test/Project.toml | 2 + test/blank_script.jl | 1 + test/get_args_fail_at_exit.jl | 21 +++ test/get_args_no_fail_at_exit.jl | 17 ++ test/runtests.jl | 279 +++++++++++++++++++++++++++++++ 16 files changed, 734 insertions(+), 143 deletions(-) create mode 100644 test/Manifest.toml create mode 100644 test/Project.toml create mode 100644 test/blank_script.jl create mode 100644 test/get_args_fail_at_exit.jl create mode 100644 test/get_args_no_fail_at_exit.jl create mode 100644 test/runtests.jl diff --git a/LICENSE b/LICENSE index 92bcf46..7afd7b8 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2020 zachmatson +Copyright (c) 2020 Zachary Matson Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/Project.toml b/Project.toml index fde1200..ec8a529 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "ArgMacros" uuid = "dbc42088-9de8-42a0-8ec8-2cd114e1ea3e" authors = ["zachmatson"] -version = "0.1.3" +version = "0.2.0" [deps] TextWrap = "b718987f-49a8-5099-9789-dcd902bef87d" diff --git a/docs/make.jl b/docs/make.jl index 9099c4f..d4915c7 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -1,4 +1,4 @@ -push!(LOAD_PATH,"../src/") +prepend!(LOAD_PATH, ["../src/"]) using Documenter using ArgMacros diff --git a/docs/src/index.md b/docs/src/index.md index 9cfe65e..fc3c54f 100644 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -5,7 +5,7 @@ Reading through this before using the package is recommended. !!! note - Version 0.1.x of ArgMacros might be rough around some edge cases. + Version 0.2.x of ArgMacros might be rough around some edge cases. Make sure to test the interface you build before using it. If you notice any issues or want to request new features, create an issue on [GitHub](https://github.com/zachmatson/ArgMacros.jl) @@ -14,14 +14,13 @@ Reading through this before using the package is recommended. ArgMacros is designed for parsing arguments in command-line Julia scripts. Compilation time is the greatest bottleneck for startup of scripts in Julia, -and is mostly unavoidable. While attention is paid to making -code that compiles relatively quickly, the emphasis of ArgMacros is on quick -parsing and type stability after compilation. +and is mostly unavoidable. ArgMacros provides quick parsing after compilation while +ensuring compilation time fast too. -Variables created using ArgMacros are statically typed, -and available immediately within your main function, without manually -retrieving them from an intermediate data structure. This can also make it -more convenient when writing scripts. +ArgMacros also provides convenience when writing scripts by offering various and easily +interchangeable formats for outputting parsed arguments, a simple interface, and guaranteed +type safety of parsed arguments. Some output formats also provide static typing of the +argument variables. ## Installation @@ -29,22 +28,31 @@ Install ArgMacros using Julia's Pkg package manager. Enter the Pkg prompt by typing `]` at the REPL and then install: ```julia-repl -(@v1.4) pkg> add ArgMacros +(@v1.5) pkg> add ArgMacros ``` Then load ArgMacros into your script with `using ArgMacros`. +## Argument Format Types + +There are four formats for your program or script to receive the parsed arguments with ArgMacros, +all of which use the same interface for argument declaration: +* Inline, typed local variables ([`@inlinearguments`](@ref)) +* An automatically generated custom `struct` type ([`@structarguments`](@ref)) +* `NamedTuple` ([`@tuplearguments`](@ref)) +* `Dict` ([`@dictarguments`](@ref)) + ## Adding Arguments All arguments must be declared using the macros provided, and all of the declarations must -exist within the [`@beginarguments`](@ref) block like so: +exist within the [`@inlinearguments`](@ref) block, or other argument macro block, like so: ```julia -@beginarguments begin +@inlinearguments begin *arguments go here* end ``` -The types of arguments supported are broken down into two main categories: +The types of arguments supported are broken down into two categories: * Options (`@argument...`) - Marked with flags - [`@argumentrequired`](@ref) - [`@argumentdefault`](@ref) @@ -68,9 +76,11 @@ For this reason, ALL options must be declared before ANY positionals, required positionals must be declared before default/optional ones, and positional arguments must be declared in the order the user is expected to enter them. +You should make your argument types `Symbol`, `String`, or subtypes of `Number`. + Here is an example with some arguments: ```julia -@beginarguments begin +@inlinearguments begin @argumentrequired String foo "-f" "--foo" @argumentdefault String "lorem ipsum" bar "--bar" @argumentflag verbose "-v" @@ -94,17 +104,23 @@ of the name of local variable they will be stored in. ## Using Argument Values -Once an argument is decalred, it is statically typed and you can be sure it holds a value of the +Once an argument is decalred, you can be sure it holds a value of the correct type. [`@argumentoptional`](@ref) and [`@positionaloptional`](@ref) will use the type `Union{T, Nothing}`, however, and may also contain `nothing`. [`@argumentflag`](@ref) uses `Bool` and [`@argumentcount`](@ref) uses `Int`. -The other macros will all store the type specified. No additional code is required to begin using the -argument value after parsing. +The other macros will all store the type specified. -You should make your argument types `Symbol`, `String`, or subtypes of `Number`. +How exactly you use the values depends on the format used, the following will demonstrate the same arguments +with each of the available formats, and some of the consequences of each of them: + +### Inline ([`@inlinearguments`](@ref)) + +The arguments are stored directly in local variables, which are statically typed. You can use them immediately +without any other boilerplate, but must respect the variable types. These variables, because they are typed, must +always be in local scope. You cannot put this block in a global scope. ```julia function main() - @beginarguments begin + @inlinearguments begin @positionalrequired Int x @positionaldefault Int 5 y @positionaloptional Int z @@ -119,17 +135,98 @@ function main() end ``` +### Custom `struct` ([`@structarguments`](@ref)) + +A new `struct` type is created to store the arguments, and you can decide if it will be mutable. +The zero-argument constructor function for the new type parses the arguments when it is called. +You must *declare* the arguments in global scope due to the rules for type declarations, +but the constructor can be used anywhere. + +The fields of the struct will all be typed. + +```julia +# Declare mutable type Args and the arguments it will hold +@structarguments true Args begin + @positionalrequired Int x + @positionaldefault Int 5 y + @positionaloptional Int z +end + +function main() + args = Args() # The arguments are parsed here + + println(args.x + args.y) # Prints x + y, the variables must be Ints + println(isnothing(args.z)) # z might be nothing, because it was optional + + # These assignemnt operations would all fail if we made Args immutable instead + args.z = nothing # It is fine to store values of type Nothing or Int in z now + args.z = 8 + args.x = 5.5 # Raises an error, x must hold Int values + args.y = nothing # Raises an error, only optional arguments can hold nothing +end +``` + +### `NamedTuple` ([`@tuplearguments`](@ref)) + +A `NamedTuple` is returned containing all of the argument values, keyed by the variable names given. You can use this +version from any scope. All of the fields are typed, and as a `NamedTuple` the returned object will be immutable. + +```julia +function main() + args = @tuplearguments begin + @positionalrequired Int x + @positionaldefault Int 5 y + @positionaloptional Int z + end + + println(args.x + args.y) # Prints x + y, the variables must be Ints + println(isnothing(args.z)) # z might be nothing, because it was optional + + # These assignemnt operations will fail because NamedTuples are always immutable + args.z = nothing + args.z = 8 + + args.x == 5.5 # Can never be true, args.x is guaranteed to be an Int + isnothing(args.y) # Must be false, y is not optional +end +``` + +### `Dict` ([`@dictarguments`](@ref)) + +A `Dict{Symbol, Any}` is returned containing all of the argument variables, keyed by the argument names as *`Symbol`s*. You can use +this version from any scope. The `Dict` type is mutable, and any type can be stored in any of its fields. Therefore, this version +does not provide as strong of a guarantee about types to the compuler when argument values are used later. However, the values +are guaranteed to be of the correct types when the `Dict` is first returned. + +```julia +function main() + args = @dictarguments begin + @positionalrequired Int x + @positionaldefault Int 5 y + @positionaloptional Int z + end + + println(args[:x] + args[:y]) # Prints x + y, the variable names are available right away and must be Ints at first + println(isnothing(args[:z])) # z might be nothing, because it was optional + args[:z] = nothing # It is fine to store values of any type in z now + args[:z] = 8 + args[:x] = 5.5 # Same for x + args[:y] = nothing # And y + args[:a] = "some string" # New entries can even be added later, of any type +end +``` + ## Validating Arguments Perhaps you want to impose certain conditions on the values of an argument beyond its type. You can use the [`@argtest`](@ref) macro, which will exit the program if a specified unary predicate returns `false` for the argument value. -If using an anonymous function for this, make sure to enclose it in parentheses so it is passed to the -macro as a single expression. +If using an operator function, make sure to enclose it in parentheses so it is passed to the +macro as a separate expression from the first argument. ```julia -@beginarguments begin +@inlinearguments begin ... @positionalrequired String input "input_file" @argtest input isfile "The input must be a valid file" # Confirm that the input file really exists @@ -146,10 +243,10 @@ When using the [`@arghelp`](@ref) macro, note that it always applies to the last The [`@helpusage`](@ref) will prepend your usage text with "Usage: ", so do not include this in the string you pass. It is recommended to place [`@helpusage`](@ref), [`@helpdescription`](@ref), and [`@helpepilog`](@ref) in that order at the -beginning of the [`@beginarguments`](@ref) block, but this is not a requirement. +beginning of the `@...arguments` block, but this is not a requirement. ```julia -@beginarguments begin +@inlinearguments begin @helpusage "example.jl input_file [output_file] [-f | --foo] [--bar] [-v]" @helpdescription """ Lorem ipsum dolor sit amet, consectetur adipiscing elit. @@ -172,11 +269,11 @@ end By default, the program will exit and print a warning if more arguments are given than the program declares. If you don't want this to happen, include the [`@allowextraarguments`](@ref) macro. -This can occur anywhere inside the [`@beginarguments`](@ref) block, but the recommended placement is at the end, +This can occur anywhere inside the `@...arguments` block, but the recommended placement is at the end, after all other help, test, and argument declarations. ```julia -@beginarguments begin +@inlinearguments begin ... @allowextraarguments end @@ -185,7 +282,7 @@ end ## Taking Argument Code out of Main Function It may be preferable, in some cases, not to declare all of your arguments and help information -inside of your main function. In this case, the [`@beginarguments`](@ref) block can be enclosed +inside of your main function. In this case, the [`@inlinearguments`](@ref) block can be enclosed in a macro: ```julia @@ -203,3 +300,7 @@ function main() # The argument values will be available here end ``` + +The [`@structarguments`](@ref) must be used in a global scope, but its constructor can then be used anywhere. +The other forms which directly return an object can be placed into an external function because they don't rely +on being in the same namespace as the point where the arguments are used, as [`@inlinearguments`](@ref) does. diff --git a/docs/src/macros.md b/docs/src/macros.md index 8f1ed12..b88f010 100644 --- a/docs/src/macros.md +++ b/docs/src/macros.md @@ -1,11 +1,14 @@ # Available Macros -## `@beginarguments` +## `@...arguments` -The `@beginarguments begin ... end` block will hold all of your `ArgMacros` code. -You shouldn't have to worry about this macro as long as you use it to enclose your other code. +The `@...arguments begin ... end` block will hold all of your `ArgMacros` code. +The [Using Argument Values](@ref) section provides a good comparison of the different available macros. ```@docs -@beginarguments +@inlinearguments +@structarguments +@tuplearguments +@dictarguments ``` ## Option Arguments diff --git a/src/ArgMacros.jl b/src/ArgMacros.jl index 62b2455..ee48c94 100644 --- a/src/ArgMacros.jl +++ b/src/ArgMacros.jl @@ -8,7 +8,8 @@ end using TextWrap using Base: @kwdef -export @beginarguments +export @beginarguments, @inlinearguments, @structarguments, + @tuplearguments, @dictarguments export @helpusage, @helpdescription, @helpepilog export @argumentrequired, @argumentdefault, @argumentoptional, @argumentflag, @argumentcount @@ -35,7 +36,7 @@ results in typed local variables. Basic usage: ```julia julia_main() - @beginarguments begin + @inlinearguments begin @argumentrequired Int foo "-f" "--foo" @argumentdefault Int 5 bar "-b" "--bar" ... diff --git a/src/constants.jl b/src/constants.jl index 4bd51d2..82d315e 100644 --- a/src/constants.jl +++ b/src/constants.jl @@ -1,20 +1,20 @@ # ArgMacros # constants.jl -const usage_symbol = Symbol("@helpusage") -const description_symbol = Symbol("@helpdescription") -const epilog_symbol = Symbol("@helpepilog") -const arghelp_symbol = Symbol("@arghelp") +const USAGE_SYMBOL = Symbol("@helpusage") +const DESCRIPTION_SYMBOL = Symbol("@helpdescription") +const EPILOG_SYMBOL = Symbol("@helpepilog") +const ARGHELP_SYMBOL = Symbol("@arghelp") -const argument_required_symbol = Symbol("@argumentrequired") -const argument_default_symbol = Symbol("@argumentdefault") -const argument_optional_symbol = Symbol("@argumentoptional") -const argument_flag_symbol = Symbol("@argumentflag") -const argument_count_symbol = Symbol("@argumentcount") -const flagged_symbols = [argument_required_symbol, argument_default_symbol, argument_optional_symbol, - argument_flag_symbol, argument_count_symbol] +const ARGUMENT_REQUIRED_SYMBOL = Symbol("@argumentrequired") +const ARGUMENT_DEFAULT_SYMBOL = Symbol("@argumentdefault") +const ARGUMENT_OPTIONAL_SYMBOL = Symbol("@argumentoptional") +const ARGUMENT_FLAG_SYMBOL = Symbol("@argumentflag") +const ARGUMENT_COUNT_SYMBOL = Symbol("@argumentcount") +const FLAGGED_SYMBOLS = [ARGUMENT_REQUIRED_SYMBOL, ARGUMENT_DEFAULT_SYMBOL, ARGUMENT_OPTIONAL_SYMBOL, + ARGUMENT_FLAG_SYMBOL, ARGUMENT_COUNT_SYMBOL] -const positional_required_symbol = Symbol("@positionalrequired") -const positional_default_symbol = Symbol("@positionaldefault") -const positional_optional_symbol = Symbol("@positionaloptional") -const positional_optional_symbols = [positional_default_symbol, positional_optional_symbol] +const POSITIONAL_REQUIRED_SYMBOL = Symbol("@positionalrequired") +const POSITIONAL_DEFAULT_SYMBOL = Symbol("@positionaldefault") +const POSITIONAL_OPTIONAL_SYMBOL = Symbol("@positionaloptional") +const POSITIONAL_OPTIONAL_SYMBOLS = [POSITIONAL_DEFAULT_SYMBOL, POSITIONAL_OPTIONAL_SYMBOL] diff --git a/src/handleast.jl b/src/handleast.jl index bd562b4..8921987 100644 --- a/src/handleast.jl +++ b/src/handleast.jl @@ -8,6 +8,11 @@ function _get_macroname(arg::Expr)::Symbol arg.args[1] end +"Get the macrocall expressions from a block as a generator" +function _getmacrocalls(block::Expr) + Iterators.filter(arg -> arg isa Expr && arg.head == :macrocall, block.args) +end + """ Enforce argument declaration ordering: Flagged → Required Positional → Optional Positional @@ -18,30 +23,49 @@ function _validateorder(block::Expr) encountered_positional = false encoundered_optional_positional = false - for arg in block.args - # Only check the macro calls - if arg isa Expr && arg.head == :macrocall - # Fix namespace issues - macroname::Symbol = _get_macroname(arg) - - if macroname in flagged_symbols - if encountered_positional - throw(ArgumentError( - "Positional arguments must be declared after all flagged arguments.\nFrom: $arg" - )) - end - elseif macroname == positional_required_symbol - encountered_positional = true - - if encoundered_optional_positional - throw(ArgumentError( - "Required positional arguments must be declared in order before all optional positional arguments.\nFrom: $arg" - )) - end - elseif macroname in positional_optional_symbols - encountered_positional = true - encoundered_optional_positional = true + for arg in _getmacrocalls(block) + # Fix namespace issues + macroname::Symbol = _get_macroname(arg) + + if macroname in FLAGGED_SYMBOLS + if encountered_positional + throw(ArgumentError( + "Positional arguments must be declared after all flagged arguments.\nFrom: $arg" + )) end + elseif macroname == POSITIONAL_REQUIRED_SYMBOL + encountered_positional = true + + if encoundered_optional_positional + throw(ArgumentError( + "Required positional arguments must be declared in order before all optional positional arguments.\nFrom: $arg" + )) + end + elseif macroname in POSITIONAL_OPTIONAL_SYMBOLS + encountered_positional = true + encoundered_optional_positional = true end end end + +"Extract name => type pair from argument macrocall expression" +function _getargumentpair(arg::Expr)::Union{Expr, Nothing} + macroname::Symbol = _get_macroname(arg) + + if macroname in (ARGUMENT_REQUIRED_SYMBOL, POSITIONAL_REQUIRED_SYMBOL) + :($(arg.args[4])::$(arg.args[3])) + elseif macroname in (ARGUMENT_DEFAULT_SYMBOL, POSITIONAL_DEFAULT_SYMBOL) + :($(arg.args[5])::$(arg.args[3])) + elseif macroname in (ARGUMENT_OPTIONAL_SYMBOL, POSITIONAL_OPTIONAL_SYMBOL) + :($(arg.args[4])::Union{$(arg.args[3]), Nothing}) + elseif macroname == ARGUMENT_FLAG_SYMBOL + :($(arg.args[3])::Bool) + elseif macroname == ARGUMENT_COUNT_SYMBOL + :($(arg.args[3])::Int) + end +end + +"Extract name => type pairs for all argument macros in block" +function _getargumentpairs(block::Expr) + Iterators.filter(!isnothing, _getargumentpair(arg) for arg in _getmacrocalls(block)) +end diff --git a/src/help.jl b/src/help.jl index 980530e..7da6ab4 100644 --- a/src/help.jl +++ b/src/help.jl @@ -88,7 +88,7 @@ macro arghelp(helptext::String) end function _quit_try_help(message::String) println(message) println("Try the --help option") - exit() + exit(1) end #= @@ -99,7 +99,7 @@ Help is generated on-demand, may want to change this for precompiled scripts function _help_check(args::Vector{String}, block::Expr) if !isnothing(_get_option_idx(args, ["-h", "--help"])) _make_help(block) |> print - exit() + exit(0) end end @@ -129,62 +129,60 @@ function _make_help(block::Expr)::Help lastpushed::Symbol = :none # Loop through all macros in the block - for arg in block.args - if arg isa Expr && arg.head == :macrocall - macroname::Symbol = _get_macroname(arg) - - # Modify the help struct as needed by the encountered macro - if macroname == usage_symbol - help.usage_text = arg.args[3] # 3rd arg for these is the string passed - elseif macroname == description_symbol - help.description_text = arg.args[3] - elseif macroname == epilog_symbol - help.epilog_text = arg.args[3] - elseif macroname == arghelp_symbol - # Add description to last pushed element in positionals or options - argvector::Vector{Argument} = getfield(help, lastpushed) - argvector[end].description = arg.args[3] - elseif macroname == argument_required_symbol - push!(help.options, Argument( - arg.args[5:end], arg.args[3], true, false, "", "") - ) - lastpushed = :options - elseif macroname == argument_default_symbol - push!(help.options, Argument( - arg.args[6:end], arg.args[3], false, false, string(arg.args[4]), "") - ) - lastpushed = :options - elseif macroname == argument_optional_symbol - push!(help.options, Argument( - arg.args[5:end], arg.args[3], false, false, "", "") - ) - lastpushed = :options - elseif macroname == argument_flag_symbol - push!(help.options, Argument( - arg.args[4:end], :Flag, false, false, "", "") - ) - lastpushed = :options - elseif macroname == argument_count_symbol - push!(help.options, Argument( - arg.args[4:end], :Count, false, false, "", "") - ) - lastpushed = :options - elseif macroname == positional_required_symbol - push!(help.positionals, Argument( - [arg.args[end]], arg.args[3], true, true, "", "") - ) - lastpushed = :positionals - elseif macroname == positional_default_symbol - push!(help.positionals, Argument( - [arg.args[end]], arg.args[3], false, true, string(arg.args[4]), "") - ) - lastpushed = :positionals - elseif macroname == positional_optional_symbol - push!(help.positionals, Argument( - [arg.args[end]], arg.args[3], false, true, "", "") - ) - lastpushed = :positionals - end + for arg in _getmacrocalls(block) + macroname::Symbol = _get_macroname(arg) + + # Modify the help struct as needed by the encountered macro + if macroname == USAGE_SYMBOL + help.usage_text = arg.args[3] # 3rd arg for these is the string passed + elseif macroname == DESCRIPTION_SYMBOL + help.description_text = arg.args[3] + elseif macroname == EPILOG_SYMBOL + help.epilog_text = arg.args[3] + elseif macroname == ARGHELP_SYMBOL + # Add description to last pushed element in positionals or options + argvector::Vector{Argument} = getfield(help, lastpushed) + argvector[end].description = arg.args[3] + elseif macroname == ARGUMENT_REQUIRED_SYMBOL + push!(help.options, Argument( + arg.args[5:end], arg.args[3], true, false, "", "") + ) + lastpushed = :options + elseif macroname == ARGUMENT_DEFAULT_SYMBOL + push!(help.options, Argument( + arg.args[6:end], arg.args[3], false, false, string(arg.args[4]), "") + ) + lastpushed = :options + elseif macroname == ARGUMENT_OPTIONAL_SYMBOL + push!(help.options, Argument( + arg.args[5:end], arg.args[3], false, false, "", "") + ) + lastpushed = :options + elseif macroname == ARGUMENT_FLAG_SYMBOL + push!(help.options, Argument( + arg.args[4:end], :Flag, false, false, "", "") + ) + lastpushed = :options + elseif macroname == ARGUMENT_COUNT_SYMBOL + push!(help.options, Argument( + arg.args[4:end], :Count, false, false, "", "") + ) + lastpushed = :options + elseif macroname == POSITIONAL_REQUIRED_SYMBOL + push!(help.positionals, Argument( + [arg.args[end]], arg.args[3], true, true, "", "") + ) + lastpushed = :positionals + elseif macroname == POSITIONAL_DEFAULT_SYMBOL + push!(help.positionals, Argument( + [arg.args[end]], arg.args[3], false, true, string(arg.args[4]), "") + ) + lastpushed = :positionals + elseif macroname == POSITIONAL_OPTIONAL_SYMBOL + push!(help.positionals, Argument( + [arg.args[end]], arg.args[3], false, true, "", "") + ) + lastpushed = :positionals end end diff --git a/src/macros.jl b/src/macros.jl index e038a0b..86a9ce9 100644 --- a/src/macros.jl +++ b/src/macros.jl @@ -8,14 +8,14 @@ function _validateflags(local_name, flags) end """ - @beginarguments begin ... end + @inlinearguments begin ... end Denote and setup a block with other macros from `ArgMacros` # Example ```julia -julia_main() - @beginarguments begin +function julia_main() + @inlinearguments begin ... @argumentrequired Int foo "-f" "--foo" @argumentdefault Int 5 bar "-b" "--bar" @@ -25,7 +25,7 @@ julia_main() end ``` """ -macro beginarguments(block::Expr) +macro inlinearguments(block::Expr) _validateorder(block) # Validate ordering of macros return quote @@ -42,6 +42,117 @@ macro beginarguments(block::Expr) end end +""" + @beginarguments begin ... end + +This macro is deprecated beginning in ArgMacros v0.2.0 +Please use @inlinearguments, which has the same interface +Or consider @structarguments, @tuplearguments, and @dictarguments depending on your use case +""" +macro beginarguments(block::Expr) + @warn """ + Macro `@beginarguments` is deprecated in ArgMacros v0.2.0 + Please use @inlinearguments, which has the same interface + Or consider @structarguments, @tuplearguments, and @dictarguments depending on your use case + """ + esc(:(@inlinearguments $block)) +end + +""" + @structarguments mutable typename begin ... end + +Denote and setup a block with other macros from `ArgMacros` +Defines an optionally mutable struct type based on the arguments and a zero-argument constructor +which will generate an instance of the struct based on the parsed arguments. + +# Example +```julia +function handleargs() + @structarguments false Args begin + ... + @argumentrequired Int foo "-f" "--foo" + @argumentdefault Int 5 bar "-b" "--bar" + ... + end + ... +end +``` +""" +macro structarguments(mutable::Bool, name::Symbol, block::Expr) + Expr(:block, + Expr(:struct, mutable, name, Expr(:block, _getargumentpairs(block)...)), + esc(Expr(:function, + :($name()::$name), + Expr(:block, + :(@inlinearguments $block), + Expr(:call, + name, + (:($(pair.args[1])) for pair in _getargumentpairs(block))... + ) + ) + )) + ) +end + +""" + @tuplearguments begin ... end + +Denote and setup a block with other macros from `ArgMacros` +Return a NamedTuple with the arguments instead of dumping them in the enclosing namespace + + +# Example +```julia +function julia_main() + args = @tuplearguments begin + ... + @argumentrequired Int foo "-f" "--foo" + @argumentdefault Int 5 bar "-b" "--bar" + ... + end + ... +end +``` +""" +macro tuplearguments(block::Expr) + Expr(:let, Expr(:block), esc(Expr(:block, + :(@inlinearguments $block), + Expr(:tuple, + (:($(pair.args[1]) = $(pair.args[1])) for pair in _getargumentpairs(block))... + ) + ))) +end + +""" + @dictarguments begin ... end + +Denote and setup a block with other macros from `ArgMacros` +Return a Dict with the arguments instead of dumping them in the enclosing namespace + + +# Example +```julia +function julia_main() + args = @dictarguments begin + ... + @argumentrequired Int foo "-f" "--foo" + @argumentdefault Int 5 bar "-b" "--bar" + ... + end + ... +end +``` +""" +macro dictarguments(block::Expr) + Expr(:let, Expr(:block), esc(Expr(:block, + :(@inlinearguments $block), + Expr(:call, + :(Dict{Symbol, Any}), + (:($(Meta.quot(pair.args[1])) => $(pair.args[1])) for pair in _getargumentpairs(block))... + ) + ))) +end + #= Remaining macros require two escapes to get to the level of the calling scope Because they will be nested in the @beginarguments macro @@ -71,7 +182,7 @@ macro argumentrequired(type::Symbol, local_name::Symbol, flags::String...) _validateflags(local_name, flags) return quote # _converttype! is completely type safe - $(esc(esc(local_name)))::$(esc(esc(type))) = _converttype!($type, _pop_argval!(splitargs, [$flags...]), $(flags[end])) + local $(esc(esc(local_name)))::$(esc(esc(type))) = _converttype!($type, _pop_argval!(splitargs, [$flags...]), $(flags[end])) end end @@ -99,7 +210,7 @@ macro argumentdefault(type::Symbol, default_value, local_name::Symbol, flags::St return quote potential_val::Union{String, Nothing} = _pop_argval!(splitargs, [$flags...]) # Convert either potential or the default value, allows default to be specified with wrong literal type - $(esc(esc(local_name)))::$(esc(esc(type))) = _converttype!($type, something(potential_val, $default_value), $(flags[end])) + local $(esc(esc(local_name)))::$(esc(esc(type))) = _converttype!($type, something(potential_val, $default_value), $(flags[end])) end end @@ -126,7 +237,7 @@ macro argumentoptional(type::Symbol, local_name::Symbol, flags::String...) return quote potential_val::Union{String, Nothing} = _pop_argval!(splitargs, [$flags...]) # Return nothing directly without calling _converttype! if arg not found - $(esc(esc(local_name)))::$(esc(esc(Union))){$(esc(esc(type))), $(esc(esc(Nothing)))} = + local $(esc(esc(local_name)))::$(esc(esc(Union))){$(esc(esc(type))), $(esc(esc(Nothing)))} = isnothing(potential_val) ? nothing : _converttype!($type, potential_val, $(flags[end])) end end @@ -151,7 +262,7 @@ end macro argumentflag(local_name::Symbol, flags::String...) _validateflags(local_name, flags) return quote - $(esc(esc(local_name)))::$(esc(esc(Bool))) = _pop_flag!(splitargs, [$flags...]) + local $(esc(esc(local_name)))::$(esc(esc(Bool))) = _pop_flag!(splitargs, [$flags...]) end end @@ -173,7 +284,7 @@ end """ macro argumentcount(local_name::Symbol, flag::String) return quote - $(esc(esc(local_name)))::$(esc(esc(Int))) = _pop_count!(splitargs, $flag) + local $(esc(esc(local_name)))::$(esc(esc(Int))) = _pop_count!(splitargs, $flag) end end @@ -201,7 +312,7 @@ end macro positionalrequired(type::Symbol, local_name::Symbol, help_name::Union{String, Nothing}=nothing) help_name_str::String = something(help_name, String(local_name)) return quote - $(esc(esc(local_name)))::$(esc(esc(type))) = _converttype!( + local $(esc(esc(local_name)))::$(esc(esc(type))) = _converttype!( $type, !isempty(splitargs) ? popfirst!(splitargs) : nothing, $help_name_str @@ -234,7 +345,7 @@ end macro positionaldefault(type::Symbol, default_value, local_name::Symbol, help_name::Union{String, Nothing}=nothing) help_name_str::String = something(help_name, String(local_name)) return quote - $(esc(esc(local_name)))::$(esc(esc(type))) =_converttype!( + local $(esc(esc(local_name)))::$(esc(esc(type))) =_converttype!( $type, !isempty(splitargs) ? popfirst!(splitargs) : $default_value, $help_name_str @@ -265,7 +376,7 @@ end macro positionaloptional(type::Symbol, local_name::Symbol, help_name::Union{String, Nothing}=nothing) help_name_str::String = something(help_name, String(local_name)) return quote - $(esc(esc(local_name)))::$(esc(esc(Union))){$(esc(esc(type))), $(esc(esc(Nothing)))} = + local $(esc(esc(local_name)))::$(esc(esc(Union))){$(esc(esc(type))), $(esc(esc(Nothing)))} = !isempty(splitargs) ? _converttype!($type, popfirst!(splitargs), $help_name_str) : nothing end end diff --git a/test/Manifest.toml b/test/Manifest.toml new file mode 100644 index 0000000..ac58105 --- /dev/null +++ b/test/Manifest.toml @@ -0,0 +1,33 @@ +# This file is machine-generated - editing it directly is not advised + +[[Base64]] +uuid = "2a0f44e3-6c83-55bd-87e4-b1978d98bd5f" + +[[Distributed]] +deps = ["Random", "Serialization", "Sockets"] +uuid = "8ba89e20-285c-5b6f-9357-94700520ee1b" + +[[InteractiveUtils]] +deps = ["Markdown"] +uuid = "b77e0a4c-d291-57a0-90e8-8db25a27a240" + +[[Logging]] +uuid = "56ddb016-857b-54e1-b83d-db4d58db5568" + +[[Markdown]] +deps = ["Base64"] +uuid = "d6f4376e-aef5-505a-96c1-9c027394607a" + +[[Random]] +deps = ["Serialization"] +uuid = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" + +[[Serialization]] +uuid = "9e88b42a-f829-5b0c-bbe9-9e923198166b" + +[[Sockets]] +uuid = "6462fe0b-24de-5631-8697-dd941f90decc" + +[[Test]] +deps = ["Distributed", "InteractiveUtils", "Logging", "Random"] +uuid = "8dfed614-e22c-5e08-85e1-65c5234f0b40" diff --git a/test/Project.toml b/test/Project.toml new file mode 100644 index 0000000..0c36332 --- /dev/null +++ b/test/Project.toml @@ -0,0 +1,2 @@ +[deps] +Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" diff --git a/test/blank_script.jl b/test/blank_script.jl new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/test/blank_script.jl @@ -0,0 +1 @@ + diff --git a/test/get_args_fail_at_exit.jl b/test/get_args_fail_at_exit.jl new file mode 100644 index 0000000..0d7868b --- /dev/null +++ b/test/get_args_fail_at_exit.jl @@ -0,0 +1,21 @@ +using ArgMacros + +let + @inlinearguments begin + @argumentrequired Int a "-a" "--aa" + @argtest a (<(10)) + @argumentdefault Float64 10 b "-b" + @argumentoptional Symbol c "--cc" + @argumentflag d "-d" + @argumentcount e "-e" + + @positionalrequired String f + @positionaldefault Int 20 g + @argtest g (x -> x % 10 == 0) + @positionaloptional Float64 h + end +end + +# Intentionally throw non-zero exit +# So we can tell if a zero exit occurred before this +exit(1) \ No newline at end of file diff --git a/test/get_args_no_fail_at_exit.jl b/test/get_args_no_fail_at_exit.jl new file mode 100644 index 0000000..9be1d3e --- /dev/null +++ b/test/get_args_no_fail_at_exit.jl @@ -0,0 +1,17 @@ +using ArgMacros + +let + @inlinearguments begin + @argumentrequired Int a "-a" "--aa" + @argtest a (<(10)) + @argumentdefault Float64 10 b "-b" + @argumentoptional Symbol c "--cc" + @argumentflag d "-d" + @argumentcount e "-e" + + @positionalrequired String f + @positionaldefault Int 20 g + @argtest g (x -> x % 10 == 0) + @positionaloptional Float64 h + end +end diff --git a/test/runtests.jl b/test/runtests.jl new file mode 100644 index 0000000..ddc3930 --- /dev/null +++ b/test/runtests.jl @@ -0,0 +1,279 @@ +println("Times for 8 argument example using all macros") +println("Load Time (using ArgMacros)") +@time using ArgMacros +using Test + +# Commands Set 1 +let + empty!(ARGS) + append!(ARGS, ["TEST STRING F", "-deeee", "30", "3.14", "-b=6.28", "--cc", "ArgMacros", "-a", "2"]) + + println("Inline Arguments Time with Precompile") + let + @time @inlinearguments begin + @argumentrequired Int a "-a" "--aa" + @argtest a (<(10)) + @argumentdefault Float64 10 b "-b" + @argumentoptional Symbol c "--cc" + @argumentflag d "-d" + @argumentcount e "-e" + + @positionalrequired String f + @positionaldefault Int 20 g + @argtest g (x -> x % 10 == 0) + @positionaloptional Float64 h + end + end + + println("Inline Arguments Time") + @time @inlinearguments begin + @argumentrequired Int a "-a" "--aa" + @argtest a (<(10)) + @argumentdefault Float64 10 b "-b" + @argumentoptional Symbol c "--cc" + @argumentflag d "-d" + @argumentcount e "-e" + + @positionalrequired String f + @positionaldefault Int 20 g + @argtest g (x -> x % 10 == 0) + @positionaloptional Float64 h + end + + println("Struct Arguments Time") + @time begin + @structarguments false ArgsStruct begin + @argumentrequired Int a "-a" "--aa" + @argtest a (<(10)) + @argumentdefault Float64 10 b "-b" + @argumentoptional Symbol c "--cc" + @argumentflag d "-d" + @argumentcount e "-e" + + @positionalrequired String f + @positionaldefault Int 20 g + @argtest g (x -> x % 10 == 0) + @positionaloptional Float64 h + end + + argsstruct = ArgsStruct() + end + + println("Tuple Arguments Time") + @time argstuple = @tuplearguments begin + @argumentrequired Int a "-a" "--aa" + @argtest a (<(10)) + @argumentdefault Float64 10 b "-b" + @argumentoptional Symbol c "--cc" + @argumentflag d "-d" + @argumentcount e "-e" + + @positionalrequired String f + @positionaldefault Int 20 g + @argtest g (x -> x % 10 == 0) + @positionaloptional Float64 h + end + + println("Dict Arguments Time") + @time argsdict = @dictarguments begin + @argumentrequired Int a "-a" "--aa" + @argtest a (<(10)) + @argumentdefault Float64 10 b "-b" + @argumentoptional Symbol c "--cc" + @argumentflag d "-d" + @argumentcount e "-e" + + @positionalrequired String f + @positionaldefault Int 20 g + @argtest g (x -> x % 10 == 0) + @positionaloptional Float64 h + end + + @testset "Correct, All Arguments" begin + @testset "Inline" begin + @test a == 2 + @test b == 6.28 + @test c == :ArgMacros + @test d + @test e == 4 + @test f == "TEST STRING F" + @test g == 30 + @test h == 3.14 + end + + @testset "Struct" begin + @test argsstruct.a == 2 + @test argsstruct.b == 6.28 + @test argsstruct.c == :ArgMacros + @test argsstruct.d + @test argsstruct.e == 4 + @test argsstruct.f == "TEST STRING F" + @test argsstruct.g == 30 + @test argsstruct.h == 3.14 + end + + @testset "Tuple" begin + @test argstuple.a == 2 + @test argstuple.b == 6.28 + @test argstuple.c == :ArgMacros + @test argstuple.d + @test argstuple.e == 4 + @test argstuple.f == "TEST STRING F" + @test argstuple.g == 30 + @test argstuple.h == 3.14 + end + + @testset "Dict" begin + @test argsdict[:a] == 2 + @test argsdict[:b] == 6.28 + @test argsdict[:c] == :ArgMacros + @test argsdict[:d] + @test argsdict[:e] == 4 + @test argsdict[:f] == "TEST STRING F" + @test argsdict[:g] == 30 + @test argsdict[:h] == 3.14 + end + end +end + +# Commands Set 2 +let + empty!(ARGS) + append!(ARGS, ["OTHER TEST STRING F", "--aa=5"]) + + @inlinearguments begin + @argumentrequired Int a "-a" "--aa" + @argtest a (<(10)) + @argumentdefault Float64 10 b "-b" + @argumentoptional Symbol c "--cc" + @argumentflag d "-d" + @argumentcount e "-e" + + @positionalrequired String f + @positionaldefault Int 20 g + @argtest g (x -> x % 10 == 0) + @positionaloptional Float64 h + end + + @structarguments false ArgsStruct begin + @argumentrequired Int a "-a" "--aa" + @argtest a (<(10)) + @argumentdefault Float64 10 b "-b" + @argumentoptional Symbol c "--cc" + @argumentflag d "-d" + @argumentcount e "-e" + + @positionalrequired String f + @positionaldefault Int 20 g + @argtest g (x -> x % 10 == 0) + @positionaloptional Float64 h + end + + argsstruct = ArgsStruct() + + argstuple = @tuplearguments begin + @argumentrequired Int a "-a" "--aa" + @argtest a (<(10)) + @argumentdefault Float64 10 b "-b" + @argumentoptional Symbol c "--cc" + @argumentflag d "-d" + @argumentcount e "-e" + + @positionalrequired String f + @positionaldefault Int 20 g + @argtest g (x -> x % 10 == 0) + @positionaloptional Float64 h + end + + argsdict = @dictarguments begin + @argumentrequired Int a "-a" "--aa" + @argtest a (<(10)) + @argumentdefault Float64 10 b "-b" + @argumentoptional Symbol c "--cc" + @argumentflag d "-d" + @argumentcount e "-e" + + @positionalrequired String f + @positionaldefault Int 20 g + @argtest g (x -> x % 10 == 0) + @positionaloptional Float64 h + end + + @testset "Correct, Minimal Arguments" begin + @testset "Inline" begin + @test a == 5 + @test b == 10.0 + @test isnothing(c) + @test !d + @test e == 0 + @test f == "OTHER TEST STRING F" + @test g == 20 + @test isnothing(h) + end + + @testset "Struct" begin + @test argsstruct.a == 5 + @test argsstruct.b == 10.0 + @test isnothing(argsstruct.c) + @test !argsstruct.d + @test argsstruct.e == 0 + @test argsstruct.f == "OTHER TEST STRING F" + @test argsstruct.g == 20 + @test isnothing(argsstruct.h) + end + + @testset "Tuple" begin + @test argstuple.a == 5 + @test argstuple.b == 10.0 + @test isnothing(argstuple.c) + @test !argstuple.d + @test argstuple.e == 0 + @test argstuple.f == "OTHER TEST STRING F" + @test argstuple.g == 20 + @test isnothing(argstuple.h) + end + + @testset "Dict" begin + @test argsdict[:a] == 5 + @test argsdict[:b] == 10.0 + @test isnothing(argsdict[:c]) + @test !argsdict[:d] + @test argsdict[:e] == 0 + @test argsdict[:f] == "OTHER TEST STRING F" + @test argsdict[:g] == 20 + @test isnothing(argsdict[:h]) + end + end +end + +# Should Create non-zero exit codes +@testset "Incorrect Arguments Rejected" begin + # Make sure the test script is set up and working with correct args + @test success(`julia --project=.. get_args_no_fail_at_exit.jl -- "TEST STRING F" -deeee 30 3.14 -b=6.28 --cc ArgMacros -a 2`) + # These should fail + @test !success(`julia --project=.. get_args_no_fail_at_exit.jl -- 30 3.14 -d -eeee -b 6.28 --cc ArgMacros -a 2`) + @test !success(`julia --project=.. get_args_no_fail_at_exit.jl -- TEST STRING F 30 3.14 -d -eeee -b 6.28 --cc ArgMacros -a 2`) + @test !success(`julia --project=.. get_args_no_fail_at_exit.jl -- "TEST STRING F" 23.3 3.14 -d -eeee -b 6.28 --cc ArgMacros -a 2`) + @test !success(`julia --project=.. get_args_no_fail_at_exit.jl -- "TEST STRING F" -eeee 25 3.14 -d -b 6.28 --cc ArgMacros -a 2`) + @test !success(`julia --project=.. get_args_no_fail_at_exit.jl -- "TEST STRING F" 30 cat -deeee -b=6.28 --cc ArgMacros -a 2`) + @test !success(`julia --project=.. get_args_no_fail_at_exit.jl -- "TEST STRING F" 30 3.14 -deeee -b=dog --cc ArgMacros -a 2`) + @test !success(`julia --project=.. get_args_no_fail_at_exit.jl -- "TEST STRING F" 30 3.14 -deeee -b=6.28 --cc ArgMacros -a bird`) + @test !success(`julia --project=.. get_args_no_fail_at_exit.jl -- "TEST STRING F" 30 3.14 -deeee -b=6.28 --cc ArgMacros`) + +end + +@testset "Help Triggered Properly" begin + # Make sure the test script is set up and working with correct args + @test success(`julia --project=.. get_args_no_fail_at_exit.jl -- "TEST STRING F" -deeee 30 3.14 -b=6.28 --cc ArgMacros -a 2`) + @test !success(`julia --project=.. get_args_fail_at_exit.jl -- "TEST STRING F" -deeee 30 3.14 -b=6.28 --cc ArgMacros -a 2`) + # These will fail on purpose if not exited early with code 0 + # We want help to trigger, so they should successfully exit early and not fail + @test success(`julia --project=.. get_args_fail_at_exit.jl -- "TEST STRING F" 30 3.14 --help -deeee -b=6.28 --cc ArgMacros -a 2`) + @test success(`julia --project=.. get_args_fail_at_exit.jl -- "TEST STRING F" 30 -h 3.14 -deeee -b=dog --cc ArgMacros -a 2`) + @test success(`julia --project=.. get_args_fail_at_exit.jl -- --help`) +end + +println("Time for 8 argument script cold launch and parsing including Julia start") +@time run(`julia --project=.. get_args_no_fail_at_exit.jl -- "TEST STRING F" -deeee 30 3.14 -b=6.28 --cc ArgMacros -a 2`) +println("Time for Julia start only") +@time run(`julia --project=.. blank_script.jl`)