Closed JacobEvelyn closed 3 years ago
Would non-flag arguments would be assigned :type
by their position in the ARGV array? How would it work?
I suppose it would be by position. As a concrete example, here's how I imagine using it:
desc "Adds two integers"
arg "NUM1", type: Integer
arg "NUM2", type: Integer
command :sum do |sum|
sum.action do |_, _, args|
puts args[0] + args[1]
end
end
Of course, a lot of the power here comes from the ability to add custom classes with accept
blocks, just like with flags:
class HTTPSURL ; end
accept(HTTPSURL) do |value|
if value[0..6] == "http://"
"https://#{value[7..-1]}"
elsif value[0..7] != "https://"
"https://#{value}"
else
value
end
end
desc "Download files over HTTPS"
arg "URL", type: HTTPSURL, multiple: true
command :download do |download|
download.action do |_, _, args|
args.each { |url| download(url) }
end
end
Does that make sense?
Sort of. What if you had other args that were of another type? You would have to specify argument position in the definition of the arg type:
arg "STR1", type: String, position: 1
arg "NUM1", type: Integer, position: 2
arg "STR2", type: String, position: 3
arg "NUM2", type: Integer, position: 4
Ah, I thought the order of arg
calls itself specified the position. (Basically I agree with @mojavelinux in https://github.com/davetron5000/gli/issues/12#issuecomment-37626324) Is there a reason that isn't the case?
Yeah, I had always wanted to expand this to be richer, but it wasn't clear how you'd do it.
Since the args are positional, there are a lot of options for how to interpret them. For example, you could have a fixed number of positional args. You could have a fixed number and optional final arg. You could have all of them optional but treated as a collective list (e.g. a file list), or some positional and some optional as a list.
To get this in an existing GLI app, you could use the pre
hook to coerce the types of args
. I don't know if args
is mutable, so your coercion might have to populate another data structure.
You can technically derive the position from the order of the args defined in Ruby, but I don't know if gli
can do this already, and I do not think that is clear in all scenarios, especially when there may be named flags interspersed with the unnamed positional args?
I think the mental model is that, regardless of where things are on the command line, args
is the ordered list of arguments to the command. OptionParser will remove all switches and flags and whatever's left are the args. So users should generally be doing stuff like:
app command --flag=foo --switch --other-flag=bar arg1 arg2 arg3
If a user wants to mix args amongst the flags and switches, they can, but that would be a strange invocation syntax.
Exactly. Since OptionParser
already figures out what the args
are regardless of their placement with flags and switches, I don't think I see the problem with that. To @davetron5000's earlier comment about the trickiness of knowing which argument is which when :optional
is used, that seems like a general problem developers must handle regardless of whether they want their arguments coerced into a given type or not.
Is the real problem here the fact that OptionParser
doesn't provide (as far as I can tell) hooks to do coercion of non-flag arguments?
Yeah, OptionParser
just leaves the unparsed args alone.
So, I think it comes down to what is the API for developers to specify how they want the args treated?
Each arg needs a way to:
SRC_URL
)We can also assume that every arg but the last one is required and that the last arg is either: totally optional, also required, can be a list of things.
And then, a way to access the parsed results. All without breaking backwards compatibility.
An explicit and simple thing might be:
args {
mime_type: { description: "Mime types to upload"}, # required since not last, type is String since that's the default
dest_url: { description: "Where to download files to", type: Dir }, # again, required since it's not last
files: { description: "Files to upload", arity: :at_least_one } # at least one is required, multiple accepted, type is Array of String
]
# also
args {
mime_type: { description: "Mime types to upload"}, # required since not last, type is String since that's the default
dest_url: { description: "Where to download files to", type: Dir }, # again, required since it's not last
file: { description: "File to upload", arity: :one } # required, type is String
]
# or
# also
args {
mime_type: { description: "Mime types to upload"}, # required since not last, type is String since that's the default
dest_url: { description: "Where to download files to", type: Dir }, # again, required since it's not last
files: { description: "Files to upload, if any", arity: :any } # not required, type is Array of String
]
Trickier is making this available. It could be that we make a fourth argument available to action
blocks:
c.action do |global,options,args,parsed_args|
end
where parsed_args
would be a hash of the values passed to arg
?
I dunno, am I overcomplicating this maybe?
That feels way overcomplicated to me. My proposal is to keep all existing functionality and just add the minimum needed for this feature. Repasting my code from above, it would look like:
desc "Adds two integers"
arg "NUM1", type: Integer
arg "NUM2", type: Integer
command :sum do |sum|
sum.action do |_, _, args|
# Note that I'm accessing the type arguments directly from the `args` array.
puts args[0] + args[1]
end
end
So the help output would look like:
NAME
sum - Adds two integers
SYNOPSIS
test.rb [global options] sum NUM1 NUM2
This project has been fine so far without needing long descriptions for arguments, or needing to specify their positions, or needing a way of accessing arguments beyond just args
array in action
blocks. I don't see why this feature needs to change any of that. Introducing a complete overhaul to how arguments are specified seems pretty unnecessary and will only complicate both this codebase and those of projects using GLI
.
(And if in the future there's a desire for long argument descriptions, that can be added separately—for instance by extending the existing syntax à la arg "NUM1", description: "an integer to add", type: Integer
)
What am I missing here?
Yeah, @JacobEvelyn you are right. That is super simple and, honestly, if we want something much more complex/featureful later, your syntax doesn't make that any more difficult.
closing this as old (almost 5 years!) re-open if you still want/need this
I've found the
:type
option very helpful for flags, and I don't see a reason why it shouldn't be usable by regular (non-flag) arguments as well. Thoughts? I'd be happy to take a stab at implementing this.