diff --git a/README.md b/README.md index 94136b3..e612599 100644 --- a/README.md +++ b/README.md @@ -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] @@ -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 ``` \ No newline at end of file diff --git a/test/test-duplicate-keys.yml b/test/test-duplicate-keys.yml new file mode 100644 index 0000000..8b02034 --- /dev/null +++ b/test/test-duplicate-keys.yml @@ -0,0 +1,6 @@ +a: first value +b: value +a: second value # Duplicate key +c: + d: value1 + d: value2 # Duplicate key \ No newline at end of file diff --git a/test/test-multiple-sorted.yml b/test/test-multiple-sorted.yml new file mode 100644 index 0000000..4dff5c0 --- /dev/null +++ b/test/test-multiple-sorted.yml @@ -0,0 +1,9 @@ +--- +a: first document +b: 1 +c: + d: true +--- +x: second document +'y': + z: false diff --git a/test/test-multiple-unsorted.yml b/test/test-multiple-unsorted.yml new file mode 100644 index 0000000..228d8fa --- /dev/null +++ b/test/test-multiple-unsorted.yml @@ -0,0 +1,9 @@ +--- +c: + d: true +b: 1 +a: first document +--- +y: + z: false +x: second document diff --git a/test/test-multiple.yml b/test/test-multiple.yml new file mode 100644 index 0000000..44d55c8 --- /dev/null +++ b/test/test-multiple.yml @@ -0,0 +1,9 @@ +--- +c: + d: true +b: 1 +a: first document +--- +y: + z: false +x: second document \ No newline at end of file diff --git a/test/test.js b/test/test.js index ceeacde..8417d6f 100755 --- a/test/test.js +++ b/test/test.js @@ -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() }) @@ -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() }) @@ -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() }) @@ -64,13 +64,13 @@ 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() }) @@ -78,14 +78,14 @@ test('CLI quoting style double', (t) => { 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() }) @@ -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() }) @@ -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() }) @@ -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() }) @@ -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() }) @@ -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() }) @@ -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) @@ -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('') @@ -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() +}) diff --git a/yaml-sort.js b/yaml-sort.js index 3de8941..e87f3d0 100755 --- a/yaml-sort.js +++ b/yaml-sort.js @@ -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, @@ -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