{"id":17244846,"url":"https://github.com/hlship/cli-tools","last_synced_at":"2025-04-14T04:09:29.849Z","repository":{"id":52110395,"uuid":"520676365","full_name":"hlship/cli-tools","owner":"hlship","description":"CLIs and subcommands for Clojure or Babashka","archived":false,"fork":false,"pushed_at":"2025-01-27T23:56:46.000Z","size":4502,"stargazers_count":26,"open_issues_count":1,"forks_count":0,"subscribers_count":2,"default_branch":"main","last_synced_at":"2025-04-14T04:09:24.325Z","etag":null,"topics":["cli","clojure"],"latest_commit_sha":null,"homepage":"","language":"Clojure","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/hlship.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGES.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":".github/CODEOWNERS","security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2022-08-02T23:21:06.000Z","updated_at":"2025-04-07T13:27:23.000Z","dependencies_parsed_at":"2024-11-26T21:25:05.371Z","dependency_job_id":"020f11cc-0e7b-487a-bad6-8b27d8437035","html_url":"https://github.com/hlship/cli-tools","commit_stats":null,"previous_names":[],"tags_count":15,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hlship%2Fcli-tools","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hlship%2Fcli-tools/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hlship%2Fcli-tools/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hlship%2Fcli-tools/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/hlship","download_url":"https://codeload.github.com/hlship/cli-tools/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248819403,"owners_count":21166477,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["cli","clojure"],"created_at":"2024-10-15T06:27:45.334Z","updated_at":"2025-04-14T04:09:29.824Z","avatar_url":"https://github.com/hlship.png","language":"Clojure","funding_links":[],"categories":[],"sub_categories":[],"readme":"# io.github.hlship/cli-tools\n\n[![Clojars Project](https://img.shields.io/clojars/v/io.github.hlship/cli-tools.svg)](https://clojars.org/io.github.hlship/cli-tools)\n\u003ca href=\"https://babashka.org\" rel=\"nofollow\"\u003e\u003cimg src=\"https://github.com/babashka/babashka/raw/master/logo/badge.svg\" alt=\"bb compatible\" style=\"max-width: 100%;\"\u003e\u003c/a\u003e\n[![cljdoc badge](https://cljdoc.org/badge/io.github.hlship/cli-tools)](https://cljdoc.org/d/io.github.hlship/cli-tools)\n\n`cli-tools` is a complement to [Babashka](https://github.com/babashka/babashka) used to create tools\nwith sub-commands, much like [Babashka tasks](https://book.babashka.org/#tasks). It is effectively\na layer on top of [org.clojure/tools.cli](https://github.com/clojure/tools.cli).\n\n`cli-tools` is more verbose than [babashka-cli](https://github.com/babashka/cli) and more opinionated.\nAt the core, you define local symbols and instructions for how those symbols map to command line options\nor positional arguments; `cli-tools` takes care of the majority of command line parsing and validation\nfor you.\n\n`cli-tools` is intended to create three types of command lines tools:\n \n- A simple tool simply parses its command line arguments and executes some code using those arguments (think: `ls` or `cat`)\n- A common tool is composed of multiple commands, across multiple namespaces. The first command line argument\n  will select the specific sub-command to execute. (think: `git`)\n- A complex tool organizes some commands into command groups that share an initial name (think `kubectl`)\n\nFor tools with multiple commands, `cli-tools` automatically adds \na built-in `help` command to list out what commands are available, and\ncan even perform basic searches for commands.\n\nFor complex tools, what `cli-tools` offers is **discoverability**.  You define the switches, options, and arguments for each command, and each command gets a `--help` switch to print\nout the command's summary. The  `help` command\nthat can list out all the commands available, neatly organized, and can even do a simple\nsearch for you.  There's even (experimental) support for zsh completions for your tool and all of its categories, commands, and options.\n\nThis kind of discoverability is a big improvement over shell aliases, and one-off shell scripts that leave you guessing what they do and what arguments need to be passed in.\n\n`cli-tools` also offers great **feedback**, using indentation, color, and careful attention\nto detail, to provide tool users with consistent, readable, and friendly error messages, command summaries, and so forth.\n\n`cli-tools` can work with Babashka, or with Clojure, but the near instantaneous startup time of Babashka is compelling\nfor the kind of low-ceremony tools that `cli-tools` is intended for.\n\nAlthough `cli-tools` can be used to build shared tools, it is also fully intended for developers to create a personal\ntoolkit of commands specific to their individual workflows, as an alternative to a collection of shell aliases and one-off shell scripts.\n\nBelow is an example of the author's personal toolkit, `flow`:\n\n![Example](images/example-usage.png)\n\nA complete and open-source example is [dialog-tool](https://github.com/hlship/dialog-tool), which also shows how to organize \na tool so that it can be installed as a Homebrew formula.\n\n## Compatibility\n\n`cli-tools` is compatible with Clojure 1.11 and above, and w/ Babashka.\n\n## defcommand\n\nThe core utility is the `net.lewisship.cli-tools/defcommand` macro, which defines a command in\nterms of a command-line interface, and a body that acts on the data collected from the command line.\n\nThe interface defines options as well as positional arguments; those options and arguments are available\nin the body of the command just as if they were parameters passed to the command.\n`defcommand` defines a function that accepts a variadic number of command line argument strings, \nparses them as options and positional arguments, binds those to local symbols, and evaluates the body.\n\nAn example to begin; let's say you are creating a command for administrating some part of your application.\nYou need to know a URL to update, and a set of key/value pairs to configure.  Let's throw in a `--verbose`\noption just for kicks.\n\n**src/app_admin/commands.clj**:\n\n```clojure\n(ns app-admin.commands\n  \"Commands specific to this project\"\n  (:require [net.lewisship.cli-tools :refer [defcommand]]))\n\n(defcommand configure\n  \"Configures the system with keys and values\"\n  [verbose [\"-v\" \"--verbose\" \"Enable verbose logging\"]\n   :args\n   host [\"HOST\" \"System configuration URL\"\n         :validate [#(re-matches #\"https?://.+\" %) \"must be a URL\"]]\n   key-values [\"DATA\" \"Data to configure as KEY=VALUE\"\n               :parse-fn (fn [s]\n                           (when-let [[_ k v] (re-matches #\"(.+)=(.+)\" s)]\n                             [(keyword k) v]))\n               :update-fn (fn [m [k v]]\n                            (assoc m k v))\n               :repeatable true]]\n  ; Placeholder:               \n  (prn :verbose verbose :host host :key-values key-values))\n```\n\nThe meat of this `configure` command has been replaced with a call to `prn`, but\nthe important part for this discussion is the interface, which takes the place of an ordinary\nfunction's parameters declaration.\n\nInitially, the interface is about options, and we define one option, `--verbose`, a flag.\nInside the body, the value will be bound to local symbol `verbose`, which will be nil if `--verbose` is\nnot specified, or true if it is.\n\n`defcommand` always adds the `-h` / `--help` flag, and implements it; the body does not get\nevaluated if help is requested, or if there's any kind of validation error processing \ncommand line arguments.\n\nAn option definition always starts with three strings: the short option name, the long option name,\nand the option description; these are positional, and a nil may be supplied.\n\nA namespace with commands is only part of the solution, to get from a terminal command line\nto the body of the `configure` function, we need to add Babashka script, `bin/app-admin`, which \ninvokes the `cli/dispatch` function.\n\n\n**bin/app-admin**:\n\n```shell\n#!/usr/bin/env bb\n\n(require '[net.lewisship.cli-tools :as cli])\n\n(cli/dispatch {:namespaces '[app-admin.commands]})\n```\n\nThe first line identifies, to the shell, that this command is implemented using Babashka.\n\n`dispatch` will find all `defcommand`s in the given namespaces, parse the first command line argument, and use\nit to find the correct command to delegate to.  That command will be passed the remaining command line arguments.\nThe default tool name will be the name of the script, `app-admin` in this example.\n\n`dispatch` also recognizes `-h`, `--help`, or `help`, and will print out a summary of the available commands.\n\nFinally, `dispatch` will allow an abbreviation of a command name to work, as long as that abbeviation uniquely\nidentifies a single possible command.\n\nHow does the `app-admin` script know where to find the code?  We add a `bb.edn` file to the `bin` directory.\n\n**bin/bb.edn**\n\n```clojure\n{:paths [\"../src\" \"../resources\"]\n :deps {io.github.hlship/cli-tools {:mvn/version \"\u003cmvn version\u003e\"}}}      \n```\n\nBabashka looks for the `bb.edn` file in the same directory as the script, and uses it to set up the classpath.\n\nThe final step is to add that `bin` directory to the shell `$PATH` environment variable; this is done in your\n`~/.zshrc` file, or equivalent.\n\nAlternately, if you already have a location for commands, such as `~/bin`, you can create a symbolic link there\nthat points to your `bin/app-admin` script; Babashka will follow links and find the neighboring `bb.edn` file \nat the final location of the script.  Handy!\n\nWith all that in place, we can now run `app-admin configure` through its paces:\n\n![app-admin](images/app-admin-help.png)\n\n\u003e You may see a short delay the first time your script is executed as dependencies are resolved and downloaded;\n\u003e Subsequent executions are lightning fast.\n\n\nHelp is provided automatically, and builds its content from the interface and the docstring\nof each command function.  The docstring is required.\n\nValidations are reported with the  tool name and command name:\n\n![app-admin error](images/app-admin-error.png)\n\nThe text above is written to standard error, and the command exit status is 1 (where 0 would mean success).\n\nUnless there are errors, the body of the command is invoked:\n\n![app-admin success](images/app-admin-success.png)\n\nThe body here just prints out the values passed in.  That's not a bad starting point when creating new scripts.\nI like to get all the command line parsing concerns out of the way before working on the _meat_ of the command.\n\n## Abbreviated Commands\n\nFeel free to give your commands long names; when `dispatch` is identifying a command to invoke\nfrom the provided name on the command line, it will\nfind any commands whose name contains the provided name; so `app-admin conf` would work, as would `app-admin c` ... \nas long as there aren't multiple matches for the substring.\n\nWhen there are multiple matches, `dispatch` will abort and the error message will identify which commands matched the provided string.\n\nException: when the provided name _exactly_ matches a command's name, then that command will be used even if \nthe provided name is also a prefix or substring of some other command name.\n\n![ambiguous command](images/flow-ambiguous.png)\n\n## Positional Arguments\n\nThe way positional arguments are defined is intended to be similar to how\noptions are defined in `clojure.tools.cli`:\n\n```\n[\"\u003cLABEL\u003e\" \"\u003cDOC\u003e\" \u003ckey/value pairs\u003e]\n```\n\nThe `\u003cLABEL\u003e` is a string used in the summary, and in validation error messages;\nthe `\u003cDOC\u003e` is a string used in the summary.  After that come key/value pairs.\n\n* `:optional` (boolean, default false) -- if true, the argument may be omitted if there isn't a\n    command line argument to match\n\n* `:repeatable` (boolean, default false) -- if true, then any remaining command line arguments are processed\nby the argument\n\n* `:parse-fn` - passed the command line argument string, returns a value, or throws an exception\n\n* `:validate` - a vector of function/message pairs\n\n* `:update-fn` - optional function used to update the (initially nil) entry for the argument in the arguments map\n \n* `:assoc-fn` - optional function used to update the arguments map; passed the map, the argument id, and the parsed value\n\n* `:update-fn` and `:assoc-fn` are mutually exclusive.\n\nFor repeatable arguments, the default update function will construct a vector of values.\nFor non-repeatable arguments, the default update function simply sets the value.\n\nOnly the final positional argument may be repeatable.\n\nAlso note that all command line arguments _must be_ consumed, either as options or as positional arguments.\nAny additional command line arguments will be reported as a validation error.\n\n## defcommand options\n\nThe interface vector of defcommand may have additional options; these are keywords that change\nhow following values in the vector are parsed. We saw this in the example above, where `:args` was used to \nswitch from defining options to defining positional arguments.\n\n### :options\n\nIndicates that any following terms define options; this is the initial parser state, so `:options`\nis rarely used.\n\n### :args\n\nIndicates that any following terms define positional arguments.\n\n### :as \\\u003csymbol\\\u003e\n\nInside the interface, you can request the _command map_ using `:as`.\nThis map captures information about the command, command line arguments,\nand any parsed information; it is used when invoking `net.lewisship.cli-tools/print-errors`, \nwhich a command may wish to do to present errors to the user.\n\n### :command \\\u003cstring\\\u003e\n\nOverrides the default name for the command, which is normally the same as the function name.\nThis is useful, for example, when the desired command name would conflict with a clojure.core symbol,\nor something else defined within your namespace.\n\nThe `:command` option is also useful when using cli-tools to define\nthe `-main` function for a simple tool (a tool with options and arguments,\nbut not subcommands); this command name will be used in the command summary (from the \n`-h` / `--help` switch).\n\n### :summary \\\u003cstring\\\u003e\n\nNormally, the summary (which appears next to the command in the `help` tool summary) is just\nthe first sentence of the command's docstring, up to the first `.`.  If, for some reason,\nthat default is incorrect, the command's summary can be explicitly specified using `:summary`.\n\n### :in-order true\n\nBy default, options are parsed using `clojure.tools.cli/parse-opts`, with the `:in-order` option set to false;\nthis means that `parse-opts` will stop at the first\noption-like string that isn't declared.\n\n```\n(defcommand remote\n  \"Use ssh to run a command remotely.\"\n  [verbose [\"-v\" \"--verbose\"]\n   :args\n   command [\"COMMAND\" \"Remote command to execute\"]\n   remote-args [\"ARGS\" \"Arguments to remote command\"\n                :optional true\n                :repeatable true]]\n     ...)\n```\n\nYou might expect that `app-admin remote ls -lR` would work, but it will fail\nwith an error that `-lR is not recognized`.\n\nYou can always use `--` to split options from arguments, so `app-admin remote -- ls -lR` will work,\nbut is clumsy.\n\nInstead, add `:in-order true` to the end of the interface, and any\nunrecognized options will be parsed as positional arguments instead,\nso `app-admin remote ls -lR` will work, and `-lR` will be provided as a string in the `remote-args`\nseq.\n\n### :let \\\u003cbindings\\\u003e\n\nIt can be useful to define local symbols that can be referenced inside the option\nand arguments definitions; the `:let` keyword is followed by a vector of bindings.\n\n```clojure\n(defcommand set-mode\n  \"Sets the execution mode\"\n  [mode [\"-m\" \"--mode MODE\" (str \"Execution mode, one of \" mode-names)\n         :parse-fn keyword\n         :validate [allowed-modes (str \"Must be one of \" mode-names)]]\n   :let [allowed-modes #{:batch :async :real-time}\n         mode-names (-\u003e\u003e allowed-modes (map name) sort (string/join \", \"))]]\n  ...)\n```\n\n\u003e Note that the `new.lewisship.cli-tools/select-option` function is an easier way to create such\n\u003e an option.\n\nIn the expanded code, the bindings are moved to the top, before the option and argument\ndefinitions.  Further, if there are multiple `:let` blocks, they are concatinated.\n\nThis also means that the bindings _can not_ reference the symbols for options or arguments.\n\n### :validate \\\u003cvector of test/message pairs\\\u003e\n\nOften you will need to perform validations that consider multiple fields.\nThe `:validate` directive adds tests that occur after primary parsing of command line options\nhas occurred, but before executing the body of the function.\n\nIt is a vector of tests and messages.\nEach test expression is evaluated in turn; if the result is falsey, then the message\nis passed to `print-errors` as an error, and `exit` is called with the value 1.\n\nA common case is to handle mutually exclusive arguments:\n\n```clojure\n(defcommand sort-data\n  \"Sorts some data\"\n  [alpha [\"-a\" \"--alpha-numeric\" \"Sort in alpha-numeric order\"]\n   numeric [\"-n\" \"--numeric\" \"Sort in numeric order\"]\n   :validate [(not (and alpha numeric)) \"Only one of --alpha-numeric or --numeric is allowed\"]]\n  ; At most one of alpha or numeric is true here\n)\n```\n\nNote that unlike a validate function for an option or argument, these are expressions that can leverage \nlocal symbols (such as `alpha` and `numeric`) and not functions that are passed a value.\n\n## Namespaces and categories\n\nEach namespace that defines commands (as passed to the `dispatch` function) becomes a _category_,\ncontaining the commands defined in that namespace.\n\nThis is used by the built-in `help` command, which prints a summary of the tool and all commands\nin the tool:\n\n```\n\u003e app-admin help\nUsage: app-admin COMMAND ...\n\nApplication adminstration tools.\n\nCommands:\n\nSystem configuration\n  configure: Configures the system with keys and values\n       list: List configuation for the system\n\nBuilt-in\n       help: List available commands\n```\n\nThe namespace `net.lewisship.cli-tools` is automatically added, and has the label \"Built-in\".\nBy default, a namespace's label is the same as it's namespace name, but this is usually\noverridden by setting the :command-category metadata on the namespace to a short string.\n\nEach category has a sort order, which defaults to 0.  The categories are sorted by this sort order,\nthen (within each set of categories with the same sort order) by label.  The sort order\ncan be specified as the :command-category-order metadata on the namespace.  `net.lewisship.cli-tools.builtins` has\na sort order of 100, so that it will generally be last.\n\nIf you want to see the list of commands without categories, use the `-f` / `--flat` option to `help`.\nIf you want to use multiple namespaces for your commands without using categories,\nadd `:flat true` to the options map passed to `dispatch`.\n\nThe `help` command itself accept a single search term; it will filter the commands and categories it outputs to only\nthose that contain the search term in either the command name, or command summary. This search is caseless.\n\n## :command-ns meta-data\n\nNormally, there is a 1:1 mapping from namespace to category. In rare cases, you may want to have multiple namespaces\nmap to the same category.\n\nA namespace may have a :command-ns meta-data, whose value is a symbol identifying another namespace.  The commands\nin the new namespace are categorized as if they were in the identified namespace.  Order counts: make sure the referenced\nnamespace is listed before the referencing namespace.\n\nAn example of this is the `net.lewisship.cli-tools.colors` namespace:\n\n```clojure\n\n(ns net.lewisship.cli-tools.colors\n  {:command-ns 'net.lewisship.cli-tools.builtins}\n  (:require [clj-commons.ansi :refer [pout]]\n            [clojure.string :as string]\n            [net.lewisship.cli-tools :refer [defcommand]]))\n\n(def ^:private width 16)\n\n(defcommand colors\n...\n```\nThis adds an additional command to the built-in category, `colors`, as if it were declared in the `builtins` namespace.\n\nYou can add this namespace to your own tools:\n\n![colors command](images/flow-colors.png)\n\n## Command Groups\n\nA category can also have a `:command-group` metadata value, a short string that acts like a command name.\nAll commands in the same namespace/category are only accessible via that group command.  The built-in `help`\ncommand will identify the command group when listing the commands in the category.\n\nCommand groups are useful when creating the largest tools with the most commands; it allows for shorter command names,\nas each commands' name will only have to be unique within it's command group, not globally.\n\n## Utilities\n\n### abort\n\nThe `net.lewisship.cli-tools/abort` function provides a uniform way to indicate a failure\nand terminate execution.\n\n`abort` can be invoked from inside a command, and will output (to standard error)\nthe tool name and command name in bold red, and the provided messages\nin red.\n\nMessages may also be exceptions, from which the exception message is extracted (exceptions with a null message\nare converted to the exception class name).\n\nFinally, `abort` invokes `exit` with the provided exit status code.\n\n## Testing\n\nNormally, the function defined by `defcommand` is passed a number of string arguments, from\n`*command-line-args*`; it then parses this into a command map, a map with an `:options` key\nthat contains all the parsed and validated values for options and positional arguments (plus a lot of undocumented internal data).\n\nFor testing purposes, you can bypass the parsing and validation, and just pass a single map to the function. \nThe map must provide a keyword key for each option or positional argument; the keys match the option or argument symbols,\neven for options that normally have a default value. All normal option or argument validation is skipped.\n\nYou may need to mock out `net.lewisship.cli-tools/print-errors` if your command\ninvokes it, as that relies on some internal state from undocumented dynamicall-bound vars. \n\nFortunately, it is quite rare for a command to need to invoke this function.\n\nWhen _not_ bypassing parsing and validation (that is, when testing by passing strings to the command function), \nvalidation errors normally print a command summary and then call `net.lewisship.cli-tools/exit`, which in turn, invokes `System/exit`; this is obviously \nproblematic for tests, as the JVM will exit during test execution.\n\nThe function `net.lewisship.cli-tools/set-prevent-exit!` can convert those cases to instead\nthrow an exception, which can be caught by tests.\n\nFurther, application code should also invoke `net.lewisship.cli-tools/exit`\nrather than `System/exit`, for the same reasons.\n\n## Caching\n\nIn order to operate, `cli-tools/dispatch` has to load all namespaces, to execute the `defcommand` macros in each, \nand collect meta-data from all the namespaces and command functions.  Thanks to Babashka, this is extremely fast,\nbut is made faster using caching.\n\n`dispatch` builds a cache based on the options passed to it, and the contents of the classpath; it can then \nload the data it needs to operate from the cache, if such data is present.\n\nWhen executing from the cache, `dispatch` will ultimately load only a single command namespace,\nto invoke the single command function.  This allows a complex tool, one with potentially hundreds of commands, to still execute the body\nof the `defcommand` within milliseconds.\n\nThis may have an even more significant impact for a tool that is built on top of Clojure, rather than Babashka.\nOur mockup of 1500 commands across 250 namespaces executes approximately \ntwice as fast using the cache (approximately 8 seconds with the cache, vs. 17 seconds without).\n\nBabashka is amazingly fast for these purposes; the same test executes in 0.23 seconds.\n\nBy default, `dispatch` will store its cache in the `~/.cli-tools-cache` directory; the environment variable\n`CLI_TOOLS_CACHE_DIR` can override this default.\n\n## Tips and Tricks\n\n### Parsing Numbers\n\nWhen an input is numeric, you can use Clojure's `parse-long` function to parse a number; it returns nil if\nthe string is not a number.  You can then check using `some?` within :validate:\n\n```\n(defcommand kill-port\n  \"Kills the listening process locking a port.\"\n  [force [\"-f\" \"--force\" \"Kill process without asking for confirmation\"]\n   :args\n   port [\"PORT\" \"Port number to kill\"\n         :parse-fn parse-long\n         :validate [some? \"Not a number\"\n                    pos? \"Must be at least 1\"]]]\n  ...)                    \n```\n\nThis handles invalid input gracefully:\n\n```\n\u003e flow kill-port abc\nError in flow kill-port: PORT: Not a number\n```\n\nYou might be tempted to use `#(Long/parseLong %)` as the parse function; this works, but the message produced comes from \nthe message of the thrown exception, and is not very friendly:\n\n```\n\u003e flow kill-port abc\nError in flow kill-port: PORT: Error in PORT: For input string: \"abc\"\n```\n\n## Job Board (experimental)\n\nFor tools that run for a while, visual feedback can be provided to the user using the _job board_\nin the `net.lewisship.cli-tools.job-status` namespace.\n\n![Job Board Demo](images/job-board-demo.gif)\n\nBackground processes (typically, running as individual threads, or core.async processes) \ncan provide feedback on status and progress through a simple API. \nThe job board updates status lines as they change, and highlights lines that have recently changed.\n\nThis is built on the `tput` command line tool, so it works on OS X and Linux, but **not on Windows**.\n\nThe above `job-status-demo`, like `colors`, can be added by including the `net.lewisship.cli-tools.job-status-demo` namespace.\n\n## zsh completions (experimental)\n\nThe namespace `net.lewisship.cli-tools.completions` adds a `completions` command.  This command will\ncompose a zsh completion script, which can be installed to a directory on the $fpath such as\n`/usr/local/share/zsh/site-functions`.\n\n![zsh completions demo](images/cli-tools-zsh-completion.gif)\n\nzsh completions greatly enhance the discoverability of commands, categories, and command options within a tool.\nHowever, this functionality is considered _experimental_ due to the complexity of zsh completion scripts.\n\n\n## Linting\n\n`defcommand` is complex and will confuse clj-kondo out of the box, but we provide\nhooks to allow clj-kondo to reasonably lint it.\n\nThe hooks are provided with config path `io.github.hlship/cli-tools`.\n\n## License\n\n`io.github.hlship/cli-tools` is (c) 2022-present Howard M. Lewis Ship.\n\nIt is released under the terms of the Apache Software License, 2.0.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fhlship%2Fcli-tools","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fhlship%2Fcli-tools","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fhlship%2Fcli-tools/lists"}