davetron5000 / gli

Make awesome command-line applications the easy way
http://davetron5000.github.io/gli
Apache License 2.0
1.26k stars 102 forks source link

Add `:type` option to arguments #241

Closed JacobEvelyn closed 3 years ago

JacobEvelyn commented 8 years ago

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.

pboling commented 8 years ago

Would non-flag arguments would be assigned :type by their position in the ARGV array? How would it work?

JacobEvelyn commented 8 years ago

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?

pboling commented 8 years ago

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
JacobEvelyn commented 8 years ago

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?

davetron5000 commented 8 years ago

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.

pboling commented 8 years ago

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?

davetron5000 commented 8 years ago

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.

JacobEvelyn commented 8 years ago

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?

davetron5000 commented 8 years ago

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:

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?

JacobEvelyn commented 8 years ago

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?

davetron5000 commented 8 years ago

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.

davetron5000 commented 3 years ago

closing this as old (almost 5 years!) re-open if you still want/need this