Skip to content
This repository has been archived by the owner on Jan 28, 2023. It is now read-only.
/ ljos Public archive

Ljos is yargs, but simplified and built around Node's util parseArgs. For the most part it works, it just needs fine-tuning. Anyone is welcome to use the ideas/code behind it.

Notifications You must be signed in to change notification settings

jly36963/ljos

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

13 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

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)
  • 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
  • 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' })
        • TODO: this is fundamentally different than yargs
          • reconcile difference with pre-processing?
          • require explicit def for options? make strict by default?
    • explicit positionals
      • cmd1 <pos1> will need a corresponding ljos.positional('pos1', { /* ... */ })
  • 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?
  • 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
  • use option definition object properties, not yargs methods
    • no yargs.string()
    • use ljos.option('opt1', {type: 'string'})
  • 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
  • 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})
    • 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?

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?
  • 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?)
  • 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 corresponding ljos.positional()
  • 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
  • only keep camel-case variants
    • middleware
  • esbuild (or some future native build tool with type checking)?

About

Ljos is yargs, but simplified and built around Node's util parseArgs. For the most part it works, it just needs fine-tuning. Anyone is welcome to use the ideas/code behind it.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published