blob: 0104ec98a4b5980edfa0266ea65c1f15c85c3731 [file]
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
import { parseCliArgs, formatUsage, OptionSpec } from '../../../src/bin/cli.js';
describe(`parseCliArgs`, () => {
const spec: OptionSpec[] = [
{ name: 'schema', alias: 's', type: String, multiple: true },
{ name: 'file', alias: 'f', type: String, multiple: true },
{ name: 'sep', type: String, defaultValue: ' | ' },
{ name: 'count', type: Number },
{ name: 'metadata', alias: 'm', type: Boolean },
{ name: 'help', type: Boolean },
];
test('parses a boolean long flag', () => {
const r = parseCliArgs(spec, ['--help']);
expect(r.values.help).toBe(true);
expect(r.positionals).toEqual([]);
});
test('parses a short alias', () => {
const r = parseCliArgs(spec, ['-m']);
expect(r.values.metadata).toBe(true);
});
test('parses repeated multi-flag', () => {
const r = parseCliArgs(spec, ['-s', 'a', '-s', 'b', '-s', 'c']);
expect(r.values.schema).toEqual(['a', 'b', 'c']);
expect(r.positionals).toEqual([]);
});
test('parses space-delimited multi-flag (greedy)', () => {
const r = parseCliArgs(spec, ['-s', 'a', 'b', 'c']);
expect(r.values.schema).toEqual(['a', 'b', 'c']);
expect(r.positionals).toEqual([]);
});
test('greedy attribution stops at the next flag', () => {
const r = parseCliArgs(spec, ['-s', 'a', 'b', 'c', '-f', 'x.arrow']);
expect(r.values.schema).toEqual(['a', 'b', 'c']);
expect(r.values.file).toEqual(['x.arrow']);
expect(r.positionals).toEqual([]);
});
test('greedy attribution spans across multi-flags', () => {
const r = parseCliArgs(spec, ['-s', 'a', 'b', 'c', '-f', 'x.arrow', 'pos.arrow']);
expect(r.values.schema).toEqual(['a', 'b', 'c']);
expect(r.values.file).toEqual(['x.arrow', 'pos.arrow']);
expect(r.positionals).toEqual([]);
});
test('positionals with no preceding multi-flag stay in positionals', () => {
const r = parseCliArgs(spec, ['file1.arrow', 'file2.arrow']);
expect(r.values.schema).toBeUndefined();
expect(r.positionals).toEqual(['file1.arrow', 'file2.arrow']);
});
test('default values are populated when flag is absent', () => {
const r = parseCliArgs(spec, []);
expect(r.values.sep).toBe(' | ');
});
test('user-supplied value overrides default', () => {
const r = parseCliArgs(spec, ['--sep', ' , ']);
expect(r.values.sep).toBe(' , ');
});
test('unknown flags do not throw, and break greedy attribution', () => {
const r = parseCliArgs(spec, ['-s', 'a', '--unknown', 'b', '-f', 'x.arrow']);
expect(r.values.schema).toEqual(['a']);
expect(r.values.unknown).toBe(true);
expect(r.positionals).toEqual(['b']);
expect(r.values.file).toEqual(['x.arrow']);
});
test('Number type is coerced from string', () => {
const r = parseCliArgs(spec, ['--count', '42']);
expect(r.values.count).toBe(42);
expect(typeof r.values.count).toBe('number');
});
test('combined: typical arrow2csv invocation', () => {
const r = parseCliArgs(spec, ['-s', 'foo', 'bar', '-f', 'simple.arrow', '-m']);
expect(r.values.schema).toEqual(['foo', 'bar']);
expect(r.values.file).toEqual(['simple.arrow']);
expect(r.values.metadata).toBe(true);
});
});
describe(`formatUsage`, () => {
test('renders header and content sections', () => {
const out = formatUsage([
{ header: 'arrow2csv', content: 'Print a CSV from an Arrow file' },
]);
expect(out).toContain('arrow2csv');
expect(out).toContain('Print a CSV from an Arrow file');
});
test('strips {bold ...} and {underline ...} markup', () => {
const out = formatUsage([
{ header: 'Synopsis', content: ['$ arrow2csv {bold -s} col1 {underline file.arrow}'] },
]);
expect(out).toContain('$ arrow2csv -s col1 file.arrow');
expect(out).not.toMatch(/\{bold/);
expect(out).not.toMatch(/\{underline/);
});
test('renders an option list with aliases and descriptions', () => {
const out = formatUsage([
{
header: 'Options',
optionList: [
{ name: 'schema', alias: 's', type: String, multiple: true, description: 'Column names' },
{ name: 'help', type: Boolean, description: 'Print this usage guide.' },
],
},
]);
expect(out).toContain('-s, --schema');
expect(out).toContain('Column names');
expect(out).toContain('--help');
expect(out).toContain('Print this usage guide.');
});
test('option list renders correctly when no options have aliases', () => {
const out = formatUsage([
{
header: 'Options',
optionList: [
{ name: 'mode', type: String, description: 'The mode' },
],
},
]);
expect(out).toContain('--mode');
expect(out).toContain('The mode');
});
test('renders type labels and synthesised type hints', () => {
const out = formatUsage([
{
header: 'Options',
optionList: [
{ name: 'schema', alias: 's', type: String, multiple: true, typeLabel: '{underline columns}', description: 'Column names' },
{ name: 'file', alias: 'f', type: String, multiple: true, description: 'The Arrow file to read' },
{ name: 'sep', type: String, description: 'Column separator' },
{ name: 'metadata', alias: 'm', type: Boolean, description: 'Print metadata' },
],
},
]);
expect(out).toContain('-s, --schema columns');
expect(out).toContain('-f, --file string[]');
expect(out).toContain('--sep string');
// Boolean options get no type label
expect(out).toMatch(/-m, --metadata\s+Print metadata/);
expect(out).not.toMatch(/--metadata\s+(string|boolean)/);
});
});