@sap/cds-dk
Version:
Command line client and development toolkit for the SAP Cloud Application Programming Model
133 lines (115 loc) • 4.5 kB
JavaScript
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