Ljos
Ljósið -- Light
Description
Ljos aims to be like yargs, but simplified and built around parseArgs.
Installation
TODO: put on npm once stable
npm i ljos
Examples
Simple
const input = "greet Jimmy";
ljos()
.command({
cmd: "greet <name>",
desc: "greet command description",
builder: (ljos) =>
ljos.positional("name", { type: "string", required: true }),
handler: (argv) => {
const { name } = argv;
console.log(`Hey there, ${name}!`);
},
})
.parse(input);
Hey there, Jimmy!
Default command
const input = "Carl";
ljos()
.command({
cmd: "$0 <name>",
desc: "default command description",
builder: (ljos) =>
ljos.positional("name", { type: "string", required: true }),
handler: (argv) => {
const { name } = argv;
console.log(`Hey there, ${name}!`);
},
})
.parse(input);
Hey there, Carl!
Optional
const input = "greet";
ljos()
.command({
cmd: "greet [name]",
desc: "greet command description",
builder: (ljos) => ljos.positional("name", { type: "string" }),
handler: (argv) => {
const { name = "friend" } = argv;
console.log(`Hey there, ${name}!`);
},
})
.parse(input);
Hey there, friend!
Input sources
const inputs = {
"from-process-argv": process.argv.slice(2), // node my-app.js add 3 4
"from-string": "add 2 3",
"from-array": ["add", "5", "6"],
};
const program = ljos()
.command({
cmd: "add <a> <b>",
desc: "add command description",
builder: (ljos) =>
ljos
.positional("a", { type: "number", required: true })
.positional("b", { type: "number", required: true }),
handler: (argv) => {
const { a, b } = argv;
const result = a + b;
console.log(`${a} + ${b} = ${result}`);
},
});
for (const [name, input] of Object.entries(inputs)) {
console.log(name);
program.parse(input);
}
from-process-argv
3 + 4 = 7
from-string
2 + 3 = 5
from-array
5 + 6 = 11
Middleware
const input = "greet patrick star";
function stringToBase64(s) {
return Buffer.from(s, "utf-8").toString("base64");
}
ljos()
.command({
cmd: "greet <first-name> <last-name>",
desc: "greet command description",
builder: (ljos) =>
ljos
.positional("first-name", { type: "string", required: true })
.positional("last-name", { type: "string", required: true }),
handler: (argv) => {
// ljos will provide both kebab- and camel-case versions of args
const { firstName, lastName } = argv;
console.log(`Hey there, ${firstName} ${lastName}!`);
},
// Array of middleware objects with callbacks
// Result of middleware callback will be merged with argv
transforms: [
// Just a callback function
({ firstName }) => ({ firstName: firstName.toUpperCase() }),
// Or an object with additional configuration
{
f: ({ lastName }) => ({ lastName: stringToBase64(lastName) }),
applyBeforeMiddleware: true,
},
],
})
.parse(input);
Hey there, PATRICK c3Rhcg==!
Check
const input = "divide 1 2";
ljos()
.command({
cmd: "divide <a> <b>",
desc: "divide command description",
builder: (ljos) =>
ljos
.positional("a", {
type: "number",
required: true,
desc: "numerator",
})
.positional("b", {
type: "number",
required: true,
desc: "denominator",
})
.check((argv) => {
const { b } = argv;
if (b === 0) {
// Fails validation, shows error messge
throw new Error("Please do not divide by 0");
}
// Passes validation, continues with execution
return;
}),
handler: (argv) => {
const { a, b } = argv;
const result = a / b;
console.log(`${a} / ${b} = ${result}`);
},
})
.parse(input);
1 / 2 = 0.5
Subcommands
const inputs = {
sum: "math sum 1 2 3 4",
product: "math product 1 2 3 4",
};
const program = ljos()
.command({
cmd: "math",
desc: "math command description",
builder: (ljos) =>
ljos
.demandCommand(1)
.command({
cmd: "sum <numbers..>",
desc: "get the sum of numbers",
builder: (ljos) =>
ljos.positional("numbers", {
array: true,
type: "number",
required: true,
}),
handler: (argv) => {
const { numbers } = argv;
const result = numbers.reduce((acc, curr) => acc + curr, 0);
console.log(`The sum of ${numbers} is ${result}`);
},
})
.command({
cmd: "product <numbers..>",
desc: "get the sum of numbers",
builder: (ljos) =>
ljos.positional("numbers", {
array: true,
type: "number",
required: true,
}),
handler: (argv) => {
const { numbers } = argv;
const result = numbers.reduce((acc, curr) => acc * curr, 1);
console.log(`The product of ${numbers} is ${result}`);
},
}),
handler: (argv) => {
const { name } = argv;
console.log(`Hey there, ${name}!`);
},
});
for (const [name, input] of Object.entries(inputs)) {
console.log(name);
program.parse(input);
}
sum
The sum of 1,2,3,4 is 10
product
The product of 1,2,3,4 is 24
Documentation
TODO
Contributing
TODO
Differences from yargs
- no browser or deno
- Deno has node/npm compatibility, not sure if they have util.parseArgs yet
- It might work when installing from npm. I'm not sure
- commands
- command module only:
ljos.cmd({ cmd: str, desc: str, builder: function, handler: function })
- no legacy builder
yargs.command('cmd', 'cmd desc', ljos => ljos, argv => argv)
- command module only:
- middlewares are more specific:
- transform:
- modify argv
- same behavior around errors
- check:
- do not modify argv
- raise errors if conditions aren't met, show usage
- transform:
- options and positionals must be explicitly defined
- explicit options
- unknown options are treated as bool opt and positional
- yargs
- yargs can properly parse bool/string/number without configuration
- config required to figure out greedy arrays, nargs, and count
- ljos
- config is needed for the parser to correctly parse anything
- eg: without config,
--name Ben
is{ name: true, _: ['Ben'] }
- to parse properly, one must use:
.option('name', { type: 'string' })
- eg: without config,
- TODO: this is fundamentally different than yargs
- reconcile difference with pre-processing?
- require explicit def for options? make strict by default?
- config is needed for the parser to correctly parse anything
- explicit positionals
cmd1 <pos1>
will need a correspondingljos.positional('pos1', { /* ... */ })
- explicit options
- no count
- is there a really compelling reason to keep count?
- no nargs
- is this a heavily used feature?
- I'm not opposed to keeping nargs
- implementing it around parseArgs might be difficult
- maybe preprocessing?
- maybe use tokens returned by parseArgs?
- is this a heavily used feature?
- no fs-related logic (config, etc)
- I would like to keep fs logic out
- cjs/esm/deno handle all differently
- does it really need to be the concern of ljos?
- can people do it themselves?
- Maybe export helper functions instead, or provide examples in docs?
- eg: recursively reading directories as commands/subcommands
- I would like to keep fs logic out
- use option definition object properties, not yargs methods
- no
yargs.string()
- use
ljos.option('opt1', {type: 'string'})
- no
- one call signature per function (where possible)
- I dislike overloads
- hard to maintain, learn, and document
- param names don't communicate intention
- if multiple optional args, use config objects intead
- I dislike overloads
- reduce number of aliases for config params, commands, etc
- smaller mental load, less corner cases, simpler types
- no parser config
- boolean-negation
- would need pre-processing (eg:
--no-save
->{save: false}
)
- would need pre-processing (eg:
- camel-case expansion, strip-aliased, strip-dashed
- all could be middleware instead?
- I want to eventually ship types with ljos, and this makes it painful
- I dreaded working around these in yargs, as tracking keys becomes complex
- eg: which keys have defaults set?
- yargs tracks a lot of things by key and associated aliases
- I imagine there are corner-case bugs when combining casing and aliases
- greedy-arrays
- This is where a lot of complex/unsolvable bugs are in yargs
- I would rather people not use greedy array options
- Am I wrong to think that variadic args should be positional?
- boolean-negation
Tasks
- adjust argsert for object params
- strict by default, only return first unknown?
- unknown/misspelled cmd causes unknown args (subcommands, positionals, etc)
- remove as many deprecated methods as possible
- furthermore, simplify existing ones
- demand
- is really complicated
- I don't think the docs show every possible use case
- solutions
- use object param instead?
- simpler than having 10+ overloads based on len(args) and types
- split into smaller methods?
- convert to
check
middleware function factories? - provide examples for people to create their own
check
middlewares?
- use object param instead?
- fix/remove/address TODO tests
- middleware
- convert validation methods to check middleware helpers
- demand / requiresArg logic
- accept check cb & middleware
- remove coerce? (just use a transform middleware?)
- convert validation methods to check middleware helpers
- Integrate @types/yargs types
- unknown option/positional
- parseArgs will treat unknown option as boolean
- not sure what happens with positional
- eg:
cmd1 <pos1>
with no correspondingljos.positional()
- eg:
- Don't allow mixture of option/positional for a given key
- might not be possible,
- options/positionals are run through the parser as options (separately)
- More flexibility around variadic positionals
- eg:
mv <src..> <dst>
does not work in yargs - solution
- Determine number of args before/after?
- Pick/remove before ones, pick/remove after ones, remaining are variadic.
- Raise error if expected > provided
- eg:
- only keep camel-case variants
- middleware
- esbuild (or some future native build tool with type checking)?