Repository: spf13/cobra
Pull request #735 (2018-08-23 12:23:59)

doc: obey SOURCE_DATE_EPOCH with manpage generation
Previously if a cobra user didn't specify an explicit .Date header, the
current time would be included in all of the generated man pages each
time they were built. This causes an issue for reproducible builds,
since each re-build of a package that includes the man pages will have
different times listed in the man pages.

To fix this, add support for SOURCE_DATE_EPOCH ([which is a standardised
packaging environment variable, designed to be used specifically for
this purpose][1]).

[1]: https://reproducible-builds.org/specs/source-date-epoch/

Signed-off-by: Aleksa Sarai <asarai@suse.de>


Patch:
diff --git a/README.md b/README.md
index da25608..5e73ffe 100644
--- a/README.md
+++ b/README.md
@@ -16,7 +16,7 @@ Many of the most widely used Go projects are built using Cobra including:
 * [CockroachDB](http://www.cockroachlabs.com/)
 * [Bleve](http://www.blevesearch.com/)
 * [ProjectAtomic (enterprise)](http://www.projectatomic.io/)
-* [GiantSwarm's swarm](https://github.com/giantswarm/cli)
+* [Giant Swarm's gsctl](https://github.com/giantswarm/gsctl)
 * [Nanobox](https://github.com/nanobox-io/nanobox)/[Nanopack](https://github.com/nanopack)
 * [rclone](http://rclone.org/)
 * [nehm](https://github.com/bogem/nehm)
@@ -152,7 +152,6 @@ In a Cobra app, typically the main.go file is very bare. It serves one purpose:
 package main
 
 import (
-
   "{pathToYourApp}/cmd"
 )
 
@@ -263,9 +262,6 @@ In a Cobra app, typically the main.go file is very bare. It serves, one purpose,
 package main
 
 import (
-  "fmt"
-  "os"
-
   "{pathToYourApp}/cmd"
 )
 
@@ -393,6 +389,7 @@ The following validators are built in:
 - `MinimumNArgs(int)` - the command will report an error if there are not at least N positional args.
 - `MaximumNArgs(int)` - the command will report an error if there are more than N positional args.
 - `ExactArgs(int)` - the command will report an error if there are not exactly N positional args.
+- `ExactValidArgs(int)` = the command will report and error if there are not exactly N positional args OR if there are any positional args that are not in the `ValidArgs` field of `Command`
 - `RangeArgs(min, max)` - the command will report an error if the number of args is not between the minimum and maximum number of expected args.
 
 An example of setting the custom validator:
diff --git a/args.go b/args.go
index a5d8a92..c4d820b 100644
--- a/args.go
+++ b/args.go
@@ -78,6 +78,18 @@ func ExactArgs(n int) PositionalArgs {
 	}
 }
 
+// ExactValidArgs returns an error if
+// there are not exactly N positional args OR
+// there are any positional args that are not in the `ValidArgs` field of `Command`
+func ExactValidArgs(n int) PositionalArgs {
+	return func(cmd *Command, args []string) error {
+		if err := ExactArgs(n)(cmd, args); err != nil {
+			return err
+		}
+		return OnlyValidArgs(cmd, args)
+	}
+}
+
 // RangeArgs returns an error if the number of args is not within the expected range.
 func RangeArgs(min int, max int) PositionalArgs {
 	return func(cmd *Command, args []string) error {
diff --git a/doc/man_docs.go b/doc/man_docs.go
index e5dbda2..4a06233 100644
--- a/doc/man_docs.go
+++ b/doc/man_docs.go
@@ -20,6 +20,7 @@ import (
 	"os"
 	"path/filepath"
 	"sort"
+	"strconv"
 	"strings"
 	"time"
 
@@ -104,14 +105,16 @@ func GenMan(cmd *cobra.Command, header *GenManHeader, w io.Writer) error {
 	if header == nil {
 		header = &GenManHeader{}
 	}
-	fillHeader(header, cmd.CommandPath())
+	if err := fillHeader(header, cmd.CommandPath()); err != nil {
+		return err
+	}
 
 	b := genMan(cmd, header)
 	_, err := w.Write(md2man.Render(b))
 	return err
 }
 
-func fillHeader(header *GenManHeader, name string) {
+func fillHeader(header *GenManHeader, name string) error {
 	if header.Title == "" {
 		header.Title = strings.ToUpper(strings.Replace(name, " ", "\\-", -1))
 	}
@@ -120,12 +123,20 @@ func fillHeader(header *GenManHeader, name string) {
 	}
 	if header.Date == nil {
 		now := time.Now()
+		if epoch := os.Getenv("SOURCE_DATE_EPOCH"); epoch != "" {
+			unixEpoch, err := strconv.ParseInt(epoch, 10, 64)
+			if err != nil {
+				return fmt.Errorf("invalid SOURCE_DATE_EPOCH: %v", err)
+			}
+			now = time.Unix(unixEpoch, 0)
+		}
 		header.Date = &now
 	}
 	header.date = (*header.Date).Format("Jan 2006")
 	if header.Source == "" {
 		header.Source = "Auto generated by spf13/cobra"
 	}
+	return nil
 }
 
 func manPreamble(buf *bytes.Buffer, header *GenManHeader, cmd *cobra.Command, dashedName string) {


Test patch:
diff --git a/args_test.go b/args_test.go
index d797b6f..c81b212 100644
--- a/args_test.go
+++ b/args_test.go
@@ -158,6 +158,52 @@ func TestExactArgsWithInvalidCount(t *testing.T) {
 	}
 }
 
+func TestExactValidArgs(t *testing.T) {
+	c := &Command{Use: "c", Args: ExactValidArgs(3), ValidArgs: []string{"a", "b", "c"}, Run: emptyRun}
+	output, err := executeCommand(c, "a", "b", "c")
+	if output != "" {
+		t.Errorf("Unexpected output: %v", output)
+	}
+	if err != nil {
+		t.Errorf("Unexpected error: %v", err)
+	}
+}
+
+func TestExactValidArgsWithInvalidCount(t *testing.T) {
+	c := &Command{Use: "c", Args: ExactValidArgs(2), Run: emptyRun}
+	_, err := executeCommand(c, "a", "b", "c")
+
+	if err == nil {
+		t.Fatal("Expected an error")
+	}
+
+	got := err.Error()
+	expected := "accepts 2 arg(s), received 3"
+	if got != expected {
+		t.Fatalf("Expected %q, got %q", expected, got)
+	}
+}
+
+func TestExactValidArgsWithInvalidArgs(t *testing.T) {
+	c := &Command{
+		Use:       "c",
+		Args:      ExactValidArgs(1),
+		ValidArgs: []string{"one", "two"},
+		Run:       emptyRun,
+	}
+
+	_, err := executeCommand(c, "three")
+	if err == nil {
+		t.Fatal("Expected an error")
+	}
+
+	got := err.Error()
+	expected := `invalid argument "three" for "c"`
+	if got != expected {
+		t.Errorf("Expected: %q, got: %q", expected, got)
+	}
+}
+
 func TestRangeArgs(t *testing.T) {
 	c := &Command{Use: "c", Args: RangeArgs(2, 4), Run: emptyRun}
 	output, err := executeCommand(c, "a", "b", "c")