Repository: alecthomas/kong
Pull request #319 (2022-07-17 18:54:25)

Add Active member to Nodes and Values
`Active` struct member added to `Node`s and `Value`s to denote that the component in the grammar has been "activated" by CLI args.  During tracing, `Node`s and `Value`s are "activated", primarily to provide extra context to mapper functions.  `ExistingFileMapper` and `ExistingDirMapper` functions updated to validate paths only if their `Value`s are a part of the "active" branch in the CLI context.


Patch:
diff --git a/context.go b/context.go
index 45c412a..b5c6e6d 100644
--- a/context.go
+++ b/context.go
@@ -346,6 +346,7 @@ func (c *Context) endParsing() {
 
 func (c *Context) trace(node *Node) (err error) { // nolint: gocyclo
 	positional := 0
+	node.Active = true
 
 	flags := []*Flag{}
 	flagNode := node
@@ -438,6 +439,7 @@ func (c *Context) trace(node *Node) (err error) { // nolint: gocyclo
 					c.endParsing()
 				}
 
+				arg.Active = true
 				err := arg.Parse(c.scan, c.getValue(arg))
 				if err != nil {
 					return err
diff --git a/mapper.go b/mapper.go
index e93d2d4..6e17d9e 100644
--- a/mapper.go
+++ b/mapper.go
@@ -613,7 +613,7 @@ func existingFileMapper(r *Registry) MapperFunc {
 			return err
 		}
 
-		if ctx.Value.Set {
+		if !ctx.Value.Active || ctx.Value.Set {
 			// early return to avoid checking extra files that may not exist;
 			// this hack only works because the value provided on the cli is
 			// checked before the default value is checked (if default is set).
@@ -649,7 +649,7 @@ func existingDirMapper(r *Registry) MapperFunc {
 			return err
 		}
 
-		if ctx.Value.Set {
+		if !ctx.Value.Active || ctx.Value.Set {
 			// early return to avoid checking extra dirs that may not exist;
 			// this hack only works because the value provided on the cli is
 			// checked before the default value is checked (if default is set).
diff --git a/model.go b/model.go
index af2cdaf..5879a4b 100644
--- a/model.go
+++ b/model.go
@@ -54,6 +54,7 @@ type Node struct {
 	Tag         *Tag
 	Aliases     []string
 	Passthrough bool // Set to true to stop flag parsing when encountered.
+	Active      bool // Denotes the node is part of an active branch in the CLI.
 
 	Argument *Value // Populated when Type is ArgumentNode.
 }
@@ -98,6 +99,7 @@ func (n *Node) AllFlags(hide bool) (out [][]*Flag) {
 	group := []*Flag{}
 	for _, flag := range n.Flags {
 		if !hide || !flag.Hidden {
+			flag.Active = true
 			group = append(group, flag)
 		}
 	}
@@ -243,6 +245,7 @@ type Value struct {
 	Format       string // Formatting directive, if applicable.
 	Position     int    // Position (for positional arguments).
 	Passthrough  bool   // Set to true to stop flag parsing when encountered.
+	Active       bool   // Denotes the value is part of an active branch in the CLI.
 }
 
 // EnumMap returns a map of the enums in this value.


Test patch:
diff --git a/mapper_test.go b/mapper_test.go
index ae4c3f1..4909758 100644
--- a/mapper_test.go
+++ b/mapper_test.go
@@ -453,6 +453,35 @@ func TestExistingFileMapperDefaultMissing(t *testing.T) {
 	assert.NoError(t, err)
 	assert.NotZero(t, cli.File)
 	assert.Contains(t, cli.File, file)
+	p = mustNew(t, &cli)
+	_, err = p.Parse([]string{})
+	assert.Error(t, err)
+	assert.Contains(t, err.Error(), "missing.txt: no such file or directory")
+}
+
+func TestExistingFileMapperDefaultMissingCmds(t *testing.T) {
+	type CLI struct {
+		CmdA struct {
+			FileA string `type:"existingfile" default:"testdata/aaa-missing.txt"`
+			FileB string `type:"existingfile" default:"testdata/bbb-missing.txt"`
+		} `cmd:""`
+		CmdC struct {
+			FileC string `type:"existingfile" default:"testdata/ccc-missing.txt"`
+		} `cmd:""`
+	}
+	var cli CLI
+	file := "testdata/file.txt"
+	p := mustNew(t, &cli)
+	_, err := p.Parse([]string{"cmd-a", "--file-a", file, "--file-b", file})
+	assert.NoError(t, err)
+	assert.NotZero(t, cli.CmdA.FileA)
+	assert.Contains(t, cli.CmdA.FileA, file)
+	assert.NotZero(t, cli.CmdA.FileB)
+	assert.Contains(t, cli.CmdA.FileB, file)
+	p = mustNew(t, &cli)
+	_, err = p.Parse([]string{"cmd-a", "--file-a", file})
+	assert.Error(t, err)
+	assert.Contains(t, err.Error(), "bbb-missing.txt: no such file or directory")
 }
 
 //nolint:dupl
@@ -486,6 +515,35 @@ func TestExistingDirMapperDefaultMissing(t *testing.T) {
 	assert.NoError(t, err)
 	assert.NotZero(t, cli.Dir)
 	assert.Contains(t, cli.Dir, dir)
+	p = mustNew(t, &cli)
+	_, err = p.Parse([]string{})
+	assert.Error(t, err)
+	assert.Contains(t, err.Error(), "missing-dir: no such file or directory")
+}
+
+func TestExistingDirMapperDefaultMissingCmds(t *testing.T) {
+	type CLI struct {
+		CmdA struct {
+			DirA string `type:"existingdir" default:"aaa-missing-dir"`
+			DirB string `type:"existingdir" default:"bbb-missing-dir"`
+		} `cmd:""`
+		CmdC struct {
+			DirC string `type:"existingdir" default:"ccc-missing-dir"`
+		} `cmd:""`
+	}
+	var cli CLI
+	dir := "testdata"
+	p := mustNew(t, &cli)
+	_, err := p.Parse([]string{"cmd-a", "--dir-a", dir, "--dir-b", dir})
+	assert.NoError(t, err)
+	assert.NotZero(t, cli.CmdA.DirA)
+	assert.NotZero(t, cli.CmdA.DirB)
+	assert.Contains(t, cli.CmdA.DirA, dir)
+	assert.Contains(t, cli.CmdA.DirB, dir)
+	p = mustNew(t, &cli)
+	_, err = p.Parse([]string{"cmd-a", "--dir-a", dir})
+	assert.Error(t, err)
+	assert.Contains(t, err.Error(), "bbb-missing-dir: no such file or directory")
 }
 
 func TestMapperPlaceHolder(t *testing.T) {