Code Monkey home page Code Monkey logo

type-flag's Introduction

type-flag

Strongly typed command-line arguments parser.

No dependencies & tree-shakable (Max 1.4 kB).

Try it out online

Tip

Looking for something more robust? 👀

Try Cleye—a CLI development tool powered by type-flag.

In addition to flag parsing, it supports argument parsing and has a beautiful --help documentation generator.

Already a sponsor? Join the discussion in the Development repo!

🚀 Install

npm i type-flag

🚦 Quick start

Let's say you want to create a script with the following usage:

$ my-script --name John --age 20

typeFlag

Here's how easy it is with type-flag:

import { typeFlag } from 'type-flag'

const parsed = typeFlag({
    name: String,
    age: {
        type: Number,
        alias: 'a'
    }
})

console.log(parsed.flags.name) // 'John'
console.log(parsed.flags.age) // 20

You can also get unknown flags and arguments from the parsed object:

// object of unknown flags passed in
console.log(parsed.unknownFlags)

// arguments
console.log(parsed._)

getFlag

Want something even simpler?

type-flag also exports a getFlag function that returns a single flag value.

import { getFlag } from 'type-flag'

const name = getFlag('--name', String)
const age = getFlag('-a,--age', Number)

console.log(name) // 'John'
console.log(age) // 20

These are quick demos but type-flag can do so much more:

  • Accept multiple flag values
  • Flag operators (e.g. =) for explicitly passing in a value
  • Parse unknown flags
  • Parse alias groups (e.g. -abc)

Keep reading to learn more!

🧑‍💻 Usage

Defining flags

Pass in an object where the key is the flag name and the value is the flag type—a parser function that takes in a string and parses it to that type. Default JavaScript constructors should be able to cover most use-cases: String, Number, Boolean, etc.

The value can also be an object with the type property as the flag type.

typeFlag({
    // Short-hand
    stringFlag: String,
    numberFlag: Number,
    booleanFlag: Boolean,

    // Object syntax:
    stringFlag: {
        type: String
    }
})

Array type

To accept multiple values of a flag, wrap the type with an array:

const parsed = typeFlag({
    myFlag: [String]
})

// $ node ./cli --my-flag A --my-flag B
parsed.flags.myFlag // => ['A', 'B']

Aliases

Flags are often given single-character aliases for shorthand usage (eg. --help to -h). To give a flag an alias, use the object syntax and set the alias property to a single-character name.

typeFlag({
    myFlag: {
        type: String,
        alias: 'm'
    }
})

// $ node ./cli -m hello
parsed.flags.myFlag // => 'hello'

Default values

Flags that are not passed in will default to being undefined. To set a different default value, use the object syntax and pass in a value as the default property. When a default is provided, the return type will reflect that instead of undefined.

When using mutable values (eg. objects/arrays) as a default, pass in a function that creates it to avoid mutation-related bugs.

const parsed = typeFlag({
    someNumber: {
        type: Number,
        default: 1
    },

    manyNumbers: {
        type: [Number],

        // Use a function to return an object or array
        default: () => [1, 2, 3]
    }
})

To get undefined in the parsed flag type, make sure strict or strictNullChecks is enabled.

kebab-case flags mapped to camelCase

When passing in the flags, they can be in kebab-case and will automatically map to the camelCase equivalent.

const parsed = typeFlag({
    someString: [String]
})

// $ node ./cli --someString hello --some-string world
parsed.flags.someString // => ['hello', 'world']

Unknown flags

When unrecognized flags are passed in, they are interpreted as a boolean, or a string if explicitly passed in. Unknown flags are not converted to camelCase to allow for accurate error handling.

const parsed = typeFlag({})

// $ node ./cli --some-flag --some-flag=1234
parsed.unknownFlags // => { 'some-flag': [true, '1234'] }

Arguments

Arguments are values passed in that are not associated with any flags. All arguments are stored in the _ property.

Everything after -- (end-of-flags) is treated as an argument (including flags) and will be stored in the _['--'] property.

const parsed = typeFlag({
    myFlag: [String]
})

// $ node ./cli --my-flag value arg1 -- --my-flag world
parsed.flags.myFlag // => ['value']
parsed._ // => ['arg1', '--my-flag', 'world']
parsed._['--'] // => ['--my-flag', 'world']

Flag value delimiters

The characters =, : and . are reserved for delimiting the value from the flag.

$ node ./cli --flag=value --flag:value --flag.value

This allows for usage like --flag:key=value or --flag.property=value to be possible.

Mutated argv array

When type-flag iterates over the argv array, it removes the tokens it parses out via mutation.

By default, type-flag works on a new copy of process.argv.slice(2) so this doesn't have any side-effects. But if you want to leverage this behavior to extract certain flags and arguments, you can pass in your own copy of process.argv.slice(2).

This may be useful for filtering out certain flags before passing down the argv to a child process.

Ignoring unknown flags

Sometimes it may be undesirable to parse unknown flags. In these cases, you can ignore them so they're left unparsed in the argv array.

const argv = process.argv.slice(2)
const parsed = typeFlag(
    {},
    argv,
    {
        ignore: type => type === 'unknown-flag'
    }
)

// $ node ./cli --unknown=hello
parsed._ // => []
argv // => ['--unknown=hello']

Ignoring everything after the first argument

Similarly to how Node.js only reads flags passed in before the first argument, type-flag can be configured to ignore everything after the first argument.

const argv = process.argv.slice(2)

let stopParsing = false
const parsed = typeFlag(
    {
        myFlag: [Boolean]
    },
    argv,
    {
        ignore: (type) => {
            if (stopParsing) {
                return true
            }
            const isArgument = type === 'argument'
            if (isArgument) {
                stopParsing = isArgument
                return stopParsing
            }
        }
    }
)

// $ node ./cli --my-flag ./file.js --my-flag
parsed.flags.myFlag // => [true]
argv // => ['./file.js', '--my-flag']

👨🏻‍🏫 Examples

Custom flag type

Basic types can be set using built-in functions in JavaScript, but sometimes you want to a new type, narrow the type, or add validation.

To create a new type, simply declare a function that accepts a string argument and returns the parsed value with the expected type.

In this example, the size flag is enforced to be either small, medium or large.

const possibleSizes = ['small', 'medium', 'large'] as const

type Sizes = typeof possibleSizes[number]

const Size = (size: Sizes) => {
    if (!possibleSizes.includes(size)) {
        throw new Error(`Invalid size: "${size}"`)
    }

    return size
}

const parsed = typeFlag({
    size: Size
})

parsed resolves to the following type:

type Parsed = {
    flags: {
        size: 'small' | 'medium' | 'large' | undefined
    }
    // ...
}

Optional value flag

To create a string flag that acts as a boolean when nothing is passed in, create a custom type that returns both types.

const OptionalString = (value: string) => {
    if (!value) {
        return true
    }

    return value
}

const parsed = typeFlag({
    string: OptionalString
})

// $ node ./cli --string
parsed.flags.string // => true

// $ node ./cli --string hello
parsed.flags.string // => 'hello'

Accepting flag values with = in it

In use-cases where flag values contain =, you can use : instead. This allows flags like --define:K=V.

const parsed = typeFlag({
    define: String
})

// $ node ./cli --define:key=value
parsed.flags.define // => 'key=value'

Dot-nested flags

type Environment = {
    TOKEN?: string
    CI?: boolean
}

const EnvironmentObject = (value: string): Environment => {
    const [propertyName, propertyValue] = value.split('=')
    return {
        [propertyName]: propertyValue || true
    }
}

const parsed = typeFlag({
    env: [EnvironmentObject]
})

const env = parsed.flags.env.reduce(
    (agg, next) => Object.assign(agg, next),
    {}
)

// $ node ./cli --env.TOKEN=123 --env.CI
env // => { TOKEN: 123, CI: true }

Inverting a boolean

To invert a boolean flag, false must be passed in with the = operator (or any other value delimiters).

const parsed = typeFlag({
    booleanFlag: Boolean
})

// $ node ./cli --boolean-flag=false
parsed.flags.booleanFlag // => false

Without explicitly specfying the flag value via =, the false will be parsed as a separate argument.

// $ node ./cli --boolean-flag false
parsed.flags.booleanFlag // => true
parsed._ // => ['false']

Counting flags

To create an API where passing in a flag multiple times increases a count (a pretty common one is -vvv), you can use an array-boolean type and count the size of the array:

const parsed = typeFlag({
    verbose: {
        type: [Boolean],
        alias: 'v'
    }
})

// $ node ./cli -vvv
parsed.flags.verbose.length // => 3

⚙️ API

typeFlag(flagSchema, argv, options)

Returns an object with the shape:

type Parsed = {
    flags: {
        [flagName: string]: InferredType
    }
    unknownFlags: {
        [flagName: string]: (string | boolean)[]
    }
    _: string[]
}

flagSchema

Type:

type TypeFunction = (...args: any[]) => unknown

type FlagSchema = {
    [flagName: string]: TypeFunction | [TypeFunction] | {
        type: TypeFunction | [TypeFunction]
        alias?: string
        default?: unknown
    }
}

An object containing flag schema definitions. Where the key is the flag name, and the value is either the type function or an object containing the type function and/or alias.

argv

Type: string[]

Default: process.argv.slice(2)

The argv array to parse. The array is mutated to remove the parsed flags.

options

Type:

type Options = {
    // Callback to skip parsing on certain argv tokens
    ignore?: (
        type: 'known-flag' | 'unknown-flag' | 'argument',
        flagOrArgv: string,
        value: string | undefined,
    ) => boolean | void
}

getFlag(flagNames, flagType, argv)

flagNames

Type: string

A comma-separated list of flag names to parse.

flagType

Type:

type TypeFunction = (...args: any[]) => unknown

type FlagType = TypeFunction | [TypeFunction]

A function to parse the flag value. Wrap the function in an array to retrieve all values.

argv

Type: string[]

Default: process.argv.slice(2)

The argv array to parse. The array is mutated to remove the parsed flags.

Sponsors

type-flag's People

Contributors

privatenumber avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar

type-flag's Issues

Arguments after "end of flags" (--) aren't easily available

Feature request

Add a __ property that only contains the arguments following the end of flags token.

Why?

Arguments passed after the "end of flags" token (--) are appended to _, but there is no way to determine at which point in the array they start. It's often the case that extra args need to be passed to a child process, which requires them to be separated from the "known" args.

With cleye's current design, it's not possible to determine where the extra args begin.

Alternatives

Expose a property containing the index of the first extra arg.

Additional context

No response

Array flags are incorrectly parsed when using tsx

// foo.ts
import { typeFlag } from "type-flag";

const parsed = typeFlag({
  verbose: {
    type: [Boolean],
    alias: "v",
  },
});

console.log(parsed.flags.verbose.length);

Call tsx foo.ts -vvv prints 9, but if we call npx tsc foo.ts first and then use node foo.js -vvv, this error will not occur.

Allow Multiple Character Aliases for Flags

Feature request

Thank you for your project, It has significantly streamlined the process of developing command-line scripts in Node.js, offering a robust and intuitive API.

Would it be possible to extend flag alias support to include multi-character aliases? This would greatly improve usability for CLIs with many options.

Motivations

for example

{
     skipChangelog: {
        type: Boolean,
        alias: 'sc',
        description: 'Skip generate changelog',
      },
      skipGitTag: {
        type: Boolean,
        alias: 'sgt',
        description: 'Skip git tag',
      },
      npmTag: {
        type: String,
        alias: 'nt',
        description: 'Npm tag',
      },
}

Single-character aliases are not good at distinguishing between different command parameters

Alternatives

No aliases

Additional context

No response

Contributions

  • I plan to open a pull request for this issue
  • I plan to make a financial contribution to this project

Types break `cleye`

Problem

Using the output of the latest develop branch with cleye breaks its types. Screenshot below:

image

I used git bisect to find what change broke it (In case you want to try, I didn't do any fancy npm linking, I just copied the dist folder into my local cleye clone. So I just ran this command for each commit git bisect suggested: pnpm i; git checkout pnpm-lock.yaml; pnpm build; rm -rf ../cleye/node_modules/type-flag/dist; rsync -r dist ../cleye/node_modules/type-flag; (cd ../cleye; pnpm build 2> /dev/null). That command succeeded on master, and failed on develop because of the above typescript error.

git bisect result:

5fcb9cc is the first bad commit
commit 5fcb9cc
Author: Hiroki Osame [email protected]
Date: Tue Apr 16 23:48:44 2024 +0900

style: remove any
path changes
README.md 6 +-
package.json 6 +-
pnpm-lock.yaml 2345 +++++++++++++++++++++++++++++++++++++------------------
src/get-flag.ts 2 +-
src/types.ts 9 +-
src/utils.ts 9 +-

6 files changed, 1587 insertions(+), 790 deletions(-)


I guess one of the any -> unknown changes broke it the typearg extends condition somehow. I'll try to find to time to dig into which exactly, but commenting in the meantime in case anything springs to mind.

Originally posted by @mmkal in #27 (comment)

Expected behavior

Output of this library to work with cleye - but maybe this issue would be better solved in the cleye repo, not sure?

Minimal reproduction URL

https://stackblitz.com/~/github.com/mmkal/cleye

Just run pnpm install && pnpm build in the above - I pushed a change to package.json in the cleye repo to install type-flag from github, then cd into node_modules and build type-flag directly before building cleye. mmkal/cleye@cd34e3a

Version

develop

Node.js version

v18.20.3, but not really relevant

Package manager

pnpm, but not really relevant

Operating system

macOS, but not really relevant

Bugs are expected to be fixed by those affected by it

  • I'm interested in working on this issue

Compensating engineering work financially will speed up resolution

  • I'm willing to offer financial support

support adding description to flags

Feature request

i would like to add a short description to a flag that shows up when user runs --help

Motivations

sometimes the flag name is not enough to explain it's specifics

Alternatives

comments in code above the flag

Additional context

No response

Contributions

  • I plan to open a pull request for this issue
  • I plan to make a financial contribution to this project

Support single-character flags

Feature request

It would be good to be able to define a flag that's only one character.

Motivations

I came across it while implementing trpc-cli, which uses cleye under the hood.

There are valid use cases for for single-character flags that aren't abbreviations, like plot --x 1 --y 2

Alternatives

Of course it will usually be possible to do awkward workarounds like plot --xx 1 --yy 2, and use x and y as aliases, and override documentation to suggest only using plot -x 1 -y 2, but this is cumbersome.

Additional context

I imagine the restriction is because it muddies the water a bit with aliases (would be confusing to have --x mean one thing, but -x be an alias for something else). If that's the reason for the restriction, maybe there could be a check added to make sure the alias exactly matches the "long" form.

Bugs are expected to be fixed by those affected by it

  • I'm interested in working on this issue

Compensating engineering work financially will speed up resolution

  • I'm willing to offer financial support

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.