UNPKG

@sap/cds-dk

Version:

Command line client and development toolkit for the SAP Cloud Application Programming Model

133 lines (115 loc) 4.5 kB
const { parseArgs } = require ('node:util') const path = require ('node:path') const fs = require ('node:fs') class Command { static filename = __filename // to be overridden in subclasses get class(){ return this.constructor } get name(){ return this.class.name } /** @type <T>(defaults: T) => [ string[], T ] */ parseArgs (conf) { const args = conf.args || process.argv.slice(2) const shorts = { ...this.class.shortcuts, ...conf.shortcuts } const options = { ...this.class.options, ...conf.options, help:false } for (let k in options) options[k] = option4 (options[k]) .short (shorts[k]||k[0]) // parse the CLI args with node:util.parseArgs const { positionals:p, values:o } = parseArgs ({ allowPositionals: true, strict: false, ...conf, args, options, }) this.positionals = p this.options = o // convert the parsed values according to the options for (let k in o) { const v = options[k]; if (!v) continue if (v.convert) o[k] = v.convert (o[k],o) } // help / debug if (o.help || args == '?' || !args.length) throw this.help() if (o.debug) throw this.debug() // parsed return [ this.positionals, this.options ] } usage() { const source = this.class.filename; if (!source) return super.usage = undefined const { dir, name } = path.parse (source) const md = path.join(dir,'help',name+'.md') return super.usage = fs.readFileSync (md,'utf8') } help (usage = this.usage()) { console.log (usage .replace(/\n# ([^\n]*)\n/g, `\n\x1b[1m$1\x1b[0m\n`) .replace(/ \*([^*]+)\*/g, ` \x1b[1m$1\x1b[0m`) .replace(/ _([^_]+)_/g, ` \x1b[4m$1\x1b[0m`) ) process.exit(0) } debug (positionals = this.positionals, o = this.options) { console.log('Debugging information:') const shortcuts = {}, options = {...o}; delete options.o for (let o in options) shortcuts[this.class.shortcuts[o] || o[0]] = o console.log ({ positionals, options, shortcuts }) process.exit(0) } static _for_cds_dk (cmd = this) { return { _is_new_cmd: true, options: Object.keys(this.options).map(o => '--'+o), // for shell completion exec() { return (new cmd) .exec (process.argv.slice(3)) }, get help() { return (new cmd).usage() }, } } } class Option { constructor (spec) { Object.assign (this,spec) } convert (x) { return x } to(fn) { this.convert = fn; return this } short(sc) { this.short = sc; return this } get array() { this.multiple = true; return this } } class Flags extends Option { constructor (defaults) { super({ type: 'string', multiple: true, default: [], defaults, }) if (defaults._excluding) { this.skip = defaults._excluding delete defaults._excluding } } convert (strings,o) { const flags = { ...this.defaults } const keep = strings4(strings).reduce?.((o,x)=>(o[x]=true,o),{}) || strings if (keep.all) for (let x in this.defaults) flags[x] = true if (this.skip) { const skip = o[this.skip].reduce?.((o,x)=>(o[x]=true,o),{}) || o[this.skip] if (skip.all) for (let x in this.defaults) delete flags[x] else for (let x in skip) delete flags[x] } for (let x in keep) flags[x] = keep[x] return flags } } const option4 = exports.option4 = (v) => { if (v instanceof Option) return v if (v instanceof RegExp) return pattern(v) if (v instanceof Array) return strings(v) if (v == null) return string('') if (typeof v === 'string') return string(v) if (typeof v === 'object') return flags(v) if (typeof v === 'boolean') return flag(v) if (typeof v === 'number') return number(v) } const option = exports.option = (x) => new Option (x) const number = exports.number = d => string (String(d)).to(Number) const pattern = exports.pattern = d => string (d.source !== '.' ? [d.source] : []) .array.to (regex4) const strings = exports.strings = d => string (d.length ? d : undefined) .array.to (strings4) const string = exports.string = d => option ({ type:'string', default:d }) const flag = exports.flag = d => option ({ type:'boolean', default:d }) const flags = exports.flags = (d,x) => new Flags (d,x) const regex4 = exports.regex4 = raw => RegExp (strings4(raw).join('|') .replaceAll ('.','\\.') .replaceAll ('*','.*'), 'i') const strings4 = exports.strings4 = raw => raw.length === 1 ? raw[0].split?.(',')||'' : raw exports.Command = Command