Creating error messages for missing arguments clojure.tools.cli

Hello,

I’m new to Clojure and programming without mutation in general.
I’m building a small command line tool using Clojure; and I’m parsing the arguments passed using clojure.tools.cli. Now some of the arguments I want to make as compulsory, and if user doesn’t pass those argument I want to print error message and exit.

Here’s snippet of the function which does this:

(defn parse-args
  [args]
  (let [cli-options
        [["-f" "--file \"path/to/file\"" :required "File"
          :id :file]
         ["-d" "--description \"description here\"" "Description"
         :id :desc
         :default "NA"]
         ["-t" "--tags \"Tag:SubTag\"" "Tags"
         :id :tags
         :default "Untagged"]
         ["-h" "--help"]]
        parsed-args (parse-opts args cli-options)
        duration  (-> parsed-args :arguments (get 0))
        file-path (-> parsed-args :options :file)
        tags      (-> parsed-args :options :tags (clojure.string/split #":"))
        desc      (-> parsed-args :options :desc)
        errors    (-> parsed-args :errors)
        errors (if (nil? file-path) (conj errors "No log file was specified (please use -f)") errors)
        errors (if (nil? duration) (conj errors "No timer duration was specified.") errors)]
    [duration file-path tags desc errors]))

main calls this functions and simply exits if errors is nil.
Notice how I’m rebinding errors in let. This works, but I guessed this isn’t a good way of doing it, so I improved it a little bit by passing a vector of field and err-msg and reducing the vector to a errors list:

(defn parse-args
  [args]
  (let [cli-options
        [["-f" "--file \"path/to/file\"" :required "File"
          :id :file]
         ["-d" "--description \"description here\"" "Description"
         :id :desc
         :default "NA"]
         ["-t" "--tags \"Tag:SubTag\"" "Tags"
         :id :tags
         :default "Untagged"]
         ["-h" "--help"]]
        parsed-args (parse-opts args cli-options)
        duration     (-> parsed-args :arguments (get 0))
        file-path    (-> parsed-args :options :file)
        tags         (-> parsed-args :options :tags (clojure.string/split #":"))
        desc         (-> parsed-args :options :desc)
        parse-errors (-> parsed-args :errors)
        errors       (reduce (fn [errors, [field err-msg]]
                               (if (nil? field) (conj errors, err-msg) errors))
                             parse-errors
                             (list [file-path "No log file was specified (please use -f)"]
                                   [duration  "No timer duration was specified."]))]
    [duration file-path tags desc errors]))

Now, I’ve three questions:

  1. Is there any way to make an argument required in clojure.tools.cli
  2. How to handle such situations, where in I have to repeatedly mutate a symbol
  3. In above example how could I have passed the vector using -> like macro. For example reduce in Elixir, take enumerable as first argument, so we can use |> operator (which I think is comparable to -> in Clojure) to push a list to Enum.reduce and apply a function.

Edited and code highlighted :slight_smile:

1 Like

If I understand your question regarding Elexir’s reduce, you might be looking for the ->> macro.

More generally, consider creating a sequence of vectors suitable for get-in to pick apart the structure returned by parse-args. A map example would be something like:

(let [parsed-args (parse-opts args cli-options)]
  (map #(get-in parsed-args %)
       [[:arguments 0]
        [:options :file]
        [:options :tags]
        [:options :desc]
        [:errors]]))

(Obviously, you would want to reduce over a set of [path error-string] pairs to produce a set of good param values and errors here.)

Another feature of tools.cli that you might want to use is passing a :parse-fn for a particular arg, which is where I’d do the tag splitting.

2 Likes