dnaeon / clingon

Command-line options parser system for Common Lisp
Other
122 stars 7 forks source link

"non-consuming" options? That is options with optional values of specific types which shouldn't signal errors but rather not consume the argument. #10

Closed simendsjo closed 1 year ago

simendsjo commented 1 year ago

Hi, I'm trying to use clingon to create an interface compatible with another tool, but the tool is unfortunately a bit strange, and I'm having some problems getting it right.

The problem is the clip option in the following interface.

cmd [--clip[=line-number],-c[line-number]] pass-name

The following is allowed and means different things:

cmd foo
cmd -c foo
cmd -c1 foo

I've implemented a maybe-int option type, but I'm having problems with the second case which gobbles up the foo parameter. Is there a way to say that the parameter shouldn't be consumed or be put back?

(cl:defmethod clingon:derive-option-value ((option option-maybe-int) arg cl:&key)
  (cl:if (cl:integerp arg)
         arg
         ;; Here I don't use the argument, so can I put `arg` back?
         0))

EDIT: I think I want something like (push arg (command-args-to-parse cmd)), but I don't have the command available when handling the option value.

dnaeon commented 1 year ago

Hey @simendsjo ,

What is the value associated with the -c|--clip option, when you only invoke these?

cmd foo
cmd -c foo
cmd -c1 foo

I assume there's a default value associated with the -c|--clip option, is that correct? If that's the case, can you simply use a regular int option with a default value?

simendsjo commented 1 year ago

I assume there's a default value associated with the -c|--clip option, is that correct? If that's the case, can you simply use a regular int option with a default value?

The problem is that I want missing, present and value-set, but an int option can only give me missing (default) and value set, but not "present but without value". I need to know that the option was missing as that should show all lines to stdout. I don't care what value is stored in clip as long as it's not a positive integer in this case.

clip result
nil
-c 1
-c2 2
dnaeon commented 1 year ago

Hey @simendsjo ,

Maybe I'm not able to understand the use case, but wouldn't something like this work?

(defun clip/options ()
  (list
   (clingon:make-option :integer
                        :short-name #\c
                        :long-name "clip"
                        :description "the --clip option"
                        :key :clip/option)))

(defun clip/handler (cmd)
  (let ((opt-value (clingon:getopt cmd :clip/option 1))
        (opt-is-set (clingon:opt-is-set-p cmd :clip/option))
        (args (clingon:command-arguments cmd)))
    (format t "--clip option is set: ~A~%" opt-is-set)
    (format t "--clip value is: ~A~%" opt-value)
    (format t "free args: ~A~%" args)))

(defun clip/command ()
  (clingon:make-command
   :name "clip"
   :description "clip command"
   :handler #'clip/handler
   :options (clip/options)))

Option is not specified on command-line, and it's value is 1.

$ cmd foo
--clip option is set: NIL
--clip value is: 1
free args: (foo)

Option is specified on the command-line.

$ cmd -c 42 foo
--clip option is set: T
--clip value is: 42
free args: (foo)

Supporting a command-line like this in my opinion is very ambiguous, where -c option might receive a value, or not.

$ cmd -c foo

In this case it's not clear whether -c is a flag (option with no arguments), or whether foo is the value associated with -c option.

simendsjo commented 1 year ago

Maybe I'm not able to understand the use case, but wouldn't something like this work?

No, unfortunately, because cmd -c foo will consume foo even though it's not an integer.

Supporting a command-line like this in my opinion is very ambiguous, where -c option might receive a value, or not.

Yes, I totally agree, but I'm trying to be backwards compatible with an existing API so tools also works with my implementation.

I guess I can solve this by fetching uiop:command-line-arguments myself, look for -c not-an-integer and change it to -c1 not-an-integer. Quite hackish, but at least it should work.

Yes, not a very good API design :/

simendsjo commented 1 year ago

Here's my solution. Replaces "-c" with "-c1" and "--clip" with "--clip=1" if it's not followed by an integer. And I pass the result to clingon:run. Good enough for me :)

(cl:defun patch-args-for-clip (args cl:&aux (result (cl:copy-list args)))
  (cl:dolist (spec (cl:list (cl:cons "-c" "-c1")
                            (cl:cons "--clip" "--clip=1"))
                   result)
    (cl:let* ((old (cl:car spec))
              (new (cl:cdr spec))
              (opt (cl:member old result :test 'cl:equal))
              (val (cl:cadr opt))
              (int? (cl:when val (cl:parse-integer val :radix 10 :junk-allowed cl:t))))
      (cl:when (cl:and opt (cl:not int?))
        (cl:nsubst new old result :test 'cl:equal)))))
simendsjo commented 1 year ago

Thanks for all your support! Guess we can close this as a won't fix as it's not really a recommended API design.