// 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. // Thin wrapper over `node:util.parseArgs` for the apache-arrow bin scripts. // Replaces `command-line-args` + `command-line-usage` to drop two runtime deps // (and their transitive trees) from the published package. import { parseArgs, type ParseArgsConfig } from 'node:util'; export interface OptionSpec { name: string; alias?: string; type: StringConstructor | BooleanConstructor | NumberConstructor; multiple?: boolean; defaultValue?: unknown; description?: string; typeLabel?: string; // Accepted for source-compat with the previous command-line-args specs; ignored. optional?: boolean; default?: unknown; } export function parseCliArgs(spec: OptionSpec[], args: string[]) { const options: NonNullable = {}; for (const o of spec) { options[o.name] = { type: o.type === Boolean ? 'boolean' : 'string', ...(o.alias && { short: o.alias }), ...(o.multiple && { multiple: true }), ...(o.defaultValue !== undefined && { default: o.defaultValue as never }), }; } const { values, tokens } = parseArgs({ options, args, strict: false, allowPositionals: true, tokens: true, }); // `parseArgs` only honours repeated multi-flags (`-s a -s b`). To match // command-line-args' greedy behaviour (`-s a b c`), walk the token stream // and route positionals to the most recently seen `multiple: true` flag // until another option appears. const multi = new Set(spec.filter((o) => o.multiple).map((o) => o.name)); const out = values as Record; const positionals: string[] = []; let owner: string | null = null; for (const tok of tokens) { if (tok.kind === 'option') { owner = multi.has(tok.name) ? tok.name : null; } else if (tok.kind === 'option-terminator') { owner = null; } else if (tok.kind === 'positional') { if (owner) { const list = (out[owner] as string[] | undefined) ?? []; out[owner] = [...list, tok.value]; } else { positionals.push(tok.value); } } } // Coerce Number-typed values; parseArgs only parses as string or boolean. for (const o of spec) { if (o.type !== Number) continue; const v = out[o.name]; if (Array.isArray(v)) out[o.name] = v.map(Number); else if (typeof v === 'string') out[o.name] = Number(v); } return { values: out, positionals }; } export interface UsageSection { header: string; content?: string | string[]; optionList?: OptionSpec[]; } // Drops the {bold ...} / {underline ...} chalk markup that command-line-usage // recognised, so existing call sites can keep their content strings unchanged. const stripStyles = (s: string) => s.replaceAll(/\{(?:bold|underline)\s+(.*?)\}/g, '$1'); // Match command-line-usage's option-line layout: `-s, --schema columns` // (uses the explicit `typeLabel`) or `-f, --file string[]` (synthesised when // no `typeLabel` was provided for a string option). Boolean options get no // trailing label. Options without an `alias` are left-flush; aliased ones get // the `-x, ` prefix. const formatOptionHead = (o: OptionSpec) => { const flag = `${o.alias ? `-${o.alias}, --` : '--'}${o.name}`; if (o.typeLabel) return `${flag} ${stripStyles(o.typeLabel)}`; if (o.type === Boolean) return flag; return `${flag} ${o.multiple ? 'string[]' : 'string'}`; }; export function formatUsage(sections: UsageSection[]): string { const out: string[] = []; for (const s of sections) { out.push('', stripStyles(s.header), ''); const content = typeof s.content === 'string' ? [s.content] : s.content ?? []; for (const line of content) out.push(' ' + stripStyles(line)); if (s.optionList?.length) { const heads = s.optionList.map((o) => formatOptionHead(o)); const width = Math.max(...heads.map((h) => h.length)); for (const [i, o] of s.optionList.entries()) { out.push(' ' + heads[i].padEnd(width + 3) + (o.description ?? '')); } } } out.push(''); return out.join('\n'); }