diff --git a/integrations/cobra/cobra_usage_test.go b/integrations/cobra/cobra_usage_test.go index 4684b7b5a..28f21dbf1 100644 --- a/integrations/cobra/cobra_usage_test.go +++ b/integrations/cobra/cobra_usage_test.go @@ -350,6 +350,29 @@ func TestStringDefaultZero(t *testing.T) { assertContains(t, got, `default="0"`) } +// TestDefaultsQuotedWhenNotIdentifiers checks that defaults KDL would read as a +// boolean, null, or number are quoted so they stay strings. +func TestDefaultsQuotedWhenNotIdentifiers(t *testing.T) { + cmd := &cobra.Command{Use: "app"} + cmd.Flags().Bool("enabled", true, "Enable the feature") + cmd.Flags().Int("parallel", -1, "Maximum parallelism") + cmd.Flags().Float64("ratio", -0.5, "Ratio") + cmd.Flags().String("sign", "+1", "Signed") + cmd.Flags().String("nothing", "null", "Null word") + cmd.Flags().String("infinite", "inf", "Inf word") + cmd.Flags().String("list", "[a]", "Brackets") + + got := Generate(cmd) + + assertContains(t, got, `default="true"`) + assertContains(t, got, `default="-1"`) + assertContains(t, got, `default="-0.5"`) + assertContains(t, got, `default="+1"`) + assertContains(t, got, `default="null"`) + assertContains(t, got, `default="inf"`) + assertContains(t, got, `default="[a]"`) +} + // TestSpecialCharacterEscaping checks that kdlQuoteAlways escapes newlines, tabs, // carriage returns, backslashes, and double quotes. func TestSpecialCharacterEscaping(t *testing.T) { diff --git a/integrations/cobra/kdl.go b/integrations/cobra/kdl.go index 1d2199f95..180e00de7 100644 --- a/integrations/cobra/kdl.go +++ b/integrations/cobra/kdl.go @@ -353,9 +353,19 @@ func needsQuoting(s string) bool { if s[0] >= '0' && s[0] <= '9' { return true } + // A sign followed by a digit (or a dot, which is quoted below) reads as a + // number, so "-1" and "+1" cannot be bare identifiers either. + if (s[0] == '-' || s[0] == '+') && len(s) > 1 && (s[1] >= '0' && s[1] <= '9' || s[1] == '.') { + return true + } + // Keyword-looking values would be parsed as a boolean, null or number. + switch s { + case "true", "false", "null", "inf", "-inf", "nan": + return true + } for _, c := range s { switch c { - case ' ', '\t', '\n', '\r', '"', '\\', '/', '(', ')', '{', '}', ';', '=', '#', '.', ',', ':': + case ' ', '\t', '\n', '\r', '"', '\\', '/', '(', ')', '[', ']', '{', '}', ';', '=', '#', '.', ',', ':': return true } }