Repository: eemeli/yaml
Pull request #559 (2024-06-30 13:43:36)

feat: Add --indent option to CLI tool
Add a `--indent` option, to be used with `--json` to output 2 space indentation.

See also #479 and #523.

eemeli: > > 1. Could you clarify what's the use case of pretty-printed JSON output? For single-shot manual use, wouldn't the same be served by piping the output to a separate JSON prettifier like `jq`?
> 
> _Non_ single-shot manual use… Like in `package.json` ([`package.yaml`](https://github.com/pnpm/pnpm/issues/1100)) `scripts`.

Eh, not sure that it really makes sense to prettify output that's meant for machine consumption, as it risks creating JSON files that will be presumed to be the source of truth, rather than intermediate build assets. But I'll grant you that there's probably a valid use case somewhere here.

> > 2. Naming the option as "pretty" and only affecting the JSON output seems rather misleading. Either the option needs to make sense independently of `--json`, or it needs to be named more specifically.
> 
> I had a similar thought, but there is no such thing as `--pretty` YAML, it already has to be indented…
> 
> I think the ideal API would be an optional parameter, maybe to `--json`, e.g `--json 2` or something, but that isn’t really a feature of `util.parseArgs`. How about `--indent`? What other name do you think would be better?

`--indent` works, esp. as it's already an [option](https://eemeli.org/yaml/#tostring-options) for YAML formatting. Switch this PR to that and make it apply to YAML output as well?
danielbayley: > it risks creating JSON files that will be presumed to be the source of truth

Well the YAML files are the source… But a _lot_ of tools only work from JSON. Harder to debug if it _has_ to be minified also…

> `--indent` works, esp. as it's already an [option](https://eemeli.org/yaml/#tostring-options) for YAML formatting. Switch this PR to that and make it apply to YAML output as well?

Good idea, will look into it…

Patch:
diff --git a/docs/09_cli.md b/docs/09_cli.md
index 123cf99..0788bcb 100644
--- a/docs/09_cli.md
+++ b/docs/09_cli.md
@@ -16,6 +16,7 @@ Usage:
 Options:
   --help, -h    Show this message.
   --json, -j    Output JSON.
+  --indent 2    Output pretty-printed data, indented by the given number of spaces.
 
 Additional options for bare "yaml" command:
   --doc, -d     Output pretty-printed JS Document objects.
diff --git a/src/cli.ts b/src/cli.ts
index a05ede8..9d3529d 100644
--- a/src/cli.ts
+++ b/src/cli.ts
@@ -24,6 +24,7 @@ Usage:
 Options:
   --help, -h    Show this message.
   --json, -j    Output JSON.
+  --indent 2    Output pretty-printed data, indented by the given number of spaces.
 
 Additional options for bare "yaml" command:
   --doc, -d     Output pretty-printed JS Document objects.
@@ -55,6 +56,7 @@ export async function cli(
       options: {
         doc: { type: 'boolean', short: 'd' },
         help: { type: 'boolean', short: 'h' },
+        indent: { type: 'string', short: 'i' },
         json: { type: 'boolean', short: 'j' },
         single: { type: 'boolean', short: '1' },
         strict: { type: 'boolean', short: 's' },
@@ -71,6 +73,8 @@ export async function cli(
     values: opt
   } = args
 
+  let indent = Number(opt.indent)
+
   stdin.setEncoding('utf-8')
 
   // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
@@ -92,7 +96,7 @@ export async function cli(
       })
       stdin.on('end', () => {
         for (const tok of lexer.lex('', false)) add(tok)
-        if (opt.json) console.log(JSON.stringify(data))
+        if (opt.json) console.log(JSON.stringify(data, null, indent))
         done()
       })
       break
@@ -110,7 +114,7 @@ export async function cli(
       })
       stdin.on('end', () => {
         for (const tok of parser.parse('', false)) add(tok)
-        if (opt.json) console.log(JSON.stringify(data))
+        if (opt.json) console.log(JSON.stringify(data, null, indent))
         done()
       })
       break
@@ -159,7 +163,8 @@ export async function cli(
         } else {
           if (reqDocEnd) console.log('...')
           try {
-            const str = String(doc)
+            indent ||= 2
+            const str = doc.toString({ indent })
             console.log(str.endsWith('\n') ? str.slice(0, -1) : str)
           } catch (error) {
             done(error as Error)
@@ -188,7 +193,7 @@ export async function cli(
           )
         }
         if (mode !== 'valid' && opt.json) {
-          console.log(JSON.stringify(opt.single ? data[0] : data))
+          console.log(JSON.stringify(opt.single ? data[0] : data, null, indent))
         }
         done()
       })


Test patch:
diff --git a/tests/cli.ts b/tests/cli.ts
index f0da517..f8aaf7b 100644
--- a/tests/cli.ts
+++ b/tests/cli.ts
@@ -114,6 +114,94 @@ const skip = Number(major) < 20
         ['[{"hello":"world"},42]']
       )
     })
+    describe('--indent', () => {
+      ok(
+        'basic',
+        'hello:\n  world: 2',
+        ['--indent', '3'],
+        ['hello:\n   world: 2']
+      )
+      ok(
+        '--json',
+        'hello: world',
+        ['--json', '--indent', '2'],
+        ['[\n  {\n    "hello": "world"\n  }\n]']
+      )
+      ok(
+        '--single',
+        'hello: world',
+        ['--json', '--indent', '2', '--single'],
+        ['{\n  "hello": "world"\n}']
+      )
+      ok(
+        'multiple',
+        'hello: world\n---\n42',
+        ['--json', '--indent', '2'],
+        ['[\n  {\n    "hello": "world"\n  },\n  42\n]']
+      )
+      ok(
+        'Lexer',
+        'hello: world',
+        ['lex', '--json', '--indent', '2'],
+        ['[\n  "\\u0002",\n  "\\u001f",\n  "hello",\n  ":",\n  " ",\n  "\\u001f",\n  "world"\n]']
+      )
+      ok(
+        'CST parser',
+        'hello: world\n',
+        ['cst', '--json', '--indent', '2'],
+        [JSON.stringify([
+          {
+            type: 'document',
+            offset: 0,
+            start: [],
+            value: {
+              type: 'block-map',
+              offset: 0,
+              indent: 0,
+              items: [
+                {
+                  start: [],
+                  key: {
+                    type: 'scalar',
+                    offset: 0,
+                    indent: 0,
+                    source: 'hello'
+                  },
+                  sep: [
+                    {
+                      type: 'map-value-ind',
+                      offset: 5,
+                      indent: 0,
+                      source: ':'
+                    },
+                    {
+                      type: "space",
+                      offset: 6,
+                      indent: 0,
+                      source: ' '
+                    }
+                  ],
+                  value: {
+                    type: 'scalar',
+                    offset: 7,
+                    indent: 0,
+                    source: 'world',
+                    end: [
+                      {
+                        type: 'newline',
+                        offset: 12,
+                        indent: 0,
+                        source: '\n'
+                      }
+                    ]
+                  }
+                }
+              ]
+            }
+          }
+        ], null, 2)]
+      )
+    })
     describe('--doc', () => {
       ok('basic', 'hello: world', ['--doc'], [{ contents: { items: [{}] } }])
       ok(