Option array list linked to another option array list

70 views Asked by At

I'm building a command-line interface (CLI) in Node.js using the 'commander' library. I've created a command called 'groupBy' which accepts two options:

'--paths' for a list of file paths.
'--jsonPaths' for a list of JSON paths.

I can successfully print something like this:

$ index.js groupBy --paths path1 path2 --jsonPaths jsonPath1 jsonPath2
In the given files path1, path2
I want to show jsonPath1, jsonPath2 values

Now, I want to add the ability to filter by values for specific JSON paths and print something like this:

In the given files path1, path2
I want to show jsonPath1, jsonPath2, jsonPath3 values
Not filtering on jsonPath1
Filtering by value1 on jsonPath2
Filtering by value2, value3 on jsonPath3

I'm struggling with the correct command structure and 'commander' implementation. I had an idea to use a command like this:

$ index.js groupBy --paths path1 path2 --jsonPaths jsonPath1 jsonPath2 jsonPath3 --filter jsonPath1 '' --filter jsonPath2 value1 --filter jsonPath3 value2 value3

However, when using 'commander,' the 'filter' option generates an array like this: [jsonPath, '', jsonPath2, value1, value2, value3]. I can't distinguish between JSON paths and values, and I can't determine which values apply to which JSON paths.

I'm looking for a way to implement this functionality with 'commander.' Any ideas or suggestions would be greatly appreciated. Thank you!

1

There are 1 answers

0
B Jam On

'commander' library let customize option processing
It let replace the default string array by anything else.
So it is possible to use an array of object like this :

[
     {
         jsonPath: "jsonPathX",
         jsonValues: [ "value1", "value2" ]
     }
]

where jsonPath is the first arg of the option, and jsonValues are the others args of the option :

-f jsonPathX value1 value2

Have to apply argParser(value, previous) method on the Option object after the creation and before the addition to the command.

const groupBy = new Command()
groupBy
    .addOption(
        new Option (
            '-f, --jsonFilters [jsonPath jsonValues...]', 
            'description'
        )
        .argParser((value, previous) => {
            // Implementation
        }
    )

argParser will be called for every arg of the option.
value is the current arg
previous is the value returned by the previous call, the first time his value is undefined. The final value of filters depend of the last value returned in the last call to argParser

In each call, there is no no easy way to differenciate args from a first filter to the others. Have to parse all the args ourself. We can access to them by using parent.args property on the current command object groupBy.

In each call, it is not easy to know what arg is already parsed. So I choice to calculate the final result at the first call and return it. In the other calls, do nothing except returning the previous result.

        .argParser((value, previous) => {
            if (previous) // not the first call (!== undefined) 
                return previous // return result already generated

            // the first call, have to generate result
            const result = []

            // First keep only the filters options like -f, --jsonFilters and their args
            let filtersArgs = []
            let currentFilterOption = false
            for (const arg of groupBy.parent.args.slice(1)) {
                if (arg.slice(0,1) === '-' ) 
                    currentFilterOption = ['-f', '--jsonFilters'].includes(arg)
                if (currentFilterOption)
                    filtersArgs.push(arg)
            }

            // while there are args to proceed
            while (filtersArgs.length) {
                // Proceed the first option group
                const nextArgs = filtersArgs.slice(1)
                const indiceNextOption = nextArgs.findIndex(arg => ['-f', '--jsonFilters'].includes(arg))
                const currentArgs = indiceNextOption < 0
                    ? nextArgs
                    : nextArgs.slice(0, indiceNextOption)
                filtersArgs = currentArgs.length
                    ? filtersArgs.slice(currentArgs.length + 1)
                    : filtersArgs.slice(1)
                const indexAlreadyFiltered = result.findIndex(({jsonPath}) => jsonPath === currentArgs.at(0))
                if (indexAlreadyFiltered > 0) {
                    for (const arg of currentArgs.slice(1)) {
                        if (! result.at(indexAlreadyFiltered).jsonValues.includes(arg))
                            result.at(indexAlreadyFiltered).jsonValues.push(arg)
                    }
                }    
                else {
                    // Allow empty jsonValues if only one arg, have to raise an error in this case in the action method
                    result.push({
                        jsonPath: currentArgs.at(0),
                        jsonValues: currentArgs.slice(1)
                    })
                }
            }
            return result
        }
    .action(async (options, command) => {
        const {
            paths, jsonPaths, jsonFilters
        } = command.opts()
        const filtersMissingParams = jsonFilters.filter(({jsonValues}) => ! jsonValues.length)
        if (filtersMissingParams.length > 0) {
            Log.error("A filter must contain more than one param.")
            Log.error("This filter(s) are invalid(s) :")
            for (const {jsonPath, jsonValues} of filtersMissingParams) {
                Log.error(jsonPath, jsonValues.join(' '))
            }
            command.help()
        }
    })