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

+1 vote
in tools.cli by

It's rather a rhetorical question as I've come to understand that tools.cli is not capable of doing anything with positional arguments but passing them on to the consumer of the library as strings in a vector. But I'll share my usecase.

I'm writing a tool that copies files from one directory to another. It takes two positional arguments, the source and destination. Right now my code contains two passes of input verification and parsing, first one for options with tools.cli and then a second one for arguments in my own function parse-arguments. It would be neat if these two things could both be done with in one step.

See the below code for a usage example.

(ns multi-machine-rsync.cli
  (:require [clojure.tools.cli :as cli]
            [clojure.java.io   :as io]
            [clojure.string    :as string]))

(defn read-settings
  [path]
  (read-string (slurp path)))

(def cli-options
  [["-h" "--help" "Show this help text"]
   ["-s" "--settings SETTING-FILE" "Path to the settings file."
    :parse-fn read-settings
    :default  "settings.edn"]])

(defn usage
  [options-summary]
  (string/join
   \newline
   ["Copies a directory from one place to another in a common"
    "directory structure using multiple machines."
    ""
    "Usage: multi-machine-rsync [OPTIONS] SOURCE DESTINATION"
    ""
    "Options:"
    options-summary]))

(defn parse-arguments
  "Parse and validate command line arguments. Either return a map                                                                                         
  indicating the program should exit (with a error message, and                                                                                           
  optional ok status), or a map indicating the action the program                                                                                         
  should take and the options provided."
  [args]
  (let [{:keys [arguments options errors summary]} (cli/parse-opts args cli-options)
  	[source destination]                       (map io/file arguments)]
    (cond
      (:help options)              {:exit-message (usage summary) :ok? true}
      errors                       {:exit-message errors}
      (nil? source)                {:exit-message "First positional argument SOURCE not given."}
      (nil? destination)           {:exit-message "Second positional argument DESTINATION not given."}
      (not (.exists source))       {:exit-message "Source directory does not exist."}
      (not (.exists destination))  {:exit-message "Destination directory does not exist."}
      :else                        {:options      options
                                    :source       source
                                    :destination  destination})))

2 Answers

+1 vote
by

As the maintainer of tools.cli, I'm going to give this some thought and decide whether to create a JIRA issue for it.

Most programs that use tools.cli end up needing some boilerplate code for help/usage, as well as some additional parsing and/or validation of arguments so enhancing the library to make this easier would help "everyone" if it can be done in a fairly simple way.

One conceptual issue to figure out is that the format for specifying options is only for actual options (i.e., starting with - or --) so there's currently nowhere to "hang" any free text argument processing right now.

0 votes
by

Do you think it would have saved you much work?

Positional parameters bring the challenge of optionals, series, key-value pairs, etc. And perhaps "dash options" are just more fish in that brook, as with "find" and "unzip". Clojure Spec has already illuminated that path. Perhaps you could integrate Spec into a tools.cli extension.

(Or throw in the towel and use --src and --dest!)

...