clon-spec
clon-spec copied to clipboard
Command-Line Object Notation: Ergonomic JSON-compatible input syntax for CLI tools.
Command-Line Object Notation
Ergonomic JSON-compatible input syntax for CLI tools.
CLON is an argument syntax spec for defining JSON-like objects in a way that makes sense for the command-line that isn't just taking raw JSON or pointing to a file. This was shamelessly stolen from HTTPie with some modification.
CLON is not meant to be a tool itself, but an argument syntax that you can use when building CLI tools that need to take arbitrary JSON-like data. An example tool would be a CLI client for an RPC system with a usage format like rpctool call <method> <args...>
. CLON makes for a reasonably human-friendly way to input the argument data. Note that internally the tool may not use this syntax to build JSON, but a compatible format like CBOR, or even just to build an in-memory structure.
Implementations
Below are some language specific libraries for easily accepting this syntax in your command-line tools:
- progrium/clon-go
- your library here
Simple Example
In these examples, clon
is an imaginary tool that takes CLON arguments and outputs the resulting structure as JSON.
$ clon name=John [email protected]
{
"name": "John",
"email": "[email protected]"
}
If the first argument is in key-value form (with =
or :=
operator), the arguments will be used to construct an object. If the first argument is simply a value, the arguments will be used to construct an array.
$ clon John [email protected] "Text string"
[
"John",
"[email protected]",
"Text string"
]
Non-string JSON fields
Non-string JSON fields use the :=
separator, which allows you to embed arbitrary JSON data into the resulting JSON object. Additionally, text and raw JSON files can also be embedded into fields using =@
and :=@
:
$ clon \
name=John \ # String (default)
age:=29 \ # Raw JSON — Number
married:=false \ # Raw JSON — Boolean
hobbies:='["http", "pies"]' \ # Raw JSON — Array
favorite:='{"tool": "HTTPie"}' \ # Raw JSON — Object
bookmarks:=@files/data.json \ # Embed JSON file
description=@files/text.txt # Embed text file
{
"age": 29,
"hobbies": [
"http",
"pies"
],
"description": "John is a nice guy who likes pies.",
"married": false,
"name": "John",
"favorite": {
"tool": "HTTPie"
},
"bookmarks": {
"HTTPie": "https://httpie.org",
}
}
When building a top level array, values can be prefixed with :
to indicate a non-string or raw JSON value. This is a shorthand for the top level array syntax described later.
$ clon John :29 :false :'{"tool": "HTTPie"}'
[
"John",
29,
false,
{
"tool": "HTTPie"
}
]
Nested JSON
You still use the existing data field operators (=
/:=
) but instead of specifying a top-level field name (like key=value
), you specify a path declaration telling where and how to put the given value inside an object:
$ clon \
platform[name]=HTTPie \
platform[about][mission]='Make APIs simple and intuitive' \
platform[about][homepage]=httpie.io \
platform[about][stars]:=54000 \
platform[apps][]=Terminal \
platform[apps][]=Desktop \
platform[apps][]=Web \
platform[apps][]=Mobile
{
"platform": {
"name": "HTTPie",
"about": {
"mission": "Make APIs simple and intuitive",
"homepage": "httpie.io",
"stars": 54000
},
"apps": [
"Terminal",
"Desktop",
"Web",
"Mobile"
]
}
}
Basic Usage
Let’s start with a simple example, and build a simple search query:
$ clon \
category=tools \
search[type]=id \
search[id]:=1
{
"category": "tools",
"search": {
"id": 1,
"type": "id"
}
}
In the example above, the search[type]
is an instruction for creating an object called search
, and setting the type
field of it to the given value ("id"
).
Also note that, just as the regular syntax, you can use the :=
operator to directly pass raw JSON values (e.g, numbers in the case above).
Building arrays is also possible, through []
suffix (an append operation). This creates an array in the given path (if there is not one already), and append the given value to that array.
$ clon \
category=tools \
search[type]=keyword \
search[keywords][]=APIs \
search[keywords][]=CLI
{
"category": "tools",
"search": {
"keywords": [
"APIs",
"CLI"
],
"type": "keyword"
}
}
If you want to explicitly specify the position of elements inside an array, you can simply pass the desired index as the path:
$ clon \
category=tools \
search[type]=keyword \
search[keywords][1]=APIs \
search[keywords][0]=CLI
{
"category": "tools",
"search": {
"keywords": [
"CLIs",
"API"
],
"type": "keyword"
}
}
If there are any missing indexes, they will be nullified in order to create a concrete object:
$ clon \
category=tools \
search[type]=platforms \
search[platforms][]=Terminal \
search[platforms][1]=Desktop \
search[platforms][3]=Mobile
{
"category": "tools",
"search": {
"platforms": [
"Terminal",
"Desktop",
null,
"Mobile"
],
"type": "platforms"
}
}
It is also possible to embed raw JSON to a nested structure, for example:
$ clon \
category=tools \
search[type]=platforms \
search[platforms]:='["Terminal", "Desktop"]' \
search[platforms][]=Web \
search[platforms][]=Mobile
{
"category": "tools",
"search": {
"platforms": [
"Terminal",
"Desktop",
"Web",
"Mobile"
],
"type": "platforms"
}
}
And just to demonstrate all of these features together, let’s create a very deeply nested JSON object:
$ clon \
shallow=value \ # Shallow key-value pair
object[key]=value \ # Nested key-value pair
array[]:=1 \ # Array — first item
array[1]:=2 \ # Array — second item
array[2]:=3 \ # Array — append (third item)
very[nested][json][3][httpie][power][]=Amaze # Nested object
Advanced Usage
Top level arrays
To build an array instead of a regular object, you can simply do that by omitting the starting key:
$ clon \
[]:=1 \
[]:=2 \
[]:=3
[
1,
2,
3
]
As mentioned before, there is a shorthand to make this scenario much easier. The first argument must not be a key-value, but key-values can be used in later arguments producing a single key object. You can also use the raw value prefix :
to include JSON arrays or objects.
$ clon :1 :2 :3 valid:=true :'[4, 5, 6]'
[
1,
2,
3,
{
"valid": true
},
[
4,
5,
6
]
]
For more complex top level elements, you can use the standard notation to apply nesting to the items by referencing their index:
$ clon \
[0][type]=platform [0][name]=terminal \
[1][type]=platform [1][name]=desktop
[
{
"type": "platform",
"name": "terminal"
},
{
"type": "platform",
"name": "desktop"
}
]
Escaping behavior
Nested JSON syntax uses the same escaping rules as the terminal. There are 3 special characters, and 1 special token that you can escape.
To include a bracket as is, escape it with a backslash (\
):
$ clon \
'foo\[bar\]:=1' \
'baz[\[]:=2' \
'baz[\]]:=3'
{
"baz": {
"[": 2,
"]": 3
},
"foo[bar]": 1
}
If use the literal backslash character (\
), escape it with another backslash:
$ clon \
'backslash[\\]:=1'
{
"backslash": {
"\\": 1
}
}
A regular integer in a path (e.g [10]
) means an array index; but if you want it to be treated as a string, you can escape the whole number by using a backslash (\
) prefix.
$ clon \
'object[\1]=stringified' \
'object[\100]=same' \
'array[1]=indexified'
{
"array": [
null,
"indexified"
],
"object": {
"1": "stringified",
"100": "same"
}
}
Guiding syntax errors
If you make a typo or forget to close a bracket, the errors SHOULD guide you to fix it. For example:
$ clon \
'foo[bar]=OK' \
'foo[baz][quux=FAIL'
Syntax Error: Expecting ']'
foo[baz][quux
^
You can follow to given instruction (adding a ]
) and repair your expression.
Type safety
Each container path (e.g., x[y][z]
in x[y][z][1]
) has a certain type, which gets defined with the first usage and can’t be changed after that. If you try to do a key-based access to an array or an index-based access to an object, you should get an error out:
$ clon \
'array[]:=1' \
'array[]:=2' \
'array[key]:=3'
Type Error: Can't perform 'key' based access on 'array' which has a type of 'array' but this operation requires a type of 'object'.
array[key]
^^^^^
Type Safety does not apply to value overrides, for example:
$ clon \
user[name]:=411 # Defined as an integer
user[name]=string # Overridden with a string
{
"user": {
"name": "string"
}
}
Raw JSON
For very complex JSON structures, it may be more convenient to pass it as raw input, for example:
$ echo -n '{"hello": "world"}' | clon
$ clon < files/data.json