Package detail

@benev/argv

benevolent-games309MIT0.3.10

command line argument parser

argv, cli, command-line, parser

readme

🎛️ @benev/argv

the greatest command line parser for typescript, maybe

🤖 for making node cli programs
🕵️ incredible typescript type inference
🧼 zero dependencies
💖 made free and open source, just for you


💁 autogenerated --help pages

pizza --help

pizza large --pepperoni --slices 3

# or, any way you like it
pizza --slices=9 large
pizza medium -p --slices=5
pizza small --pepperoni="no" --slices="2"


📖 build your cli

  1. install @benev/argv via npm
     npm i @benev/argv
  2. import stuff
     import {cli, command, arg, param, string, number} from "@benev/argv"
  3. specify your cli, and perform the parsing
     const {args, params} = cli(process.argv, {
       name: "pizza",
       commands: command({
         args: [
           arg("size").required(string),
         ],
         params: {
           slices: param.default(number, "1"),
           pepperoni: param.flag("p"),
         },
       }),
     }).tree
  4. now you have your args and params
     args.size // "large"
     params.slices // 5
     params.pepperoni // true
    • this is the "flat strategy" for receiving your args and params.
      simple, easy!
  5. all your types automagically work!
    it took me a long time to make it elegant and cool like this.


🧑‍🔧 configuring your cli's args and params

  • let's start by making a command
      command({
        args: [],
        params: {},
      })
  • a command can optionally accept a help string
      command({
        help: "what a time to be alive!",
        args: [],
        params: {},
      })
  • let's add positional args
      command({
        args: [
          arg("active").required(boolean),
          arg("count").default(number, "101"),
          arg("name").optional(string),
        ],
        params: {},
      })
    • args are in an array, so each needs a name, eg "active" above
    • there are three modes, required, default, and optional
    • default requires a fallback value
    • there are three basic types, string, number, and boolean, but you can make your own types
  • now let's talk about params
      command({
        args: [],
        params: {
          active: param.required(boolean),
          count: param.default(number, "101"),
          name: param.optional(string),
          verbose: param.flag("-v"),
        },
      })
    • pretty similar. but see the way the names are different?
    • there's a new variety of param called flag, of course, it's automatically a boolean (how could it be otherwise?)

validation for args and params

  • you can set a validate function on any arg or param
      arg("quality").optional(number, {
        validate: n => {
          if (n > 100) throw new Error("to big")
          if (n < 0) throw new Error("to smol")
          return n
        },
      })
    • if you throw any error in a validate, it will be printed all nice-like to the user

help literally everywhere!

  • in fact, every arg and param can have its own help

      command({
        help: "it's the best command, nobody makes commands like me",
    
        args: [
          arg("active").required(boolean, {
            help: "all systems go?",
          }),
    
          arg("count").default(number, "101", {
            help: "number of dalmatians",
          }),
    
          arg("name").optional(string, {
            help: `
              see this multi-line string?
              it will be trimmed all nicely on the help page.
            `
          }),
        ],
    
        params: {
          active: param.required(boolean, {
            help: "toggle this carefully!",
          }),
    
          count: param.default(number, "101", {
            help: "classroom i'm late for",
          }),
    
          name: param.optional(string, {
            help: "pick your pseudonym",
          }),
    
          verbose: param.flag("-v", {
            help: "going loud",
          }),
        },
      })

list helper

  • okay this is seriously crazy cool, check this out
      param.required(list(string))
  • you can just wrap any type in the list helper
    • user inputs comma-separated values mp3,wav,ogg
    • you get an array ["mp3", "wav", "ogg"]
  • is works with any type, like numbers and such
      param.required(list(number))
    • now you get a number[] array (not strings)
    • yes, list preserves the type's validation

choice helper

  • user can choose one of the permitted values
      param.required(string, choice(["thick", "thin"]))
  • you can add a help to it as well
      param.required(string, choice(["thick", "thin"], {
        help: "made with organic whole-wheat flour",
      }))

multipleChoice helper

  • allows the user to choose multiple permitted choices
      param.required(string, multipleChoice(["bacon", "lettuce", "tomatoes"]))
  • you can add help, and decide if the user is allowed to make zero choices
      param.required(string, choice(["bacon", "lettuce", "tomatoes"], {
        help: "all available ingredients are gmo'd",
        zeroAllowed: true, // defaults to false
      }))


🌳 tree of multiple commands

  • the commands object is a recursive tree with command leaves
      const {tree} = cli(process.argv, {
        name: "converter",
        commands: {
          image: command({
            args: [],
            params: {
              quality: param.required(number),
            },
          }),
          media: {
            audio: command({
              args: [],
              params: {
                mono: param.required(boolean),
              },
            }),
            video: command({
              args: [],
              params: {
                codec: param.required(string),
              },
            })
          },
        },
      })

tree strategy

  • you get this tree object that reflects its shape
      tree.image?.params.quality // 9
      tree.media.audio?.mono // false
      tree.media.video?.codec // "av1"
    • all the commands are undefined except for the "selected" command
    • and yes, all the typings work

command-execution strategy

  • you can choose to provide each command with an async execute function
      command({
        args: [],
        params: {
          active: param.required(boolean),
          count: param.default(number, "101"),
        },
        async execute({params}) {
          params.active // true
          params.count // 101
        },
      })
    • your execute function receives the relevant fully-typed args, params, and some more stuff
  • your execute function can opt-into pretty-printing errors (with colors) by throwing an ExecutionError

      import {ExecutionError, command} from "@benev/argv"
    
      async execute({params}) {
        throw new ExecutionError("scary error printed in red!")
      }
  • if you choose to use this command-execution strategy, then you need to call your cli's final execute function
      // 👇 awaiting cli execution
      await cli(process.argv, {
        name: "pizza",
        commands: {
          meatlovers: command({
            args: [],
            params: {
              meatiness: param.required(number),
            },
            async execute({params}) {
              console.log(params.meatiness) // 9
            },
          }),
          hawaiian: command({
            args: [],
            params: {
              pineappleyness: param.required(number),
            },
            async execute({params}) {
              console.log(params.pineappleyness) // 8
            },
          }),
        },
      }).execute()
        // ☝️ calling cli final execute


🛠️ custom types

  • i can't believe i got all the types working for everything with custom types
  • it's easy to make your own types
      const date = asType({
        name: "date",
        coerce: string => new Date(string),
      })
    • the name is shown in help pages
    • the coerce function takes a string input, and you turn it into anything you like
  • then you can use 'em in your args and params like normal
      param.required(date)
  • hey why not make a list of 'em while we're at it
      param.required(list(date))
  • feeling spiffy? make a whole group of custom types with this one weird tip
      const myTypes = asTypes({
        date: string => new Date(string),
        integer: string => Math.floor(Number(string)),
      })
    • asTypes will use your object's property names as the type name
  • your custom types can throw errors and it works as validation

      const integer = asType({
        name: "integer",
        coerce: string => {
          const n = Number(string)
    
          if (isNaN(n))
            throw new Error("not a number")
    
          if (!Number.isSafeInteger(n))
            throw new Error("not a safe integer")
    
          return n
        },
      })


🦚 custom themes

  • you can set the theme for your --help pages

      import {themes} from "@benev/argv"
    
      await cli(process.argv, {
    
        // the default theme
        theme: themes.standard,
    
        ...otherStuff,
      }).execute()
    • maybe try themes.seaside for a more chill vibe
    • if you hate fun, use themes.noColor to disable ansi colors
  • make your own theme like this

      import {theme, color} from "@benev/argv"
    
      const seaside = theme({
        plain: [color.white],
        error: [color.brightRed, color.bold],
        program: [color.brightCyan, color.bold],
        command: [color.cyan, color.bold],
        property: [color.blue],
        link: [color.brightBlue, color.underline],
        arg: [color.brightBlue, color.bold],
        param: [color.brightBlue, color.bold],
        flag: [color.brightBlue],
        required: [color.cyan],
        mode: [color.blue],
        type: [color.brightBlue],
        value: [color.cyan],
      })


🌠 give me a github star!

  • i worked way too hard on this
  • please submit issues for any problems or questions
  • maybe make a cool help theme and submit a PR for it

changelog

legend

  • 🟥 breaking change
  • 🔶 maybe breaking change
  • 🍏 non-breaking addition, fix, or enhancement

v0.3.2

  • 🔶 change exports of some undocumented formatting functions
  • 🍏 add command extraArgs option, now you can document extra arguments
  • 🍏 add choice helper option zeroAllowed
  • 🍏 add multipleChoice helper
  • 🍏 enhance word wrapping behavior, more breaking characters than just whitespace

v0.3.1

  • 🍏 improved help pages structuring, can view command subtrees
  • 🍏 improved custom theming facilities, added seaside theme
  • 🔶 removed some undocumented theming types and functions

v0.3.0

  • 🟥 custom types!
    • import the types import {string, number, boolean} from "@benev/argv"
    • String becomes string
    • Number becomes number
    • Boolean becomes boolean
  • 🟥 default fallback is no longer in the options

      // old
      param.default(string, {fallback: "hello"})
    
      // new
      param.default(string, "hello")
  • 🍏 new helpers, asType, asTypes, list

v0.2.0

  • 🟥 massive nuclear rewrite

v0.1.0

  • redesign param parsing to remove dashes
    • params["--flavor"] becomes params.flavor
    • this change requires downstream change in Params type signatures
    • remove the "--" double dashes that prefix your params
    • the parser now delivers params without the "--" for you

v0.0.0

  • initial release