diff --git a/src/command/__tests__/command.spec.ts b/src/command/__tests__/command.spec.ts index 0404da3..0c906ab 100644 --- a/src/command/__tests__/command.spec.ts +++ b/src/command/__tests__/command.spec.ts @@ -253,6 +253,76 @@ describe("Command", () => { ) }) + describe("with boolean option before arguments", () => { + it("command should be able to handle variadic arguments with boolean option alias", async () => { + const action = jest.fn().mockReturnValue("OK!") + prog + .command("order", "Order something") + .argument("", "Pizza types") + .option("-v, --verbose", "Detailed log") + .action(action) + const args = ["order", "-v", "pepperoni", "regina"] + await prog.run(args) + expect(action).toHaveBeenCalledWith( + expect.objectContaining({ + args: { types: ["pepperoni", "regina"] }, + options: { v: true, verbose: true }, + }), + ) + }) + + it("command should be able to handle variadic arguments with boolean option", async () => { + const action = jest.fn().mockReturnValue("OK!") + prog + .command("order", "Order something") + .argument("", "Pizza types") + .option("-v, --verbose", "Detailed log") + .action(action) + const args = ["order", "--verbose", "pepperoni", "regina"] + await prog.run(args) + expect(action).toHaveBeenCalledWith( + expect.objectContaining({ + args: { types: ["pepperoni", "regina"] }, + options: { v: true, verbose: true }, + }), + ) + }) + + it("command should be able to handle variadic arguments with boolean option and explicit value", async () => { + const action = jest.fn().mockReturnValue("OK!") + prog + .command("order", "Order something") + .argument("", "Pizza types") + .option("-v, --verbose", "Detailed log") + .action(action) + const args = ["order", "--verbose=true", "pepperoni", "regina"] + await prog.run(args) + expect(action).toHaveBeenCalledWith( + expect.objectContaining({ + args: { types: ["pepperoni", "regina"] }, + options: { v: true, verbose: true }, + }), + ) + }) + + it("command should be able to handle variadic arguments with negative boolean option", async () => { + const action = jest.fn().mockReturnValue("OK!") + prog + .command("order", "Order something") + .argument("", "Pizza types") + .option("-v, --verbose", "Detailed log") + .action(action) + const args = ["order", "--no-verbose", "pepperoni", "regina"] + await prog.run(args) + expect(action).toHaveBeenCalledWith( + expect.objectContaining({ + args: { types: ["pepperoni", "regina"] }, + options: { v: false, verbose: false }, + }), + ) + }) + }) + it("command should check arguments range (variable)", async () => { const action = jest.fn().mockReturnValue("hey!") const cmd = prog diff --git a/src/parser/__tests__/parser.spec.ts b/src/parser/__tests__/parser.spec.ts index 5e4c755..3dce744 100644 --- a/src/parser/__tests__/parser.spec.ts +++ b/src/parser/__tests__/parser.spec.ts @@ -88,20 +88,66 @@ describe("Parser", () => { expect(result.args).toEqual(["my-arg1", "my-arg2"]) }) - it("should handle 'boolean' option", () => { - const line = - "--my-opt true --my-bool 1 --my-false=0 --another=yes --not-now=no -y=yes --not-forced=yes" - const result = parseLine(line, { - boolean: ["myOpt", "myBool", "myFalse", "another", "notNow", "y"], + describe("boolean options", () => { + it("should handle 'boolean' option", () => { + const line = + "--my-opt --my-bool --my-false=0 --another=yes --not-now=no -y=yes --not-forced=yes --no-negative" + const result = parseLine(line, { + boolean: ["myOpt", "myBool", "myFalse", "another", "notNow", "y", "negative"], + }) + expect(result.options).toEqual({ + myOpt: true, + myBool: true, + myFalse: false, + another: true, + notNow: false, + y: true, + notForced: "yes", + negative: false, + }) }) - expect(result.options).toEqual({ - myOpt: true, - myBool: true, - myFalse: false, - another: true, - notNow: false, - y: true, - notForced: "yes", + + it("should handle flag followed by arguments", async () => { + const line = "-v arg1 arg2" + const result = parseLine(line, { + boolean: ["v"], + }) + expect(result.options).toEqual({ + v: true, + }) + }) + + it("should handle long option followed by arguments", async () => { + const line = "--verbose arg1 arg2" + const result = parseLine(line, { + boolean: ["verbose"], + }) + expect(result.options).toEqual({ + verbose: true, + }) + }) + + it("should handle negative option followed by arguments", async () => { + const line = "--no-verbose arg1 arg2" + const result = parseLine(line, { + boolean: ["verbose"], + }) + expect(result.options).toEqual({ + verbose: false, + }) + }) + + it("should handle negative option correcly in rawOptions", async () => { + const line = "--no-verbose" + const result = parseLine(line, { + boolean: ["verbose"], + }) + expect(result.options).toEqual({ + verbose: false, + }) + expect(result.rawOptions).toEqual({ + "--no-verbose": true, + }) }) }) diff --git a/src/parser/index.ts b/src/parser/index.ts index e1ae9cf..3715106 100644 --- a/src/parser/index.ts +++ b/src/parser/index.ts @@ -250,12 +250,13 @@ class OptionParser { handleOptWithoutValue(name: string, tree: Tree): void { const next = tree.next() - const nextIsOptOrUndef = isOptionStr(next) || isDdash(next) || next === undefined + const cleanName = formatOptName(name) + const shouldTakeNextAsVal = this.shouldTakeNextAsValue(cleanName, next) this.compute( name, - cast(name, nextIsOptOrUndef ? true : (next as string), this.config), + cast(name, shouldTakeNextAsVal ? (next as string) : true, this.config), ) - if (!nextIsOptOrUndef) { + if (shouldTakeNextAsVal) { tree.forward() } } @@ -265,10 +266,7 @@ class OptionParser { val = true const next = tree.next() const last = names[names.length - 1] - const alias = this.config.alias[last] - const shouldTakeNextAsVal = - next && !isOptionStr(next) && !isDdash(next) && !this.isBoolean(last, alias) - if (shouldTakeNextAsVal) { + if (this.shouldTakeNextAsValue(last, next)) { tree.forward() val = next as string } @@ -276,6 +274,11 @@ class OptionParser { this.computeMulti(names, val) } + shouldTakeNextAsValue(cleaned: string, next: string | undefined): boolean { + const nextIsOptOrUndef = isOptionStr(next) || isDdash(next) || next === undefined + return !nextIsOptOrUndef && !this.isBoolean(cleaned, this.config.alias[cleaned]) + } + visit(tree: Tree): boolean { // only handle options /* istanbul ignore if */ @@ -313,7 +316,8 @@ class OptionParser { : [prop] ).concat(val) } else { - this.rawOptions[name] = this.options[cleanName] = no ? !val : val + this.options[cleanName] = no ? !val : val + this.rawOptions[name] = val } if (alias) { this.options[alias] = this.options[cleanName]