Skip to content

Commit

Permalink
feat: add multi-document YAML support and improve documentation
Browse files Browse the repository at this point in the history
                    - Add support for multiple YAML documents in a single file
                    - Add support for invalid yaml
                    - Add test cases for multi-document YAML handling
                    - Add test cases for invalid yaml (duplicared keys)
                    - Fix test expectations for quoted values
                    - Update README.md with forceQuotes option documentation

Signed-off-by: Romain LABAT <[email protected]>
  • Loading branch information
Romain7495 authored and ddebin committed Dec 9, 2024
1 parent 5d81b40 commit 334b6c0
Show file tree
Hide file tree
Showing 7 changed files with 182 additions and 66 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ Options:
--indent, --id Indentation width (in spaces) [number] [default: 2]
-e, --encoding Input encoding [choices: "ascii", "utf8", "utf16le"] [default: "utf8"]
-q, --quotingStyle Strings will be quoted using this quoting style [choices: "single", "double"] [default: "single"]
-f, --forceQuotes Force quotes for all scalar values [boolean] [default: false]
-w, --lineWidth Wrap line width (-1 for unlimited width) [number] [default: 80]
-h, --help Show help [boolean]
--version Show version number [boolean]
Expand All @@ -34,5 +35,6 @@ Examples:
yaml-sort --input config.yml Sorts alphabetically and overwrites the file config.yml
yaml-sort --input config.yml --lineWidth 100 --stdout Sorts the file config.yml and output result to STDOUT wrapped to 100 columns
yaml-sort --input config.yml --indent 4 --output sorted.yml Indents with 4 spaces and outputs result to file sorted.yml
yaml-sort --input config.yml --forceQuotes --quotingStyle double Forces double quotes for all scalar values
cat config.yml | yaml-sort Sorts alphabetically from STDIN
```
6 changes: 6 additions & 0 deletions test/test-duplicate-keys.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
a: first value
b: value
a: second value # Duplicate key
c:
d: value1
d: value2 # Duplicate key
9 changes: 9 additions & 0 deletions test/test-multiple-sorted.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
a: first document
b: 1
c:
d: true
---
x: second document
'y':
z: false
9 changes: 9 additions & 0 deletions test/test-multiple-unsorted.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
c:
d: true
b: 1
a: first document
---
y:
z: false
x: second document
9 changes: 9 additions & 0 deletions test/test-multiple.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
c:
d: true
b: 1
a: first document
---
y:
z: false
x: second document
185 changes: 127 additions & 58 deletions test/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,10 @@ test('CLI w/o arg (STDIN)', (t) => {
const proc = spawn(t, 'cat test.yml | ../yaml-sort.js', opts)
proc.exitCode(0)
proc.stdout.match('a: Lorem ipsum dolor sit amet, consectetur adipiscing elit...\n' +
'b:\n' +
' b: 35\n' +
' c:\n' +
' d: false\n')
'b:\n' +
' b: 35\n' +
' c:\n' +
' d: false\n')
proc.stderr.match('')
proc.end()
})
Expand All @@ -37,10 +37,10 @@ test('CLI w/ arg', (t) => {
const proc = spawn(t, '../yaml-sort.js --input test.yml --stdout', opts)
proc.exitCode(0)
proc.stdout.match('a: Lorem ipsum dolor sit amet, consectetur adipiscing elit...\n' +
'b:\n' +
' b: 35\n' +
' c:\n' +
' d: false\n')
'b:\n' +
' b: 35\n' +
' c:\n' +
' d: false\n')
proc.stderr.match('')
proc.end()
})
Expand All @@ -49,13 +49,13 @@ test('CLI quoting style single', (t) => {
const proc = spawn(t, '../yaml-sort.js --input test-edges.yml --stdout --quotingStyle single', opts)
proc.exitCode(0)
proc.stdout.match('a: Lorem ipsum dolor sit amet, consectetur adipiscing elit...\n' +
'b:\n' +
' b: 35\n' +
' c:\n' +
' a: \'hello: "john"\'\n' +
' d: false\n' +
' e: \'"foo"\'\n' +
' f: \'\'\'foo\'\'\'\n')
'b:\n' +
' b: 35\n' +
' c:\n' +
' a: \'hello: "john"\'\n' +
' d: false\n' +
' e: \'"foo"\'\n' +
' f: \'\'\'foo\'\'\'\n')
proc.stderr.match('')
proc.end()
})
Expand All @@ -64,28 +64,28 @@ test('CLI quoting style double', (t) => {
const proc = spawn(t, '../yaml-sort.js --input test-edges.yml --stdout --quotingStyle double', opts)
proc.exitCode(0)
proc.stdout.match('a: Lorem ipsum dolor sit amet, consectetur adipiscing elit...\n' +
'b:\n' +
' b: 35\n' +
' c:\n' +
' a: "hello: \\"john\\""\n' +
' d: false\n' +
' e: "\\"foo\\""\n' +
' f: "\'foo\'"\n')
'b:\n' +
' b: 35\n' +
' c:\n' +
' a: "hello: \\"john\\""\n' +
' d: false\n' +
' e: "\\"foo\\""\n' +
' f: "\'foo\'"\n')
proc.stderr.match('')
proc.end()
})

test('CLI --output', (t) => {
const proc = spawn(t,
'../yaml-sort.js --input test.yml --output output.yml' +
' && cat output.yml' +
' && rm -f output.yml', opts)
' && cat output.yml' +
' && rm -f output.yml', opts)
proc.exitCode(0)
proc.stdout.match('a: Lorem ipsum dolor sit amet, consectetur adipiscing elit...\n' +
'b:\n' +
' b: 35\n' +
' c:\n' +
' d: false\n')
'b:\n' +
' b: 35\n' +
' c:\n' +
' d: false\n')
proc.stderr.match('')
proc.end()
})
Expand All @@ -94,24 +94,24 @@ test('CLI --output -', (t) => {
const proc = spawn(t, '../yaml-sort.js --input test.yml --output -', opts)
proc.exitCode(0)
proc.stdout.match('a: Lorem ipsum dolor sit amet, consectetur adipiscing elit...\n' +
'b:\n' +
' b: 35\n' +
' c:\n' +
' d: false\n')
'b:\n' +
' b: 35\n' +
' c:\n' +
' d: false\n')
proc.stderr.match('')
proc.end()
})
test('CLI --output (STDIN)', (t) => {
const proc = spawn(t,
'cat test.yml | ../yaml-sort.js --input - --output output.yml' +
' && cat output.yml' +
' && rm -f output.yml', opts)
' && cat output.yml' +
' && rm -f output.yml', opts)
proc.exitCode(0)
proc.stdout.match('a: Lorem ipsum dolor sit amet, consectetur adipiscing elit...\n' +
'b:\n' +
' b: 35\n' +
' c:\n' +
' d: false\n')
'b:\n' +
' b: 35\n' +
' c:\n' +
' d: false\n')
proc.stderr.match('')
proc.end()
})
Expand All @@ -120,10 +120,10 @@ test('CLI (STDIN) (STDOUT)', (t) => {
const proc = spawn(t, 'cat test.yml | ../yaml-sort.js', opts)
proc.exitCode(0)
proc.stdout.match('a: Lorem ipsum dolor sit amet, consectetur adipiscing elit...\n' +
'b:\n' +
' b: 35\n' +
' c:\n' +
' d: false\n')
'b:\n' +
' b: 35\n' +
' c:\n' +
' d: false\n')
proc.stderr.match('')
proc.end()
})
Expand All @@ -132,10 +132,10 @@ test('CLI --indent', (t) => {
const proc = spawn(t, '../yaml-sort.js --input test.yml --stdout --indent 4', opts)
proc.exitCode(0)
proc.stdout.match('a: Lorem ipsum dolor sit amet, consectetur adipiscing elit...\n' +
'b:\n' +
' b: 35\n' +
' c:\n' +
' d: false\n')
'b:\n' +
' b: 35\n' +
' c:\n' +
' d: false\n')
proc.stderr.match('')
proc.end()
})
Expand All @@ -144,12 +144,12 @@ test('CLI --lineWidth', (t) => {
const proc = spawn(t, '../yaml-sort.js --input test.yml --stdout --lineWidth 40', opts)
proc.exitCode(0)
proc.stdout.match('a: >-\n' +
' Lorem ipsum dolor sit amet, consectetur\n' +
' adipiscing elit...\n' +
'b:\n' +
' b: 35\n' +
' c:\n' +
' d: false\n')
' Lorem ipsum dolor sit amet, consectetur\n' +
' adipiscing elit...\n' +
'b:\n' +
' b: 35\n' +
' c:\n' +
' d: false\n')
proc.stderr.match('')
proc.end()
})
Expand All @@ -158,10 +158,10 @@ test('CLI --lineWidth unlimited', (t) => {
const proc = spawn(t, '../yaml-sort.js --input test-long-line.yml --stdout --lineWidth -1', opts)
proc.exitCode(0)
proc.stdout.match('a: Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam nec volutpat mi, ac consectetur est. Proin venenatis tortor ut erat interdum finibus.\n' +
'b:\n' +
' b: 35\n' +
' c:\n' +
' d: false\n')
'b:\n' +
' b: 35\n' +
' c:\n' +
' d: false\n')
proc.stderr.match('')
proc.end()
})
Expand All @@ -184,6 +184,23 @@ test('CLI --check SUCCESS', (t) => {
proc.end()
})

test('CLI --check invalid YAML (duplicate keys) FAIL', (t) => {
const proc = spawn(t, '../yaml-sort.js --input test-duplicate-keys.yml --check', opts)
proc.exitCode(1)
proc.stdout.match('')
proc.stderr.match(/duplicated mapping key/)

proc.end()
})

test('CLI --check valid YAML SUCCESS', (t) => {
const proc = spawn(t, '../yaml-sort.js --input sorted.yml --check', opts)
proc.exitCode(0)
proc.stdout.match('')
proc.stderr.match('')
proc.end()
})

test('CLI --encoding FAIL', (t) => {
const proc = spawn(t,
'../yaml-sort.js --input test-utf16le.yml --stdout', opts)
Expand All @@ -204,8 +221,8 @@ test('CLI --encoding SUCCESS', (t) => {
test('CLI --lineWidth SUCCESS', (t) => {
const proc = spawn(t,
'../yaml-sort.js --input test.yml --output output.yml' +
' && ../yaml-sort.js --input output.yml --check' +
' && rm -f output.yml', opts)
' && ../yaml-sort.js --input output.yml --check' +
' && rm -f output.yml', opts)
proc.exitCode(0)
proc.stdout.match('')
proc.stderr.match('')
Expand All @@ -229,3 +246,55 @@ test('CLI --check --stdout FAIL', (t) => {
proc.stderr.match(/Arguments check and stdout are mutually exclusive/)
proc.end()
})

test('CLI multiple YAML documents with single quotes', (t) => {
const proc = spawn(t, '../yaml-sort.js --input test-multiple.yml --stdout --quotingStyle single --forceQuotes', opts)
proc.exitCode(0)
proc.stdout.match(
'---\n' +
'a: \'first document\'\n' +
'b: 1\n' +
'c:\n' +
' d: true\n' +
'---\n' +
'x: \'second document\'\n' +
'\'y\':\n' +
' z: false\n'
)
proc.stderr.match('')
proc.end()
})

test('CLI multiple YAML documents with double quotes', (t) => {
const proc = spawn(t, '../yaml-sort.js --input test-multiple.yml --stdout --quotingStyle double --forceQuotes', opts)
proc.exitCode(0)
proc.stdout.match(
'---\n' +
'a: "first document"\n' +
'b: 1\n' +
'c:\n' +
' d: true\n' +
'---\n' +
'x: "second document"\n' +
'"y":\n' +
' z: false\n'
)
proc.stderr.match('')
proc.end()
})

test('CLI multiple YAML documents --check FAIL', (t) => {
const proc = spawn(t, '../yaml-sort.js --input test-multiple-unsorted.yml --check --quotingStyle single --forceQuotes', opts)
proc.exitCode(1)
proc.stdout.match('')
proc.stderr.match('\'test-multiple-unsorted.yml\' is not sorted and/or formatted (indent, line width).\n')
proc.end()
})

test('CLI multiple YAML documents --check SUCCESS', (t) => {
const proc = spawn(t, '../yaml-sort.js --input test-multiple-sorted.yml --check', opts)
proc.exitCode(0)
proc.stdout.match('')
proc.stderr.match('')
proc.end()
})
28 changes: 20 additions & 8 deletions yaml-sort.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,11 @@ const argv = yargs
describe: 'Strings will be quoted using this quoting style',
choices: ['single', 'double']
})
.option('forceQuotes', {
alias: 'f',
describe: 'Force quotes around all strings',
boolean: true
})
.option('lineWidth', {
alias: 'w',
default: 80,
Expand All @@ -88,28 +93,35 @@ argv.input.forEach((file) => {
}

const output =
argv.stdout || (argv.output === '.') || (isStdin && !argv.output)
? process.stdout.fd
: (argv.output ? argv.output : file)
argv.stdout || (argv.output === '.') || (isStdin && !argv.output)
? process.stdout.fd
: (argv.output ? argv.output : file)

const content = fs.readFileSync(isStdin ? process.stdin.fd : file, argv.encoding)

const sorted = yaml.dump(yaml.load(content), {
const documents = yaml.loadAll(content)

const sortedDocuments = documents.map(doc => yaml.dump(doc, {
sortKeys: true,
indent: argv.indent,
lineWidth: argv.lineWidth,
quotingType: argv.quotingStyle === 'double' ? '"' : "'"
})
quotingType: argv.quotingStyle === 'double' ? '"' : "'",
forceQuotes: argv.forceQuotes
}))

const hasDocumentStart = content.toString().trimStart().startsWith('---')

const sortedContent = (hasDocumentStart ? '---\n' : '') + sortedDocuments.join('---\n')

if (argv.check) {
if (sorted !== content.toString()) {
if (sortedContent !== content.toString()) {
success = false
console.warn(`'${file}' is not sorted and/or formatted (indent, line width).`)
}
} else {
fs.writeFile(
output,
sorted,
sortedContent,
(error) => {
if (error) {
success = false
Expand Down

0 comments on commit 334b6c0

Please sign in to comment.