Welcome! Please see the About page for a little more info on how this works.

0 votes
in tools.cli by

I have a program that accepts a number of options at the CLI that I allow to be specified in a config file as well. That way, instead of passing --quiet to all calls, one can put into .splint.edn the map {quiet true} to have it enabled automatically. It is valuable sometimes to override the config file by passing the option at the CLI so when building the options map, I first load the config file and then merge in the CLI options: (merge local-config cli-options)

However, if I specify a default in the tools.cli spec, I lose that behavior because the defaulted option will unconditionally overwrite the config file when merged as it is always present in the CLI options map.

I've taken to leaving out the :default key from my tools.cli specs, opting to just write "Defaults to true." in the option description string. For example, [nil "--[no-]parallel" "Run splint in parallel. Defaults to true."] and then later (get config :parallel true) and then later (merge local-config cli-options). This means my summary doesn't have the pretty-printed FALSE in the option. (For example, --[no]-parallel FALSE Run splint in parallel.)

I've a few ideas about how to solve this little snag for me (beyond "You've already solved it, do nothing!"):
1) return an additional key from cli/parse-opts, something like :defaults. It would be a seq or set of the options that used the defaults. That way, I can add some logic like (merge (select-keys cli-options defaults) local-config (apply dissoc cli-options defaults)).
2) Add a :default-display to the cli/parse-opts specs, which will print the default without adding it to the options map. It would be on the user (dev) to ensure it's actually added to the options map or used in some way.
3) Add some functionality that allows for specifying a default in the summary but puts something else in the map. For example, --[no]-parallel FALSE but the map gets {:parallel :false}. This would let me choose the right value as appropriate.

Thanks so much!

You can achieve this a few ways without any change to tools.cli.

If the config file is well-known, then the program can read the config file first and use its contents as the CLI defaults. (Technically the config file would be a defaults file.)

If the config file is set by a command option or the help message must state ground-truth defaults and not config-file settings, then the program can declare defaults in a distinct map and compute both the CLI definition and the effective option settings from it:  compute the CLI definition from the defaults map with a function that bundles the defaults into the CLI-definition option description, and compute the effective option settings by merging the defaults map, the config settings, and the CLI settings in that order.
I'm not sure I fully understand the code path you're describing. I went into more detail in a comment under the accepted answer, as `:default-fn` (with some help) will work for me. I'd love to hear more about your idea if you feel like sharing, tho. It seems interesting, just hard to convert abstraction to actual code.

1 Answer

+1 vote
selected by
Best answer

I'm not quite sure I understand what you're trying to do but I'm thinking the :default-fn option may serve you better?

Sorry it's confusing. Maybe I can explain it a little simpler.

My program can print things to stdout. To check if it should, it looks at `:quiet` on the context object. Determining the value of `:quiet` follows this logic:

If the user has passed in `--quiet`, then `:quiet` is true.
Else if the user has set `quiet` in the config file, then `:quiet` is true.
Else `:quiet` is false.

I would like to display the `false` default in the `--help` summary from tools.cli, because it's nicely formatted (as seen below).

--[no-]quiet              false  Print no diagnostics, only summary.

However, by adding `:default false` in my `cli-options`, `cli/parse-opts` unconditionally returns `{:options {:quiet false}}`, so I can't know if the user supplied the `--quiet` option or tools.cli has merely returned the default.

Trying `:default-fn` again, I think it will work but the documentation doesn't have any examples of usage which is why I didn't have success previously. Maybe an example or two would help others in the future.

Here's a demonstration of how `:default-fn` interacts with `:default` and passed-in args, for anyone else who has the same request:

(def cli-options
  [[nil "--[no-]quiet" "Print no diagnostics, only summary."
    :default false
    :default-fn (fn [v] (prn :default-fn v) :new-val)]])

(defn validate-opts
  (let [{:keys [options]} (cli/parse-opts args cli-options)]
    (prn :options options)))

$ clojure -M:run
:default-fn {:quiet false}
:options {:quiet :new-val}

$ clojure -M:run --quiet
:options {:quiet true}

$ clojure -M:run --no-quiet
:options {:quiet false}
I forgot to formally offer, so if you accept pull requests, I can write up an example or two.
Contrib libraries do not accept PRs. I'll create a JIRA for this and if you've signed the CLA, you could provide a patch for it.

If you don't want to get that formal, hit me up on Slack with some suggestion documentation changes and I'll update the readme based on that :)
FWIW, the README already says:

;; The :default values are applied first to options. Sometimes you might want
;; to apply default values after parsing is complete, or specifically to
;; compute a default value based on other option values in the map. For those
;; situations, you can use :default-fn to specify a function that is called
;; for any options that do not have a value after parsing is complete, and
;; which is passed the complete, parsed option map as it's single argument.
;; :default-fn (constantly 42) is effectively the same as :default 42 unless
;; you have a non-idempotent option (with :update-fn or :assoc-fn) -- in which
;; case any :default value is used as the initial option value rather than nil,
;; and :default-fn will be called to compute the final option value if none was
;; given on the command-line (thus, :default-fn can override :default)

and I'm about to add:

;; Note: validation is *not* performed on the result of :default-fn (this is
;; an open issue for discussion and is not currently considered a bug).
I read that, but I find it quite dense. Compare that paragraph to the multiple examples demonstrating individual parts of the specs above it in the readme.

I’ll take a crack at something a little more readable.