Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[V1] Fix regression in handling flag-values starting with hyphen #1135

Open
wants to merge 2 commits into
base: v1
from

Conversation

@thaJeztah
Copy link

thaJeztah commented May 8, 2020

fixes #1092

Fix for a regression between v1.22.1 and v1.22.2, where
flag values starting with a hyphen would be parsed as a flag:

runc update test_update --memory-swap -1
Incorrect Usage: flag provided but not defined: -1

This problem was caused by reorderArgs() not properly checking
if the next arg in the list was a value for the flag (and not
the next flag);

Before this patch:

args before reordering: --opt,--opt-value,arg1
args after reordering:  --opt,arg1,--opt-value
args before reordering: --opt,--opt-value,arg1,arg2
args after reordering:  --opt,arg1,--opt-value,arg2
args before reordering: arg1,--opt,--opt-value,arg2
args after reordering:  --opt,arg2,arg1,--opt-value

After this patch is applied:

args before reordering: --opt,--opt-value,arg1
args after reordering:  --opt,--opt-value,arg1
args before reordering: --opt,--opt-value,arg1,arg2
args after reordering:  --opt,--opt-value,arg1,arg2
args before reordering: arg1,--opt,--opt-value,arg2
args after reordering:  --opt,--opt-value,arg1,arg2

What type of PR is this?

(REQUIRED)

  • bug
  • cleanup
  • documentation
  • feature

What this PR does / why we need it:

see description above

Which issue(s) this PR fixes:

fixes #1092

Special notes for your reviewer:

(fill-in or delete this section)

Testing

Test was added

Release Notes

Fix a regression where flag values starting with a hyphen would be parsed as a flag instead of a value for a flag
@thaJeztah thaJeztah force-pushed the thaJeztah:fix_flag_parsing branch from ffa1a37 to cfcbe47 May 8, 2020
@thaJeztah
Copy link
Author

thaJeztah commented May 8, 2020

wrote the test to a separate file (regression_test.go) and ran git bisect (don't do this often, so 😅)

git bisect start
git bisect good v1.22.0
git bisect bad v1.22.3
Bisecting: 62 revisions left to test after this (roughly 6 steps)
[41eb645fa27833f6d76257a58e62ab9b09366c82] Merge branch 'master' into pass-through-regression


git bisect run go test -run TestParseAndRunHyphenValues
...
Bisecting: 0 revisions left to test after this (roughly 0 steps)
[bfeb4c88ec4344b456abce2b587d7bc0b957e879] Merge branch 'master' into pass-through-regression
running go test -run TestParseAndRunHyphenValues
PASS
ok  	github.com/urfave/cli	0.006s
7d0751fa1309a01b405cde327cc43275d13016a2 is the first bad commit
commit 7d0751fa1309a01b405cde327cc43275d13016a2
Author: Lynn Cyrin <lynn@textio.com>
Date:   Wed Sep 11 19:25:51 2019 -0700

    add argIsFlag check

:100644 100644 d33435e6dd71cca15fab4a44d8ee3289f333232c 6471f1005b3e9c33be475d6dc1f322eddabfd606 M	command.go
bisect run success
git bisect log

git bisect start
# good: [bfe2e925cfb6d44b40ad3a779165ea7e8aff9212] Merge pull request #882 from urfave/lynncyrin-patch-1
git bisect good bfe2e925cfb6d44b40ad3a779165ea7e8aff9212
# bad: [c354ceca3e45cda773c3846bcb0c67cec4046b03] Merge pull request #981 from urfave/string-slice-issue-v1
git bisect bad c354ceca3e45cda773c3846bcb0c67cec4046b03
# bad: [41eb645fa27833f6d76257a58e62ab9b09366c82] Merge branch 'master' into pass-through-regression
git bisect bad 41eb645fa27833f6d76257a58e62ab9b09366c82
# good: [ef47250cda5ff52a313118c01ad6b0c5b4877a70] Merge branch 'master' into asahasrabuddhe-patch-1
git bisect good ef47250cda5ff52a313118c01ad6b0c5b4877a70
# good: [2071bcfb8328cb13e5575a586f06a33563b04054] checkpoint
git bisect good 2071bcfb8328cb13e5575a586f06a33563b04054
# bad: [09cdbbfe281fd79e92616a28f1fdf701e9007c22] more docs
git bisect bad 09cdbbfe281fd79e92616a28f1fdf701e9007c22
# bad: [fcfc69e54ff0e870bae1eaf599b7d15b46abb86b] Merge remote-tracking branch 'origin/master' into pass-through-regression
git bisect bad fcfc69e54ff0e870bae1eaf599b7d15b46abb86b
# bad: [7d0751fa1309a01b405cde327cc43275d13016a2] add argIsFlag check
git bisect bad 7d0751fa1309a01b405cde327cc43275d13016a2
# good: [bfeb4c88ec4344b456abce2b587d7bc0b957e879] Merge branch 'master' into pass-through-regression
git bisect good bfeb4c88ec4344b456abce2b587d7bc0b957e879
# first bad commit: [7d0751fa1309a01b405cde327cc43275d13016a2] add argIsFlag check

Looks like the regression was in 7d0751f (#872)

command.go Outdated Show resolved Hide resolved
command.go Outdated
fmt.Println("before reordering:", strings.Join(args, ","))
if !c.SkipArgReorder {
args = reorderArgs(c.Flags, args)
}
fmt.Println("after reordering: ", strings.Join(args, ","))
Comment on lines 192 to 196

This comment has been minimized.

@thaJeztah

thaJeztah May 8, 2020 Author

Problem is in this area;

=== RUN   TestParseAndRunHyphenValues/foo_test_--opt_-opt-value_arg1
before reordering: --opt,-opt-value,arg1
after reordering:  --opt,arg1,-opt-value
    TestParseAndRunHyphenValues/foo_test_--opt_-opt-value_arg1: helpers_test.go:20: (command_test.go:175) Expected <nil> (type <nil>) - Got flag provided but not defined: -opt-value (type *errors.errorString)
    TestParseAndRunHyphenValues/foo_test_--opt_-opt-value_arg1: helpers_test.go:20: (command_test.go:176) Expected [arg1] (type []string) - Got [] (type []string)
    TestParseAndRunHyphenValues/foo_test_--opt_-opt-value_arg1: helpers_test.go:20: (command_test.go:177) Expected -opt-value (type string) - Got  (type string)

looks like the reorderArgs does not take into account if an arg is a value for a flag, and now arg1 becomes an argument for --opt;

after reordering:  --opt,arg1,-opt-value
@thaJeztah thaJeztah force-pushed the thaJeztah:fix_flag_parsing branch from b76f239 to 8486894 May 8, 2020
@thaJeztah thaJeztah changed the title [v1] Add test-case for flag-values starting with hyphen Fix regression in handling flag-values starting with hyphen May 8, 2020
@thaJeztah thaJeztah marked this pull request as ready for review May 8, 2020
@thaJeztah thaJeztah requested a review from urfave/cli as a code owner May 8, 2020
@thaJeztah thaJeztah requested review from rliebz and asahasrabuddhe May 8, 2020
@thaJeztah
Copy link
Author

thaJeztah commented May 8, 2020

Found the issue; ready for review @AkihiroSuda @lynncyrin PTAL

@thaJeztah thaJeztah changed the title Fix regression in handling flag-values starting with hyphen [V1] Fix regression in handling flag-values starting with hyphen May 8, 2020
Copy link
Contributor

AkihiroSuda left a comment

Thanks

command_test.go Outdated Show resolved Hide resolved
@lynncyrin
Copy link
Member

lynncyrin commented May 14, 2020

@thaJeztah I have a requested change since this issue is particularly thorny!

Tangentially, I really appreciate how simple the fix is 🙏

@thaJeztah
Copy link
Author

thaJeztah commented May 14, 2020

Thanks for your review 🤗

Tangentially, I really appreciate how simple the fix is 🙏

Yes! It was hiding in plain sight, yet so easy to miss.

@thaJeztah thaJeztah force-pushed the thaJeztah:fix_flag_parsing branch from 298346c to ab8752d May 14, 2020
Fix for a regression between v1.22.1 and v1.22.2, where
flag values starting with a hyphen would be parsed as a flag:

    runc update test_update --memory-swap -1
    Incorrect Usage: flag provided but not defined: -1

This problem was caused by `reorderArgs()` not properly checking
if the next arg in the list was a value for the flag (and not
the next flag);

Before this patch:

    args before reordering: --opt,--opt-value,arg1
    args after reordering:  --opt,arg1,--opt-value
    args before reordering: --opt,--opt-value,arg1,arg2
    args after reordering:  --opt,arg1,--opt-value,arg2
    args before reordering: arg1,--opt,--opt-value,arg2
    args after reordering:  --opt,arg2,arg1,--opt-value

After this patch is applied:

    args before reordering: --opt,--opt-value,arg1
    args after reordering:  --opt,--opt-value,arg1
    args before reordering: --opt,--opt-value,arg1,arg2
    args after reordering:  --opt,--opt-value,arg1,arg2
    args before reordering: arg1,--opt,--opt-value,arg2
    args after reordering:  --opt,--opt-value,arg1,arg2

Co-authored-by: lynn (they) <lynncyrin@gmail.com>
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
@thaJeztah thaJeztah force-pushed the thaJeztah:fix_flag_parsing branch 2 times, most recently from beaff81 to 119006d May 14, 2020
If a `--` delimiter is encountered, any remaining _non-option-value_
arguments must be handled as argument.

From the POSIX spec (emphasis mine)
https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap12.html#tag_12_0):

> The first -- argument that *is not an option-argument* should be accepted
> as a delimiter indicating the end of options. Any following arguments
> should be treated as operands, even if they begin with the '-' character.

There was a corner-case scenario, where a `--flag` followed by a `--` did
not use `--` as value for the flag, but instead considered it as delimiter.

As a result;

    foo test arg1 --opt -- arg2

Would be reordered as:

   foo test --opt arg1 arg2

Which caused `arg1` to be used as value for `--opt`, instead of `--`,
which is the actual value.

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
@thaJeztah thaJeztah force-pushed the thaJeztah:fix_flag_parsing branch from 119006d to 3358e6f May 14, 2020
Copy link
Author

thaJeztah left a comment

OK, that was a lot of fun; adding tests for -- revealed a corner-case situation that looks like a bug. I pushed a second commit to address that problem, but (see my comment inline) could use some extra eyes; thanks!

@@ -546,8 +546,7 @@ func TestApp_CommandWithFlagBeforeTerminator(t *testing.T) {

expect(t, parsedOption, "my-option")
expect(t, args[0], "my-arg")
expect(t, args[1], "--")

This comment has been minimized.

@thaJeztah

thaJeztah May 14, 2020 Author

Could use extra eyes on this change; it's a result of the latest commit / fix, but could potentially be a breaking change for users (so at least should be called out in the release notes); I think I interpreted the POSIX spec correctly with my patch, but specs can sometimes be ambiguous, so could use the extra eyes.

I also haven't checked the v2 branch/version yet (possibly the fix is needed there as well, or in another form)

This comment has been minimized.

@thaJeztah

thaJeztah May 14, 2020 Author

So far, the twitter-verse seems to consolidate to my interpretation being correct 😂 https://twitter.com/thaJeztah/status/1260921988102656002?s=20

This comment has been minimized.

@lynncyrin

lynncyrin Jul 2, 2020 Member

I'm having a hard time imagining if this is [ a mundane change, or if it'll cause mass chaos for somebody somewhere ]. Is there a way to keep this behavior as-is???

@thaJeztah
Copy link
Author

thaJeztah commented May 14, 2020

(I'm ignoring codecov for now, as I think it's just a rounding error 😅)

@lynncyrin lynncyrin self-requested a review May 19, 2020
@thaJeztah
Copy link
Author

thaJeztah commented Jun 24, 2020

@lynncyrin anything needed to move this forward?

@lynncyrin
Copy link
Member

lynncyrin commented Jul 2, 2020

@thaJeztah I have (hopefully only) 1 more question!

Copy link
Member

lynncyrin left a comment

(I'm formally requesting changes for my comment on the -- change! re-request my code review when you've answered it)

@@ -234,7 +234,7 @@ func reorderArgs(commandFlags []Flag, args []string) []string {
break

// checks if this arg is a value that should be re-ordered next to its associated flag
} else if nextIndexMayContainValue && !strings.HasPrefix(arg, "-") {

This comment has been minimized.

@cyphar

cyphar Jul 3, 2020

This is still not quite correct if you wanted to do something like:

% runc run --bundle --some-real-runc-flag ctr

Where it would still re-order things. The least-ugly way I can think of solving this is that we change nextIndexMayContainValue to make use of TakesValue() (with some interface casting) and removing this "skip reordering flags that start with -" logic if it doesn't break anything.

I also hasten to point out that urfave/cli has actually had this behaviour for a very long time and we (speaking collectively in the containers community) have misunderstood how urfave/cli handles this case. I discussed this in #1152 (which I've marked as a duplicate of this) and I've had to work around this in umoci (see opencontainers/umoci#328 for how I ended up hotfixing this -- you just disable argument reordering). Since we want to avoid breaking other users of urfave/cli we might have to migrate runc, umoci, and containerd away from urfave/cli (maybe to spf13/cobra which is what Docker uses)...

This comment has been minimized.

@cyphar

cyphar Jul 3, 2020

A possible patch to do the above might look like the following. This change does pass all of the existing tests...

diff --git a/command.go b/command.go
index f02d3589ff3a..3c8e971a1c53 100644
--- a/command.go
+++ b/command.go
@@ -223,7 +223,7 @@ func (c *Command) useShortOptionHandling() bool {
 func reorderArgs(commandFlags []Flag, args []string) []string {
        var remainingArgs, reorderedArgs []string
 
-       nextIndexMayContainValue := false
+       nextIndexIsValue := false
        for i, arg := range args {
 
                // dont reorder any args after a --
@@ -233,17 +233,29 @@ func reorderArgs(commandFlags []Flag, args []string) []string {
                        remainingArgs = append(remainingArgs, args[i:]...)
                        break
 
-                       // checks if this arg is a value that should be re-ordered next to its associated flag
-               } else if nextIndexMayContainValue && !strings.HasPrefix(arg, "-") {
-                       nextIndexMayContainValue = false
+                       // if this argument is a value then it isn't a flag and should be
+                       // kept with its associated flag (which was the last flag
+                       // reordered)
+               } else if nextIndexIsValue {
+                       nextIndexIsValue = false
                        reorderedArgs = append(reorderedArgs, arg)
 
                        // checks if this is an arg that should be re-ordered
-               } else if argIsFlag(commandFlags, arg) {
+               } else if flag := findFlagForArg(commandFlags, arg); flag != nil {
                        // we have determined that this is a flag that we should re-order
                        reorderedArgs = append(reorderedArgs, arg)
-                       // if this arg does not contain a "=", then the next index may contain the value for this flag
-                       nextIndexMayContainValue = !strings.Contains(arg, "=")
+
+                       // if the flag takes an argument and doesn't contain a "=" then the
+                       // next index contains a value for this flag -- unfortunately we
+                       // have to depend on the Flag implementing DocGenerationFlag for
+                       // TakesValue().
+                       if docFlag, ok := (*flag).(DocGenerationFlag); ok {
+                               nextIndexIsValue = docFlag.TakesValue() && !strings.Contains(arg, "=")
+                       } else {
+                               // assume that flags take arguments
+                               // XXX: This is quite ugly and I'm not sure what we should do here.
+                               nextIndexIsValue = !strings.Contains(arg, "=")
+                       }
 
                        // simply append any remaining args
                } else {
@@ -254,15 +266,16 @@ func reorderArgs(commandFlags []Flag, args []string) []string {
        return append(reorderedArgs, remainingArgs...)
 }
 
-// argIsFlag checks if an arg is one of our command flags
-func argIsFlag(commandFlags []Flag, arg string) bool {
+// findFlagForArg checks if an arg is one of our command flags and returns the
+// Flag if the arg is a flag (nil otherwise).
+func findFlagForArg(commandFlags []Flag, arg string) *Flag {
        // checks if this is just a `-`, and so definitely not a flag
        if arg == "-" {
-               return false
+               return nil
        }
        // flags always start with a -
        if !strings.HasPrefix(arg, "-") {
-               return false
+               return nil
        }
        // this line turns `--flag` into `flag`
        if strings.HasPrefix(arg, "--") {
@@ -279,12 +292,12 @@ func argIsFlag(commandFlags []Flag, arg string) bool {
                for _, key := range strings.Split(flag.GetName(), ",") {
                        key := strings.TrimSpace(key)
                        if key == arg {
-                               return true
+                               return &flag
                        }
                }
        }
-       // return false if this arg was not one of our flags
-       return false
+       // couldn't find the flag
+       return nil
 }
 
 // Names returns the names including short names and aliases.

This comment has been minimized.

@thaJeztah

thaJeztah Jul 3, 2020 Author

@cyphar I see you commented on the first commit; did you check with the second commit as well?

This comment has been minimized.

@thaJeztah

thaJeztah Jul 3, 2020 Author

(I was looking at the requested changes to preserve some of the existing behaviour)

This comment has been minimized.

@cyphar

cyphar Jul 3, 2020

No, this was just talking about the behaviour of the first change (and not the fun -- semantics that you quizzed everyone on in May). I /think/ that it should be fairly straight-forward (move the -- check after the "is this an option-value check" as in your current patch) but I agree with both of you that that particular change looks quite scary.

I think the --opt -a case is far more clear-cut.

This comment has been minimized.

@thaJeztah

thaJeztah Jul 3, 2020 Author

So, in your example case (runc run --bundle --some-real-runc-flag) both --bundle and --some-real-runc-flag are "known" flags to runc, but the second one should be treated as value for --bundle (so --bundle="--some-real-runc-flag") or as flag? In the former case, wouldn't it have to be used as runc run --bundle -- --some-real-runc-flag? 🤔)

This comment has been minimized.

@cyphar

cyphar Jul 3, 2020

The former (--some-real-runc-flag treated as a avalue for --bundle). The motivating use-case is (aside from POSIX-correctness and compatibility with getopt(3)) similar to --:

% runc run --bundle --foo ctr # where ./--foo is a directory containing an OCI bundle

(Well, for umoci it's more like umoci config --image foo:bar --config.cmd sh --config.cmd -c but that's a slightly more complicated example. But the root cause is the same as your original --memory-limit -1 issue.)

Should mean that --foo is a value for --bundle regardless of whether --foo is a known command-line flag. Currently (and with your patches), urfave/cli will not reorder --bundle --foo (where --foo is not a valid flag for runc) but it will reorder the flags if you do --bundle --systemd-cgroup (where --systemd-cgroup is a supported flag for runc). Even more strangely, --bundle=--systemd-cgroup works without issue because the arguments don't get reordered.

The patch I posted above should make it so that --bundle=--foo acts exactly like --bundle --foo regardless of whether --foo is a valid runc flag.

In the former case, wouldn't it have to be used as runc run --bundle -- --some-real-runc-flag?

I disagree (especially if we take the interpretation that -- should be an option-value in that case). If you use getopt:

% getopt -o '' -l bundle:,systemd-cgroup: -- --bundle --systemd-cgroup container-name
 --bundle '--systemd-cgroup' -- 'container-name'

But also it's the obvious behaviour if you just inject =s:

 getopt -o '' -l bundle:,systemd-cgroup: -- --bundle=--systemd-cgroup container-name
 --bundle '--systemd-cgroup' -- 'container-name'

This comment has been minimized.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Linked issues

Successfully merging this pull request may close these issues.

None yet

4 participants
You can’t perform that action at this time.