Repository: oclif/core
Pull request #1148 (2024-07-26 18:45:12)

fix: ignore escaped delimiters
Ignore escaped delimiters when splitting input string for flags with `delimiter` and `multiple`

**Before**
```
my-cli --flag one,two,three` => {flag: ['one', 'two', 'three']}
```

**After**
```
my-cli --flag one\,two,three` => {flag: ['one,two', 'three']}
```

This is useful when you have flag values that contain the delimiter but shouldn't be split.

_Depending on you shell, you might need to double escape to get the desired effect._

@W-16343745@
https://github.com/forcedotcom/cli/issues/2928

mshanemc: QA: threw extra UT at it, all ✅ 

the breaking scenario would be "I'm already using `multiple` and `delimiter: ','` AND I have the escape character like `\,` or `\\,` being used by my command and that's currently being interpreted as a delimiter and now it's about to not be.

seems pretty low-probability.



Patch:
diff --git a/src/parser/parse.ts b/src/parser/parse.ts
index ff8489e..adb3d95 100644
--- a/src/parser/parse.ts
+++ b/src/parser/parse.ts
@@ -412,17 +412,27 @@ export class Parser<
 
         // multiple with custom delimiter
         if (fws.inputFlag.flag.type === 'option' && fws.inputFlag.flag.delimiter && fws.inputFlag.flag.multiple) {
+          // regex that will identify unescaped delimiters
+          const makeDelimiter = (delimiter: string) => new RegExp(`(?<!\\\\)${delimiter}`)
           return {
             ...fws,
             valueFunction: async (i) =>
               (
                 await Promise.all(
                   (i.tokens ?? [])
-                    .flatMap((token) => token.input.split((i.inputFlag.flag as OptionFlag<any>).delimiter ?? ','))
+                    .flatMap((token) =>
+                      token.input.split(makeDelimiter((i.inputFlag.flag as OptionFlag<any>).delimiter ?? ',')),
+                    )
                     // trim, and remove surrounding doubleQuotes (which would hav been needed if the elements contain spaces)
                     .map((v) =>
                       v
                         .trim()
+                        // remove escaped characters from delimiter
+                        // example: --opt="a\,b,c" -> ["a,b", "c"]
+                        .replaceAll(
+                          new RegExp(`\\\\${(i.inputFlag.flag as OptionFlag<any>).delimiter}`, 'g'),
+                          (i.inputFlag.flag as OptionFlag<any>).delimiter ?? ',',
+                        )
                         .replace(/^"(.*)"$/, '$1')
                         .replace(/^'(.*)'$/, '$1'),
                     )


Test patch:
diff --git a/test/parser/parse.test.ts b/test/parser/parse.test.ts
index bb1c4cb..1a5cbd4 100644
--- a/test/parser/parse.test.ts
+++ b/test/parser/parse.test.ts
@@ -519,6 +519,33 @@ See more help with --help`)
         })
         expect(out.flags).to.deep.include({foo: ['a', 'b']})
       })
+      it('parses single flag starting with with \\ escape char', async () => {
+        const out = await parse(['--foo', '\\file:foo'], {
+          flags: {
+            foo: Flags.custom({multiple: true})(),
+          },
+        })
+        expect(out.flags).to.deep.include({foo: ['\\file:foo']})
+      })
+      it('parses multiple space-delimited flags', async () => {
+        const out = await parse(['--foo', 'a', 'b', 'c'], {
+          flags: {foo: Flags.string({multiple: true})},
+        })
+        expect(out.flags).to.deep.include({foo: ['a', 'b', 'c']})
+      })
+      it('parses multiple space-delimited flags ending with with \\ escape char', async () => {
+        const out = await parse(['--foo', 'c:\\', 'd:\\'], {
+          flags: {foo: Flags.string({multiple: true})},
+        })
+        expect(out.flags).to.deep.include({foo: ['c:\\', 'd:\\']})
+      })
+      it('parses multiple space-delimited flags ending with with \\ escape char', async () => {
+        const out = await parse(['--foo', 'c:\\', 'd:\\'], {
+          flags: {foo: Flags.string({multiple: true})},
+        })
+        expect(out.flags).to.deep.include({foo: ['c:\\', 'd:\\']})
+      })
+
       it('allowed options on multiple', async () => {
         const out = await parse(['--foo', 'a', '--foo=b'], {
           flags: {
@@ -646,6 +673,41 @@ See more help with --help`)
             expect(error.message).to.include('Expected --foo=b c to be one of: a a, b b')
           }
         })
+        it('retains escape char without delimiter', async () => {
+          const out = await parse(['--foo', 'a\\'], {
+            flags: {
+              foo: Flags.string({multiple: true, delimiter: ','}),
+            },
+          })
+          expect(out.flags).to.deep.include({foo: ['a\\']})
+        })
+        it('does not split on escaped delimiter', async () => {
+          const out = await parse(['--foo', 'a\\,b,c'], {
+            flags: {
+              foo: Flags.string({multiple: true, delimiter: ','}),
+            },
+          })
+          expect(out.flags).to.deep.include({foo: ['a,b', 'c']})
+        })
+        it('escapes with multiple invocation', async () => {
+          const out = await parse(['--foo', 'a\\,b', '--foo', 'b'], {
+            flags: {
+              foo: Flags.string({multiple: true, delimiter: ','}),
+            },
+          })
+          expect(out.flags).to.deep.include({foo: ['a,b', 'b']})
+        })
+
+        it('comma-escaped stringified json', async () => {
+          const val = '{"a":"b"\\,"c":"d"}'
+          const expected = '{"a":"b","c":"d"}'
+          const out = await parse(['--foo', val], {
+            flags: {
+              foo: Flags.string({multiple: true, delimiter: ','}),
+            },
+          })
+          expect(out.flags).to.deep.include({foo: [expected]})
+        })
       })
     })
 
@@ -709,7 +771,6 @@ See more help with --help`)
             strict: false,
             '--': false,
           })
-          console.log(out)
           expect(out.argv).to.deep.equal(['foo', 'bar', '--', '--myflag'])
           expect(out.args).to.deep.equal({argOne: 'foo'})
         })