Skip to content

ikappaki/argq.alpha

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

3 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

argq.Ξ±

About

(Ξ±-status, everything is subject to change)

argq is a small Clojure library with no dependencies to support uniform argument quoting across platforms, and much more.

Problem

It is most useful for cross-platform compatibility to be able to write command line tools whose arguments can use the same syntax across all platforms, but unfortunately different shells across different platforms treat some characters specially, requiring using unique syntax handling on each platform that is not applicable to other platforms.

A good example of such characters is the double quote " and its escaped sequences such as \" and \\\". There is no known common syntax to make escaped double quotes to work both on Unix and MS-Windows shells.

argq lib

argq is an extensible, dependencies free, non-intrusive library which solves the argument syntax diversity problem by limiting the set of allowable syntax characters in an argument to those that have no special meaning in any platform, while escaping everything else.

Features

  • Uniform argument syntax across all platforms.
  • Inline tools to assist user with escaping and un-escaping of arguments.
  • Only a single function call is required at the start of your program to enable the functionality.
  • Has no external dependencies and is non-intrusive, only kicks in if an argument matches a known tag.
  • Provides single character escape mnemonics that should be easy to remember or guess their meaning mechanically after some use, see *-escape syntax.
  • Extensible via new tags to use the full power of Clojure to transform arguments in any way imaginable beyond quoting, inspired by the edn tagged elements.

Concerns

  • Syntax looks alien and might scare people off, while writers should not impose such syntax on people.

Thoughts

The library as a minimum can be used to solve a very particular issue which has dare consequences: publishing cross platform command line instructions to the world.

If the instructions do not work as advertised on some platforms of interest, as it is currently the likely case for MS-Windows, then this is most probable to scare newcomers off and confuse the rest of the people, limiting Clojure reach to specific platforms only. The library addresses this issue.

The syntax should not look alien (i.e. incomprehensible) to Clojurians. It uses the familiar tagged literal syntax of #ns/symbol to indicate the element which follows should be treated in a special way. Most of the argument value maintains its original form except for a dozen of symbols that are escaped using mnemonic characters, thus the hope is that a user should be able to reason about the argument value without much difficulty.

Furthermore, the library's use is optional and its syntax at the very least is not to be imposed on others who don't use it. It comes with tooling to let authors publish their CLI invocation to the web without the need to know anything about the quoting rules. Users can just copy paste the that command from the web on their shell and it should just work across any platform/shell.

The library offers much more than cross-platform argument compatibility for those that want to opt-in (see Case studies for an example of how to write an extension to invoke sub commands in arguments).

Setup

Manually, to include lib dependency in deps.edn

ikappaki/argq.alpha {:git/url  "https://github.com/ikappaki/argq.alpha" :sha "..."}

or with neil, to add the library to deps.edn

neil add dep ikappaki/argq.alpha --latest-sha

or to upgrade to the latest sha

neil dep upgrade :lib ikappaki/argq.alpha

Require the library

(:require [ikappaki.argq.alpha :as q])

At the beginning of your -main function, pass the command line arguments sequence to q/args-parse and indicate what action to take on parsing error:

(defn -main
  [& args]
  (let [args (q/args-parse args :on-error! (fn [error _args]
                                             (println :error (pr-str error))
                                             (System/exit 1)))]
    ;; ...
    ))

You can now pass argq values to your program arguments, using the "'#ns/tag[opts] element'" syntax.

Usage

*-escape syntax

Escape codes are two character codes starting with * where the second character is either a single digit or a Latin character mnemonic.

code escapes explain special in
** or *a * asterisc argq
*A & Ambersand PS
*B ` Backtick PS
*C ^ Caret PS
*D $ Dollar unix shell, PS
*G > Greater-than PS
*I | pIpe PS
*L < Less-than PS
*P % Per-cent cmd
*q ' single-quote all
*Q " double-Quote all
*S \ backSlash all
*1 \" 1-time backslashed double quote all

tags

The lib only kicks in when there is an argument of the form "'#ns/tag[opts] element'" and the ns/tag is a known tag.

Please note that the argument must be wrapped in a pair of double and single quotes "' to maximize cross platform compatibility.

As an example, a command line argument of "'#clj/esc this is a *Qstring*Q'" has a ns/tag of clj/esc and an element of this is a *Qstring*Q.

argq will dispatch the element to the :clj/esc handler, which will un-escape it and pass this is a "string" to the program as the actual argument value.

Information with examples about each standard tag follows.

clj/help

Get help about the known tags

your-program "'#clj/help'"

clj/esc

Unescape rest of the argument and pass value to program as such.

your-program "'#clj/esc *Q this argum*Dnt has *1 double quotes *1 *Q'"
# arg1 => " this argum$nt has \" double quotes \" "

If you want to know the actual value that will be passed to the program, append the :v (for Verbose) option to the tag

your-program "'#clj/esc:v I*qm using the following escaped chars *a *A *B *C *D *G *I *L *P *q *Q *S *X *1'"
# arg1 => I'm using the following escaped chars * & ` ^ $ > | < % ' " \ *X \"
#
# stdout => #:ikappaki.argq.alpha{:pos 0,
#                                 :in
#                                 "I*qm using the following escaped chars *a *A *B *C *D *G *I *L *P *q *Q *S *X *1",
#                                 :out
#                                 "I'm using the following escaped chars * & ` ^ $ > | < % ' \" \\ *X \\\""}

You can mix arguments of any type in any order

your-program "first argument" "'#clj/esc second *Largument*G'" "third argument"
# arg1 => first argument
# arg2 => second <argument>
# arg3 => third argument

clj/prompt

Let argq prompt the user for the argument value and print out its escaped counterpart that can be used on the command line as an escaped argument for future invocations

your-program "'#clj/prompt please enter a value for this argument'"
# stdout => Enter value for arg at pos 1, please enter a value for this argument:
# stdin <= "test 100%"
#
# arg1 => "test 100%"
#
# stdout => :input
#             "test 100%"
#
#           :use-this-as-a-safe-command-line-argument-replacement
#             "'#clj/esc *Qtest 100*P*Q'"

The user entered a hypothetical `"test 100%" value as the argument input when prompted, and can now replace the program's argument with the suggested quoted value

your-program "'#clj/esc *Qtest 100*P*Q'"

i.e. your-program is going to receive "test 100%" as the argument value.

clj/publish

When ready, you can publish your arguments to the world for use on any platform

your-program "'#clj/publish'" 'first argument' "second <argument>" "'#clj/prompt enter third argument'"
# stdout => Enter value for arg at pos 4, enter third argument:
# stdin <= "test 100%"
#
# arg1 => first argument
# arg2 => second <argument>
# arg3 => "test 100%"
#
# stdout => :cross-platform-args
#             'first argument' "'#clj/esc second *Largument*G'" "'#clj/esc *Qtest 100*P*Q'"

playground

Clone this codebase and give argq arguments passing a try

[powershell -Command] clojure -M:main-test "first arg" "'#clj/esc *0quoted arg*0'" "'#clj/prompt enter last argument'"

You can add new tags in your code by simply defining a new transform method. The argq/file tag is not part of the spec and has a very simple definition in test/ikappaki/argq/main.clj.

(defmethod q/transform :argq/file
  ;; Returns an `argq` result map of the contents of file at ELEMENT
  ;; path, or an error with INFOrmation about the argument if
  ;; something went wrong.
  ;;
  ;; Return map can have the following keys:
  ;;
  ;; :argq/res the contents of the file.
  ;;
  ;; :argq/err error details.
  [_ element & info]
  (if-not element
    {:argq/err [:argq/file :error :!element info]}

    (try
      {:argq/res (slurp element)}
      (catch Exception e
        {:argq/err [:argq/file :error (pr-str (ex-message e)) info]}))))
[powershell -Command] clojure -M:main-test "'#clj/help'"
[powershell -Command] clojure -M:main-test "next argument is read from a file" "'#argq/file deps.edn'"

Case studies

Real world example cases that can benefit from argq.

Clojure CLI Tools

πŸ“œ Official doc: Clojure CLI invocation that fails with PowerShell on MS-Windows

https://clojure.org/guides/deps_and_cli#command_line_deps

It invokes clojure with a dependency specified on the command line as an edn map

clojure -Sdeps '{:deps {org.clojure/core.async {:mvn/version "1.5.648"}}}'

but this won't work on MS-Windows, because the second argument will be passed in to the program without the quotes, i.e. the version number will be unquoted resulting to an error.

with argq the call can be made cross-platform with

clojure -Sdeps "'#clj/esc {:deps {org.clojure/core.async {:mvn/version *Q1.5.648*Q}}}'"

πŸ“œ Official doc: Clojure CLI invocation on MS-Windows that comes three different variants depending on the shell it is invoked on

https://github.com/clojure/tools.deps.alpha/wiki/clj-on-Windows

The page lists three different ways to pass a deps map as an argument based on the shell being invoked on MS-Windows

  1. PowerShell

    clj -Sdeps '{:deps {viebel/klipse-repl {:mvn/version ""0.2.3""}}}' -m klipse-repl.main
  2. Command prompt

    powershell -command clj -Sdeps '{:deps {viebel/klipse-repl {:mvn/version """"""0.2.3""""""}}}' -m klipse-repl.main
  3. Git Bash

    powershell -command 'clj -Sdeps "{:deps {viebel/klipse-repl {:mvn/version """"0.2.3""""}}}" -m klipse-repl.main'

while argq can be called the same across all platforms

[powershell -Command] clj -Sdeps "'#clj/esc {:deps {viebel/klipse-repl {:mvn/version *Q0.2.3*Q}}}'" -m klipse-repl.main

πŸ“œ Ask Clojure: question on how to pass command output as edn string inline argument

https://ask.clojure.org/index.php/11585/convention-bypassing-parsing-reduce-quotes-arguments-passed

The question asks how it might be possible to use command substitution on the Unix shell (i.e. $()) to pass the output of a command as an argument to clojure -X when the latter only expects and accepts EDN values

clojure -X:bench :json "$(my_json_producing_cmd --blah)"

The above hypothetical my_json_producing_cmd command will return a json string which can't be parsed as valid EDN form by clojure -X.

A potential solution using argq, is to create a new shell tag that will run the given command and return its std output as an argument. If it is called with an :s option, then the argument value will be returned as an EDN string (i.e. using pr-str)

clojure -X:bench :json "'#argq/shell:s my_json_producing_cmd --blah'"

This will also work across all platforms, not just with the Unix shell.

A toy shell tag can be trivially written with babashka.process as

(defmethod q/transform :argq/shell
  ;; Returns an `argq` result map of the stdout of running the ELEMENT
  ;; command, or an error with INFOrmation about the argument if
  ;; something went wrong.
  ;;
  ;; Supports the following kw options in OPT-SET:
  ;;
  ;; :s Convert result to Clojure string.
  ;;
  ;; Return map can have the following keys:
  ;;
  ;; :argq/res the stdout of the command.
  ;;
  ;; :argq/err error details.
  [_ element & {:keys [opts-set] :as info}]
  (if-not element
    {:argq/err [:argq/shell :error :!element info]}

    (try
      {:argq/res (let [{:keys [out]} (-> (p/process element {:out :string})
                                                  p/check)]
                   (if (some #{:s} opts-set)
                     (pr-str out)
                     out))}
      (catch Exception e
        {:argq/err [:argq/shell :error (pr-str (ex-message e)) info]}))))

(see test/ikappaki/argq/main.clj).

Ξ±-status

Some of the naming choices are likely to change to facilitate greater acceptance

  • Escape codes start with *, perhaps other symbols such as ! or / or ? are more pleasant to the eye?
  • The standard tag literals have a namespace of clj (e.g. as in #clj/publish), this could change to either something shorted (e.g. q) or more library specific (e.g. argq)?
  • The standard tag literals have a long name, such as #clj/publish, this can change to something shorter, such as #clj/pub or #clj/p or aliased as such?

About

A small clojure library to support uniform argument quoting across platforms, and much more

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published