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