├── LICENSE └── README.md /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Jeff Lindsay 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Command-Line Object Notation 2 | Ergonomic JSON-compatible input syntax for CLI tools. 3 | 4 | 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](https://httpie.io/docs/cli/json) with some modification. 5 | 6 | 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 `. 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. 7 | 8 | ### Implementations 9 | 10 | Below are some language specific libraries for easily accepting this syntax in your command-line tools: 11 | 12 | * [progrium/clon-go](https://github.com/progrium/clon-go) 13 | * your library here 14 | 15 | ## Simple Example 16 | In these examples, `clon` is an *imaginary tool* that takes CLON arguments and outputs the resulting structure as JSON. 17 | 18 | ```shell 19 | $ clon name=John email=john@example.org 20 | ``` 21 | ```json 22 | { 23 | "name": "John", 24 | "email": "john@example.org" 25 | } 26 | ``` 27 | 28 | 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. 29 | 30 | ```shell 31 | $ clon John john@example.org "Text string" 32 | ``` 33 | ```json 34 | [ 35 | "John", 36 | "john@example.org", 37 | "Text string" 38 | ] 39 | ``` 40 | 41 | ## Non-string JSON fields 42 | 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 `:=@`: 43 | 44 | ```shell 45 | $ clon \ 46 | name=John \ # String (default) 47 | age:=29 \ # Raw JSON — Number 48 | married:=false \ # Raw JSON — Boolean 49 | hobbies:='["http", "pies"]' \ # Raw JSON — Array 50 | favorite:='{"tool": "HTTPie"}' \ # Raw JSON — Object 51 | bookmarks:=@files/data.json \ # Embed JSON file 52 | description=@files/text.txt # Embed text file 53 | ``` 54 | ```json 55 | { 56 | "age": 29, 57 | "hobbies": [ 58 | "http", 59 | "pies" 60 | ], 61 | "description": "John is a nice guy who likes pies.", 62 | "married": false, 63 | "name": "John", 64 | "favorite": { 65 | "tool": "HTTPie" 66 | }, 67 | "bookmarks": { 68 | "HTTPie": "https://httpie.org", 69 | } 70 | } 71 | ``` 72 | 73 | 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. 74 | 75 | ```shell 76 | $ clon John :29 :false :'{"tool": "HTTPie"}' 77 | ``` 78 | ```json 79 | [ 80 | "John", 81 | 29, 82 | false, 83 | { 84 | "tool": "HTTPie" 85 | } 86 | ] 87 | ``` 88 | 89 | ## Nested JSON 90 | 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: 91 | 92 | ```shell 93 | $ clon \ 94 | platform[name]=HTTPie \ 95 | platform[about][mission]='Make APIs simple and intuitive' \ 96 | platform[about][homepage]=httpie.io \ 97 | platform[about][stars]:=54000 \ 98 | platform[apps][]=Terminal \ 99 | platform[apps][]=Desktop \ 100 | platform[apps][]=Web \ 101 | platform[apps][]=Mobile 102 | ``` 103 | ```json 104 | { 105 | "platform": { 106 | "name": "HTTPie", 107 | "about": { 108 | "mission": "Make APIs simple and intuitive", 109 | "homepage": "httpie.io", 110 | "stars": 54000 111 | }, 112 | "apps": [ 113 | "Terminal", 114 | "Desktop", 115 | "Web", 116 | "Mobile" 117 | ] 118 | } 119 | } 120 | ``` 121 | 122 | ### Basic Usage 123 | 124 | Let’s start with a simple example, and build a simple search query: 125 | 126 | ```shell 127 | $ clon \ 128 | category=tools \ 129 | search[type]=id \ 130 | search[id]:=1 131 | ``` 132 | ```json 133 | { 134 | "category": "tools", 135 | "search": { 136 | "id": 1, 137 | "type": "id" 138 | } 139 | } 140 | ``` 141 | 142 | 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"`). 143 | 144 | 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). 145 | 146 | 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. 147 | 148 | ```shell 149 | $ clon \ 150 | category=tools \ 151 | search[type]=keyword \ 152 | search[keywords][]=APIs \ 153 | search[keywords][]=CLI 154 | ``` 155 | ```json 156 | { 157 | "category": "tools", 158 | "search": { 159 | "keywords": [ 160 | "APIs", 161 | "CLI" 162 | ], 163 | "type": "keyword" 164 | } 165 | } 166 | ``` 167 | 168 | If you want to explicitly specify the position of elements inside an array, you can simply pass the desired index as the path: 169 | 170 | ```shell 171 | $ clon \ 172 | category=tools \ 173 | search[type]=keyword \ 174 | search[keywords][1]=APIs \ 175 | search[keywords][0]=CLI 176 | ``` 177 | ```json 178 | { 179 | "category": "tools", 180 | "search": { 181 | "keywords": [ 182 | "CLIs", 183 | "API" 184 | ], 185 | "type": "keyword" 186 | } 187 | } 188 | ``` 189 | 190 | If there are any missing indexes, they will be nullified in order to create a concrete object: 191 | 192 | ```shell 193 | $ clon \ 194 | category=tools \ 195 | search[type]=platforms \ 196 | search[platforms][]=Terminal \ 197 | search[platforms][1]=Desktop \ 198 | search[platforms][3]=Mobile 199 | ``` 200 | ```json 201 | { 202 | "category": "tools", 203 | "search": { 204 | "platforms": [ 205 | "Terminal", 206 | "Desktop", 207 | null, 208 | "Mobile" 209 | ], 210 | "type": "platforms" 211 | } 212 | } 213 | ``` 214 | 215 | It is also possible to embed raw JSON to a nested structure, for example: 216 | 217 | ```shell 218 | $ clon \ 219 | category=tools \ 220 | search[type]=platforms \ 221 | search[platforms]:='["Terminal", "Desktop"]' \ 222 | search[platforms][]=Web \ 223 | search[platforms][]=Mobile 224 | ``` 225 | ```json 226 | { 227 | "category": "tools", 228 | "search": { 229 | "platforms": [ 230 | "Terminal", 231 | "Desktop", 232 | "Web", 233 | "Mobile" 234 | ], 235 | "type": "platforms" 236 | } 237 | } 238 | ``` 239 | 240 | And just to demonstrate all of these features together, let’s create a very deeply nested JSON object: 241 | 242 | ```shell 243 | $ clon \ 244 | shallow=value \ # Shallow key-value pair 245 | object[key]=value \ # Nested key-value pair 246 | array[]:=1 \ # Array — first item 247 | array[1]:=2 \ # Array — second item 248 | array[2]:=3 \ # Array — append (third item) 249 | very[nested][json][3][httpie][power][]=Amaze # Nested object 250 | ``` 251 | 252 | ### Advanced Usage 253 | 254 | #### Top level arrays 255 | 256 | To build an array instead of a regular object, you can simply do that by omitting the starting key: 257 | 258 | ```shell 259 | $ clon \ 260 | []:=1 \ 261 | []:=2 \ 262 | []:=3 263 | ``` 264 | ```json 265 | [ 266 | 1, 267 | 2, 268 | 3 269 | ] 270 | ``` 271 | 272 | 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. 273 | 274 | ```shell 275 | $ clon :1 :2 :3 valid:=true :'[4, 5, 6]' 276 | ``` 277 | ```json 278 | [ 279 | 1, 280 | 2, 281 | 3, 282 | { 283 | "valid": true 284 | }, 285 | [ 286 | 4, 287 | 5, 288 | 6 289 | ] 290 | ] 291 | ``` 292 | 293 | For more complex top level elements, you can use the standard notation to apply nesting to the items by referencing their index: 294 | 295 | ```shell 296 | $ clon \ 297 | [0][type]=platform [0][name]=terminal \ 298 | [1][type]=platform [1][name]=desktop 299 | ``` 300 | ```json 301 | [ 302 | { 303 | "type": "platform", 304 | "name": "terminal" 305 | }, 306 | { 307 | "type": "platform", 308 | "name": "desktop" 309 | } 310 | ] 311 | ``` 312 | 313 | #### Escaping behavior 314 | Nested JSON syntax uses the same escaping rules as the terminal. There are 3 special characters, and 1 special token that you can escape. 315 | 316 | To include a bracket as is, escape it with a backslash (`\`): 317 | 318 | ```shell 319 | $ clon \ 320 | 'foo\[bar\]:=1' \ 321 | 'baz[\[]:=2' \ 322 | 'baz[\]]:=3' 323 | ``` 324 | ```json 325 | { 326 | "baz": { 327 | "[": 2, 328 | "]": 3 329 | }, 330 | "foo[bar]": 1 331 | } 332 | ``` 333 | 334 | If use the literal backslash character (`\`), escape it with another backslash: 335 | 336 | ```shell 337 | $ clon \ 338 | 'backslash[\\]:=1' 339 | ``` 340 | ```json 341 | { 342 | "backslash": { 343 | "\\": 1 344 | } 345 | } 346 | ``` 347 | 348 | 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. 349 | 350 | ```shell 351 | $ clon \ 352 | 'object[\1]=stringified' \ 353 | 'object[\100]=same' \ 354 | 'array[1]=indexified' 355 | ``` 356 | ```json 357 | { 358 | "array": [ 359 | null, 360 | "indexified" 361 | ], 362 | "object": { 363 | "1": "stringified", 364 | "100": "same" 365 | } 366 | } 367 | ``` 368 | 369 | #### Guiding syntax errors 370 | 371 | If you make a typo or forget to close a bracket, the errors SHOULD guide you to fix it. For example: 372 | 373 | ``` 374 | $ clon \ 375 | 'foo[bar]=OK' \ 376 | 'foo[baz][quux=FAIL' 377 | Syntax Error: Expecting ']' 378 | foo[baz][quux 379 | ^ 380 | ``` 381 | 382 | You can follow to given instruction (adding a `]`) and repair your expression. 383 | 384 | #### Type safety 385 | 386 | 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: 387 | 388 | ``` 389 | $ clon \ 390 | 'array[]:=1' \ 391 | 'array[]:=2' \ 392 | 'array[key]:=3' 393 | Type Error: Can't perform 'key' based access on 'array' which has a type of 'array' but this operation requires a type of 'object'. 394 | array[key] 395 | ^^^^^ 396 | ``` 397 | 398 | Type Safety does not apply to value overrides, for example: 399 | 400 | ```shell 401 | $ clon \ 402 | user[name]:=411 # Defined as an integer 403 | user[name]=string # Overridden with a string 404 | ``` 405 | ```json 406 | { 407 | "user": { 408 | "name": "string" 409 | } 410 | } 411 | ``` 412 | 413 | ## Raw JSON 414 | 415 | For very complex JSON structures, it may be more convenient to pass it as raw input, for example: 416 | 417 | ```shell 418 | $ echo -n '{"hello": "world"}' | clon 419 | ``` 420 | 421 | ```shell 422 | $ clon < files/data.json 423 | ``` 424 | 425 | --------------------------------------------------------------------------------