1.2.1-alpha.1 • Published 7 months ago

@nutsloop/ivy-input v1.2.1-alpha.1

Weekly downloads
-
License
Apache-2.0
Repository
github
Last release
7 months ago

@nutsloop/ivy-input.js


Ivy Framework to build CLI applications. esModule.

Index of Contents



Description


Framework to build CLI applications:

  • spawn new thread for complex operation.
  • help--help commands automatic generation.
  • version--version commands automatic generation.
  • callbacks and callbacks rest-parameters for commands, global flags and flags.
  • callbacks can be async or sync for all commands, global flags and flags.
  • callback for flags can return a value, that will be passed to the command callback.
  • globals callback DO NOT return any value to the command and are executed before everything else.
    • good for setting environment variables.
  • pass extra data to callbacks for all commands, global flags and flags.
  • flags and global-flags features:
    • --flag=3 style.
    • infer type from the value passed to flags & global-flags.
      • string number boolean array kvp json
    • flags callbacks can be prioritized.
    • flags can be mandatory.
    • global-flags must be called before the command.
    • flags and global flags can have conflicts.
    • accept key->value pair (kvp) as argument --flag='!key:value|key2:value2' exclamation mark is mandatory.

Boilerplate OR Scratch

Boilerplate

with this command you can initialize a boilerplate project.

npx @nutsloop/ivy-input init \
  --name='cli-app' \
  --semver='1.0.0' \
  --description='another cli app'

for a more detailed information, have a look at the ivyrun/input-boilerplate

Scratch

with this command you can install the package and develop 'from scratch' the project.

npm install @nutsloop/ivy-input

simple implementation in one file.

initialize a simple JavaScript project.

mkdir ./hello-world && cd ./hello-world
npm init -y
npm pkg set type="module"; # mandatory to specify esModule
npm pkg set name="hello-world"; # let's name the project
npm pkg set version="1.0.0"; # let's give it a version
npm pkg set description="another hello world cli app"; # let's give it a description

npm add @nutsloop/ivy-input # install the package

touch ./index.js

⚠ remember to give index.js file executable permission

chmod u+x ./index.js

file index.js

This hello-world cli application has

  • one command called say-it and
  • one flag called --silent

It doesn't do anything special, it just prints a message to the console. If the --silent flag is passed, the message will not be printed.


#!/usr/bin/env -S node
import { cli, run, flag, command } from '@nutsloop/ivy-input';

// the logic function where all the commands and flags are defined.
async function logic(parsed_argv) {
  // set the SILENCED environment variable to false.
  process.env.SILENT = 'false'
  await command('say-it', {
    description: 'print the message "hello world!".',
    usage: 'hello-world say-it',
    has_flag: true,
      cb: () => {
        if(process.env.SILENT === 'false'){
          console.log('hello world!');
        }
      }
    });
  await flag('--silent', {
    alias: 'silent',
    description: 'do not print the hello world message.',
    usage: 'hello-world say-it --silent',
    is_flag_of: 'say-it',
    cb: {
      fn: () => {
        process.env.SILENT = 'true';
      },
      type: 'sync'
    }
  });
  	// `cli` method will be called when the `run` method parses the process.argv
  await cli(parsed_argv);
}
// the method `run` will parse the process.argv and call the logic function.
// no need to remove the first two elements of the process.argv.
await run(process.argv, logic, 'hello-world');

let's go complex.

initialize a simple JavaScript project.

mkdir ./read-that && cd ./read-that
npm init -y
npm pkg set type="module"; # mandatory to specify esModule
npm pkg set name="read-that"; # let's name the project
npm pkg set version="1.0.0"; # let's give it a version
npm pkg set description="read a Json file. and log the content."; # let's give it a description

npm add @nutsloop/ivy-input # install the package

echo '{"some": "Jason", "data":["0", "1"]}' >> ./data.json

touch ./index.js

⚠ remember to give index.js file executable permission

chmod u+x ./index.js

file index.js

This read-that cli application has

  • one command called data and
  • one flag called --filename[-f]

It will read a json file and log the content to the console.


#!/usr/bin/env -S node

import { access, readFile } from 'node:fs/promises'
import { command, run, cli, flag } from '@nutsloop/ivy-input'
import { extname, resolve } from 'node:path'

// The very entry of the read-that cli application.
const logic = async (parsed_argv) => {

  // The read command callback function
  const read_cb = async (data) => {

    let exitCode = 0

    // filename from the given flag is now absolute path.
    const filename = data.get('filename');

    // It reads the file, and if the readFile fails, it returns a json string with the error message given.
    const content = await readFile(filename, { encoding: 'utf-8' }).catch(error => {
      exitCode = 1

      return `{"error":"${ error.message }"}`
    })

    // It converts to object the give json data.
    let json_data
    try{
      json_data = JSON.parse(content)
    }catch ( error ) {
      json_data = `{"error":"${error.message}"}`
      exitCode = 1
    }

    console.log(json_data)
    process.exit(exitCode)
  };

  await command('data', {
      description: 'read a Json file given from the flag --filename[-f]',
      usage: `npx read-that data --filename='data.json'`,
      has_flag: true,
      // to use the `required_flag` property, you need to use the alias given to the flag while defining it.
      required_flag: [ 'filename' ],
      cb: read_cb
  });

  // the filename callback function
  const filename_cb = async (data) => {
    // only json files are allowed
    if(extname(data) !== '.json'){
      console.error('only json file')
      process.exit(1)
    }

    // check if the file exists
    const file_exists = await access(resolve(process.cwd(), data)).then(() => true).catch(() => false)
    if(!file_exists){
      console.error('file does not exist')
      process.exit(1)
    }

    return resolve(process.cwd(), data)
  };

  await flag(['--filename', '-f'], {
    alias: 'filename',
    description: `the filename of the json file to read
ONLY relative path to the current working dir of the app, ⚠️ no initial slash or dot needed!
`,
    usage: `npx read-that --filename='data.json'`,
    is_flag_of: 'data',
    cb: {
      type: 'async',
      fn: filename_cb
      },
    type: 'string'
  });

  // `cli` method will be called when the `run` method parses the process.argv
  await cli(parsed_argv).catch(console.error);
};

// - the `run` method will parse the process.argv and call the logic function.
await run(process.argv, logic, 'read-that').catch(console.error);

the Command class automatically creates two commands:

  • help: to access the doc entries, you need to pass:
    • key->value (opts) to the --view flag to retrieve the doc for the flag where key is the command-name & value is the flag-name
      • example ./index.js help --view=read:--filename to retrieve the flag doc
    • just the name of the command to the --view flag to retrieve the doc for the command
      • example ./index.js help --view=read to retrieve the command doc
  • version: the command version relays to the package.json file entry version

let's run the app and all its functionalities

# usage
./index.js read --filename='test.json' # will print the object
./index.js read --filename='test.js' # will print 'only json files'
./index.js read --filename='no-file.json' # will print the ENOENT error message

# automatic commands help & version
./index.js version # will print the version
./index.js help --view=read:--filename # will print the doc entry for the --filename flag
./index.js help --view=read:-f # same same as above