Repository: scionproto/scion
Pull request #3829 (2020-07-13 12:05:33)

cmd: reduce duplication
Share the common commands across different binaries

The following commands are shared where appropriate.

- `<bin> completion`
- `<bin> version`
- `<bin> sample`
- `<bin> sample config`

Further, commands are refactored to be easier to compose by taking
a pather that provides the path for example in the help message.

With this change, the sciond CLI is slightly different:

`sciond --help_config` is now `sciond sample config`

<!-- Reviewable:start -->
---
This change is [<img src="https://reviewable.io/review_button.svg" height="34" align="absmiddle" alt="Reviewable"/>](https://reviewable.io/reviews/scionproto/scion/3829)
<!-- Reviewable:end -->



Patch:
diff --git a/go/cs/BUILD.bazel b/go/cs/BUILD.bazel
index def9947e4..8f6b8bcac 100644
--- a/go/cs/BUILD.bazel
+++ b/go/cs/BUILD.bazel
@@ -4,10 +4,8 @@ load("//:scion.bzl", "scion_go_binary")
 go_library(
     name = "go_default_library",
     srcs = [
-        "completion.go",
         "cs.go",
         "sample.go",
-        "version.go",
     ],
     importpath = "github.com/scionproto/scion/go/cs",
     visibility = ["//visibility:private"],
@@ -42,6 +40,7 @@ go_library(
         "//go/lib/snet:go_default_library",
         "//go/lib/sock/reliable:go_default_library",
         "//go/lib/topology:go_default_library",
+        "//go/pkg/command:go_default_library",
         "//go/pkg/cs:go_default_library",
         "//go/pkg/cs/trust:go_default_library",
         "//go/pkg/cs/trust/handler:go_default_library",
diff --git a/go/cs/cs.go b/go/cs/cs.go
index ebe4dcc3a..343a5044a 100644
--- a/go/cs/cs.go
+++ b/go/cs/cs.go
@@ -51,6 +51,7 @@ import (
 	"github.com/scionproto/scion/go/lib/snet"
 	"github.com/scionproto/scion/go/lib/sock/reliable"
 	"github.com/scionproto/scion/go/lib/topology"
+	"github.com/scionproto/scion/go/pkg/command"
 	"github.com/scionproto/scion/go/pkg/cs"
 	cstrust "github.com/scionproto/scion/go/pkg/cs/trust"
 	trusthandler "github.com/scionproto/scion/go/pkg/cs/trust/handler"
@@ -82,9 +83,12 @@ func main() {
 		},
 	}
 	cmd.AddCommand(
-		newCompletion(cmd),
-		newSample(cmd),
-		newVersion(cmd),
+		command.NewCompletion(cmd),
+		command.NewSample(cmd,
+			command.NewSampleConfig(&config.Config{}),
+			newSamplePolicy,
+		),
+		command.NewVersion(cmd),
 	)
 	cmd.Flags().StringVar(&flags.config, "config", "", "Configuration file (required)")
 	cmd.MarkFlagRequired("config")
diff --git a/go/cs/sample.go b/go/cs/sample.go
index 140e88a8c..3f93ad8ac 100644
--- a/go/cs/sample.go
+++ b/go/cs/sample.go
@@ -22,39 +22,15 @@ import (
 	"gopkg.in/yaml.v2"
 
 	"github.com/scionproto/scion/go/cs/beacon"
-	"github.com/scionproto/scion/go/cs/config"
 	"github.com/scionproto/scion/go/lib/serrors"
+	"github.com/scionproto/scion/go/pkg/command"
 )
 
-func newSample(pather CommandPather) *cobra.Command {
-	var cmd = &cobra.Command{
-		Use:   "sample",
-		Short: "Display sample files",
-	}
-	cmd.AddCommand(
-		newSampleConfig(cmd),
-		newSamplePolicy(cmd),
-	)
-	return cmd
-}
-
-func newSampleConfig(pather CommandPather) *cobra.Command {
-	var cmd = &cobra.Command{
-		Use:     "config",
-		Short:   "Display sample configuration file",
-		Example: fmt.Sprintf("  %[1]s config", pather.CommandPath()),
-		Run: func(cmd *cobra.Command, args []string) {
-			(&config.Config{}).Sample(os.Stdout, nil, nil)
-		},
-	}
-	return cmd
-}
-
-func newSamplePolicy(pather CommandPather) *cobra.Command {
+func newSamplePolicy(pather command.Pather) *cobra.Command {
 	var cmd = &cobra.Command{
 		Use:     "policy",
 		Short:   "Display sample policy file",
-		Example: fmt.Sprintf("  %[1]s policy", pather.CommandPath()),
+		Example: fmt.Sprintf("  %[1]s policy > policy.yml", pather.CommandPath()),
 		RunE: func(cmd *cobra.Command, args []string) error {
 			var sample beacon.Policy
 			sample.InitDefaults()
diff --git a/go/pkg/command/BUILD.bazel b/go/pkg/command/BUILD.bazel
new file mode 100644
index 000000000..4d131f9eb
--- /dev/null
+++ b/go/pkg/command/BUILD.bazel
@@ -0,0 +1,18 @@
+load("@io_bazel_rules_go//go:def.bzl", "go_library")
+
+go_library(
+    name = "go_default_library",
+    srcs = [
+        "completion.go",
+        "pather.go",
+        "sample.go",
+        "version.go",
+    ],
+    importpath = "github.com/scionproto/scion/go/pkg/command",
+    visibility = ["//visibility:public"],
+    deps = [
+        "//go/lib/config:go_default_library",
+        "//go/lib/env:go_default_library",
+        "@com_github_spf13_cobra//:go_default_library",
+    ],
+)
diff --git a/go/cs/completion.go b/go/pkg/command/completion.go
similarity index 92%
rename from go/cs/completion.go
rename to go/pkg/command/completion.go
index 8bf974ac4..768487f49 100644
--- a/go/cs/completion.go
+++ b/go/pkg/command/completion.go
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package main
+package command
 
 import (
 	"fmt"
@@ -21,7 +21,8 @@ import (
 	"github.com/spf13/cobra"
 )
 
-func newCompletion(pather CommandPather) *cobra.Command {
+// NewCompletion creates a command that provides bash completion.
+func NewCompletion(pather Pather) *cobra.Command {
 	var flags struct {
 		shell string
 	}
diff --git a/go/pkg/command/pather.go b/go/pkg/command/pather.go
new file mode 100644
index 000000000..f02c4be53
--- /dev/null
+++ b/go/pkg/command/pather.go
@@ -0,0 +1,46 @@
+// Copyright 2020 Anapaya Systems
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package command
+
+import "strings"
+
+// Pather returns the path to a command.
+type Pather interface {
+	CommandPath() string
+}
+
+// StringPather implements Pather for the string type.
+type StringPather string
+
+func (s StringPather) CommandPath() string {
+	return string(s)
+}
+
+// JoinedPather joins together the paths of multiple Pather objects.
+type JoinedPather []Pather
+
+// CommandPath concatenates all the pather in the list.
+func (l JoinedPather) CommandPath() string {
+	var parts []string
+	for _, p := range l {
+		parts = append(parts, p.CommandPath())
+	}
+	return strings.Join(parts, " ")
+}
+
+// Join links two pathers.
+func Join(a, b Pather) JoinedPather {
+	return JoinedPather{a, b}
+}
diff --git a/go/pkg/command/sample.go b/go/pkg/command/sample.go
new file mode 100644
index 000000000..7fa6548ca
--- /dev/null
+++ b/go/pkg/command/sample.go
@@ -0,0 +1,53 @@
+// Copyright 2020 Anapaya Systems
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package command
+
+import (
+	"fmt"
+	"os"
+
+	"github.com/spf13/cobra"
+
+	"github.com/scionproto/scion/go/lib/config"
+)
+
+// NewSample creates a command with sampler sub commands.
+func NewSample(pather Pather, cmds ...func(Pather) *cobra.Command) *cobra.Command {
+	var cmd = &cobra.Command{
+		Use:   "sample",
+		Short: "Display sample files",
+	}
+	joined := Join(pather, cmd)
+	for _, sampler := range cmds {
+		cmd.AddCommand(sampler(joined))
+	}
+	return cmd
+}
+
+// NewSampleConfig creates factory that creates a command that displays a sample
+// configuration file.
+func NewSampleConfig(cfg config.Sampler) func(Pather) *cobra.Command {
+	return func(pather Pather) *cobra.Command {
+		var cmd = &cobra.Command{
+			Use:     "config",
+			Short:   "Display sample configuration file",
+			Example: fmt.Sprintf("  %[1]s config > cfg.toml", pather.CommandPath()),
+			Run: func(cmd *cobra.Command, args []string) {
+				(cfg).Sample(os.Stdout, nil, nil)
+			},
+		}
+		return cmd
+	}
+}
diff --git a/go/cs/version.go b/go/pkg/command/version.go
similarity index 84%
rename from go/cs/version.go
rename to go/pkg/command/version.go
index fa5e1157e..6aa010d7f 100644
--- a/go/cs/version.go
+++ b/go/pkg/command/version.go
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package main
+package command
 
 import (
 	"fmt"
@@ -22,13 +22,14 @@ import (
 	"github.com/scionproto/scion/go/lib/env"
 )
 
-func newVersion(pather CommandPather) *cobra.Command {
+// NewVersion creates a command that displays the SCION version information.
+func NewVersion(pather Pather) *cobra.Command {
 	var cmd = &cobra.Command{
 		Use:     "version",
 		Short:   "Show the SCION version information",
 		Example: fmt.Sprintf("  %[1]s version", pather.CommandPath()),
 		Run: func(cmd *cobra.Command, args []string) {
-			fmt.Println(env.VersionInfo())
+			fmt.Print(env.VersionInfo())
 		},
 	}
 	return cmd
diff --git a/go/pkg/trust/BUILD.bazel b/go/pkg/trust/BUILD.bazel
index 6155b8195..997d2523f 100644
--- a/go/pkg/trust/BUILD.bazel
+++ b/go/pkg/trust/BUILD.bazel
@@ -72,6 +72,7 @@ go_test(
         "//go/lib/spath:go_default_library",
         "//go/lib/util:go_default_library",
         "//go/lib/xtest:go_default_library",
+        "//go/pkg/command:go_default_library",
         "//go/pkg/trust/internal/metrics:go_default_library",
         "//go/pkg/trust/mock_trust:go_default_library",
         "//go/proto:go_default_library",
diff --git a/go/scion-pki/BUILD.bazel b/go/scion-pki/BUILD.bazel
index 6e765ba15..594a2615c 100644
--- a/go/scion-pki/BUILD.bazel
+++ b/go/scion-pki/BUILD.bazel
@@ -19,6 +19,7 @@ go_library(
     deps = [
         "//go/lib/env:go_default_library",
         "//go/lib/serrors:go_default_library",
+        "//go/pkg/command:go_default_library",
         "//go/scion-pki/certs:go_default_library",
         "//go/scion-pki/testcrypto:go_default_library",
         "//go/scion-pki/trcs:go_default_library",
diff --git a/go/scion-pki/certs/BUILD.bazel b/go/scion-pki/certs/BUILD.bazel
index 51036b673..9f81b9c5f 100644
--- a/go/scion-pki/certs/BUILD.bazel
+++ b/go/scion-pki/certs/BUILD.bazel
@@ -22,6 +22,7 @@ go_library(
         "//go/lib/snet/addrutil:go_default_library",
         "//go/lib/sock/reliable:go_default_library",
         "//go/lib/svc:go_default_library",
+        "//go/pkg/command:go_default_library",
         "//go/pkg/trust:go_default_library",
         "//go/pkg/trust/renewal:go_default_library",
         "@com_github_spf13_cobra//:go_default_library",
diff --git a/go/scion-pki/certs/certs.go b/go/scion-pki/certs/certs.go
index 1b36bf787..6c015b40e 100644
--- a/go/scion-pki/certs/certs.go
+++ b/go/scion-pki/certs/certs.go
@@ -23,6 +23,7 @@ import (
 
 	"github.com/scionproto/scion/go/lib/scrypto/cppki"
 	"github.com/scionproto/scion/go/lib/serrors"
+	"github.com/scionproto/scion/go/pkg/command"
 )
 
 var certTypes = map[string]cppki.CertType{
@@ -43,20 +44,21 @@ func getTypes() []string {
 	return options
 }
 
-func Cmd() *cobra.Command {
-	root := &cobra.Command{
+func Cmd(pather command.Pather) *cobra.Command {
+	cmd := &cobra.Command{
 		Use:   "certs",
 		Short: "Interact with certificates for the SCION control plane PKI.",
 	}
-	root.AddCommand(
-		newValidateCmd(),
-		newVerifyCmd(),
-		newRenewCmd(),
+	joined := command.Join(pather, cmd)
+	cmd.AddCommand(
+		newValidateCmd(joined),
+		newVerifyCmd(joined),
+		newRenewCmd(joined),
 	)
-	return root
+	return cmd
 }
 
-func newValidateCmd() *cobra.Command {
+func newValidateCmd(pather command.Pather) *cobra.Command {
 	var flags struct {
 		certType string
 	}
@@ -70,8 +72,8 @@ In case the 'any' type is specified, this command attempts to identify what type
 a certificate is and validates it accordingly. The identified type is stated in
 the output.
 `,
-		Example: `  scion-pki certs validate --type cp-root /tmp/certs/cp-root.crt
-	  scion-pki certs validate --type any /tmp/certs/cp-root.crt`,
+		Example: fmt.Sprintf(`  %[1]s validate --type cp-root /tmp/certs/cp-root.crt
+  %[1]s validate --type any /tmp/certs/cp-root.crt`, pather.CommandPath()),
 		Args: cobra.ExactArgs(1),
 		RunE: func(cmd *cobra.Command, args []string) error {
 			expectedType, checkType := certTypes[flags.certType]
diff --git a/go/scion-pki/certs/renew.go b/go/scion-pki/certs/renew.go
index 2472f147c..6677eede3 100644
--- a/go/scion-pki/certs/renew.go
+++ b/go/scion-pki/certs/renew.go
@@ -45,6 +45,7 @@ import (
 	"github.com/scionproto/scion/go/lib/snet/addrutil"
 	"github.com/scionproto/scion/go/lib/sock/reliable"
 	"github.com/scionproto/scion/go/lib/svc"
+	"github.com/scionproto/scion/go/pkg/command"
 	"github.com/scionproto/scion/go/pkg/trust"
 	"github.com/scionproto/scion/go/pkg/trust/renewal"
 )
@@ -62,7 +63,7 @@ type subjectVars struct {
 	StreetAddress      string  `json:"street_address,omitempty"`
 }
 
-func newRenewCmd() *cobra.Command {
+func newRenewCmd(pather command.Pather) *cobra.Command {
 	var flags struct {
 		keyFile           string
 		outFile           string
@@ -81,20 +82,20 @@ func newRenewCmd() *cobra.Command {
 		Use:   "renew",
 		Short: "Renew AS certificate",
 		Args:  cobra.MaximumNArgs(1),
-		Example: `  scion-pki certs renew
+		Example: fmt.Sprintf(`  %[1]s renew
 	--key cp-as.key \
 	--transportcert ISD1-ASff00_0_112.pem \
 	--transportkey cp-as.key \
 	--trc ISD1-B1-S1.trc
 
-  scion-pki certs renew
+  %[1]s renew
 	--key fresh.key \
 	--transportcert ISD1-ASff00_0_112.pem \
 	--transportkey cp-as.key \
 	--trc ISD1-B1-S1.trc \
 	--template csr.json \
 	  1-ff00:0:110
-		`,
+		`, pather.CommandPath()),
 		Long: `'renew' sends a certificate chain renewal request to the CA control service.
 
 The transport certificate chain and key are used to sign the renewal requests.
diff --git a/go/scion-pki/certs/verify.go b/go/scion-pki/certs/verify.go
index 79e516c89..5fb8046f3 100644
--- a/go/scion-pki/certs/verify.go
+++ b/go/scion-pki/certs/verify.go
@@ -23,9 +23,10 @@ import (
 
 	"github.com/scionproto/scion/go/lib/scrypto/cppki"
 	"github.com/scionproto/scion/go/lib/serrors"
+	"github.com/scionproto/scion/go/pkg/command"
 )
 
-func newVerifyCmd() *cobra.Command {
+func newVerifyCmd(pather command.Pather) *cobra.Command {
 	var flags struct {
 		trcFile  string
 		unixTime int64
@@ -39,8 +40,9 @@ func newVerifyCmd() *cobra.Command {
 The chain must be a PEM bundle with the AS certificate first, and the CA
 certificate second.
 `,
-		Example: `  scion-pki certs verify --trc ISD1-B1-S1.trc ISD1-ASff00_0_110.pem`,
-		Args:    cobra.ExactArgs(1),
+		Example: fmt.Sprintf(`  %[1]s verify --trc ISD1-B1-S1.trc ISD1-ASff00_0_110.pem`,
+			pather.CommandPath()),
+		Args: cobra.ExactArgs(1),
 		RunE: func(cmd *cobra.Command, args []string) error {
 			cmd.SilenceUsage = true
 			chain, err := cppki.ReadPEMCerts(args[0])
@@ -72,7 +74,6 @@ certificate second.
 	cmd.MarkFlagRequired("trc")
 
 	return cmd
-
 }
 
 func loadTRC(trcFile string) (cppki.SignedTRC, error) {
diff --git a/go/scion-pki/main.go b/go/scion-pki/main.go
index bd57f23b5..ffa83f3a3 100644
--- a/go/scion-pki/main.go
+++ b/go/scion-pki/main.go
@@ -20,13 +20,14 @@ import (
 
 	"github.com/spf13/cobra"
 
+	"github.com/scionproto/scion/go/pkg/command"
 	"github.com/scionproto/scion/go/scion-pki/certs"
 	"github.com/scionproto/scion/go/scion-pki/testcrypto"
 	"github.com/scionproto/scion/go/scion-pki/trcs"
 )
 
 func main() {
-	root := &cobra.Command{
+	cmd := &cobra.Command{
 		Use:   "scion-pki",
 		Short: "SCION Control Plane PKI Management Tool",
 		Args:  cobra.NoArgs,
@@ -40,15 +41,15 @@ func main() {
 		SilenceErrors: true,
 	}
 
-	root.AddCommand(
-		newCompletion(),
+	cmd.AddCommand(
+		command.NewCompletion(cmd),
 		newVersion(),
-		certs.Cmd(),
-		trcs.Cmd(),
-		testcrypto.Cmd(),
+		certs.Cmd(cmd),
+		trcs.Cmd(cmd),
+		testcrypto.Cmd(cmd),
 	)
 
-	if err := root.Execute(); err != nil {
+	if err := cmd.Execute(); err != nil {
 		fmt.Fprintf(os.Stderr, "Error: %s\n", err)
 		os.Exit(1)
 	}
diff --git a/go/scion-pki/trcs/BUILD.bazel b/go/scion-pki/trcs/BUILD.bazel
index 4eae31955..902d84462 100644
--- a/go/scion-pki/trcs/BUILD.bazel
+++ b/go/scion-pki/trcs/BUILD.bazel
@@ -21,6 +21,7 @@ go_library(
         "//go/lib/scrypto/cms/protocol:go_default_library",
         "//go/lib/scrypto/cppki:go_default_library",
         "//go/lib/serrors:go_default_library",
+        "//go/pkg/command:go_default_library",
         "//go/scion-pki/conf:go_default_library",
         "@com_github_google_go_cmp//cmp:go_default_library",
         "@com_github_spf13_cobra//:go_default_library",
diff --git a/go/scion-pki/trcs/combine.go b/go/scion-pki/trcs/combine.go
index 3eaf871fe..f79ef4c39 100644
--- a/go/scion-pki/trcs/combine.go
+++ b/go/scion-pki/trcs/combine.go
@@ -27,9 +27,10 @@ import (
 	"github.com/scionproto/scion/go/lib/scrypto/cms/protocol"
 	"github.com/scionproto/scion/go/lib/scrypto/cppki"
 	"github.com/scionproto/scion/go/lib/serrors"
+	"github.com/scionproto/scion/go/pkg/command"
 )
 
-func newCombine() *cobra.Command {
+func newCombine(pather command.Pather) *cobra.Command {
 	var flags struct {
 		out     string
 		payload string
@@ -45,8 +46,8 @@ provided with the '--payload' flag.
 No further checks are made. Check that the TRC is valid and verifiable with the
 appropriate commands.
 `,
-		Example: `  scion-pki trcs combine --payload ISD1-B1-S1.pld -o ISD1-B1-S1.trc ` +
-			`ISD1-B1-S1.org1 ISD1-B1-S1.org2`,
+		Example: fmt.Sprintf(`  %[1]s combine --payload ISD1-B1-S1.pld -o ISD1-B1-S1.trc `+
+			`ISD1-B1-S1.org1 ISD1-B1-S1.org2`, pather.CommandPath()),
 		Args: cobra.MinimumNArgs(1),
 		RunE: func(cmd *cobra.Command, args []string) error {
 			cmd.SilenceUsage = true
diff --git a/go/scion-pki/trcs/extract.go b/go/scion-pki/trcs/extract.go
index 56376a727..22794eb90 100644
--- a/go/scion-pki/trcs/extract.go
+++ b/go/scion-pki/trcs/extract.go
@@ -24,31 +24,32 @@ import (
 	"github.com/spf13/cobra"
 
 	"github.com/scionproto/scion/go/lib/serrors"
+	"github.com/scionproto/scion/go/pkg/command"
 )
 
-func newExtract() *cobra.Command {
+func newExtract(pather command.Pather) *cobra.Command {
 	cmd := &cobra.Command{
 		Use:   "extract",
 		Short: "Exctract parts of a signed TRC",
-		Example: `  scion-pki trcs extract payload -o payload.der input.trc
-  scion-pki trcs extract certificates -o bundle.pem input.trc`,
 	}
+	joined := command.Join(pather, cmd)
 	cmd.AddCommand(
-		newExtractPayload(),
-		newExtractCertificates(),
+		newExtractPayload(joined),
+		newExtractCertificates(joined),
 	)
 	return cmd
 }
 
-func newExtractPayload() *cobra.Command {
+func newExtractPayload(pather command.Pather) *cobra.Command {
 	var flags struct {
 		out string
 	}
 
 	cmd := &cobra.Command{
 		Use:     "payload",
+		Aliases: []string{"pld"},
 		Short:   "Extract the TRC payload",
-		Example: `  scion-pki trcs extract payload -o payload.der input.trc`,
+		Example: fmt.Sprintf(`  %[1]s payload -o payload.der input.trc`, pather.CommandPath()),
 		Long: `'payload' extracts the asn.1 encoded DER TRC payload.
 
 To inspect the created asn.1 file you can use the openssl tool:
@@ -74,15 +75,16 @@ To inspect the created asn.1 file you can use the openssl tool:
 	return cmd
 }
 
-func newExtractCertificates() *cobra.Command {
+func newExtractCertificates(pather command.Pather) *cobra.Command {
 	var flags struct {
 		out string
 	}
 
 	cmd := &cobra.Command{
 		Use:     "certificates",
+		Aliases: []string{"certs"},
 		Short:   "Extract the bundled certificates",
-		Example: `  scion-pki trcs extract certificates -o bundle.pem input.trc`,
+		Example: fmt.Sprintf(`  %[1]s certificates -o bundle.pem input.trc`, pather.CommandPath()),
 		Long:    `'certificates' extracts the certificates into a bundeld PEM file.`,
 		Args:    cobra.ExactArgs(1),
 		RunE: func(cmd *cobra.Command, args []string) error {
diff --git a/go/scion-pki/trcs/human.go b/go/scion-pki/trcs/human.go
index e8e18a934..94c5eb106 100644
--- a/go/scion-pki/trcs/human.go
+++ b/go/scion-pki/trcs/human.go
@@ -31,9 +31,10 @@ import (
 	"github.com/scionproto/scion/go/lib/scrypto/cms/protocol"
 	"github.com/scionproto/scion/go/lib/scrypto/cppki"
 	"github.com/scionproto/scion/go/lib/serrors"
+	"github.com/scionproto/scion/go/pkg/command"
 )
 
-func newHuman() *cobra.Command {
+func newHuman(pather command.Pather) *cobra.Command {
 	var flags struct {
 		format string
 		strict bool
@@ -42,8 +43,8 @@ func newHuman() *cobra.Command {
 	cmd := &cobra.Command{
 		Use:   "human",
 		Short: "Represent TRC in a human readable form",
-		Example: `  scion-pki trcs human ISD1-B1-S1.pld.der
-  scion-pki trcs human ISD1-B1-S1.trc`,
+		Example: fmt.Sprintf(`  %[1]s human ISD1-B1-S1.pld.der
+  %[1]s human ISD1-B1-S1.trc`, pather.CommandPath()),
 		Long: `'human' outputs the TRC contents in a human readable form.
 
 The input file can either be a TRC payload, or a signed TRC.
diff --git a/go/scion-pki/trcs/payload.go b/go/scion-pki/trcs/payload.go
index 6b4461f0d..a942fe0a1 100644
--- a/go/scion-pki/trcs/payload.go
+++ b/go/scion-pki/trcs/payload.go
@@ -26,10 +26,11 @@ import (
 	"github.com/scionproto/scion/go/lib/addr"
 	"github.com/scionproto/scion/go/lib/scrypto/cppki"
 	"github.com/scionproto/scion/go/lib/serrors"
+	"github.com/scionproto/scion/go/pkg/command"
 	"github.com/scionproto/scion/go/scion-pki/conf"
 )
 
-func newPayload() *cobra.Command {
+func newPayload(pather command.Pather) *cobra.Command {
 	var flags struct {
 		out  string
 		tmpl string
@@ -37,9 +38,10 @@ func newPayload() *cobra.Command {
 	}
 
 	cmd := &cobra.Command{
-		Use:     "payload",
-		Short:   "Generate new TRC payload",
-		Example: `  scion-pki trcs payload -t template.toml -o payload.der`,
+		Use:   "payload",
+		Short: "Generate new TRC payload",
+		Example: fmt.Sprintf(`  %[1]s payload -t template.toml -o payload.der`,
+			pather.CommandPath()),
 		Long: `'payload' creates the asn.1 encoded der file.
 
 To inspect the created asn.1 file you can use the openssl tool:
diff --git a/go/scion-pki/trcs/trcs.go b/go/scion-pki/trcs/trcs.go
index bff6bb5ee..d565f2aed 100644
--- a/go/scion-pki/trcs/trcs.go
+++ b/go/scion-pki/trcs/trcs.go
@@ -16,19 +16,22 @@ package trcs
 
 import (
 	"github.com/spf13/cobra"
+
+	"github.com/scionproto/scion/go/pkg/command"
 )
 
-func Cmd() *cobra.Command {
+func Cmd(pather command.Pather) *cobra.Command {
 	cmd := &cobra.Command{
 		Use:   "trcs",
 		Short: "Generate TRCs for the SCION control plane PKI",
 	}
+	joined := command.Join(pather, cmd)
 	cmd.AddCommand(
-		newCombine(),
-		newHuman(),
-		newExtract(),
-		newPayload(),
-		newVerify(),
+		newCombine(joined),
+		newHuman(joined),
+		newExtract(joined),
+		newPayload(joined),
+		newVerify(joined),
 	)
 	return cmd
 }
diff --git a/go/scion-pki/trcs/verify.go b/go/scion-pki/trcs/verify.go
index 55df84bf5..131da79e4 100644
--- a/go/scion-pki/trcs/verify.go
+++ b/go/scion-pki/trcs/verify.go
@@ -30,9 +30,10 @@ import (
 	"github.com/scionproto/scion/go/lib/scrypto/cms/protocol"
 	"github.com/scionproto/scion/go/lib/scrypto/cppki"
 	"github.com/scionproto/scion/go/lib/serrors"
+	"github.com/scionproto/scion/go/pkg/command"
 )
 
-func newVerify() *cobra.Command {
+func newVerify(pather command.Pather) *cobra.Command {
 	var flags struct {
 		anchor string
 	}
@@ -40,9 +41,8 @@ func newVerify() *cobra.Command {
 	cmd := &cobra.Command{
 		Use:   "verify",
 		Short: "Verify a TRC chain",
-		Example: `  scion-pki trcs verify --anchor bundle.pem ISD1-B1-S1.trc
-  scion-pki trcs verify --anchor ISD1-B1-S1.trc ISD1-B1-S2.trc ISD1-B1-S3.trc
-	`,
+		Example: fmt.Sprintf(`  %[1]s verify --anchor bundle.pem ISD1-B1-S1.trc
+  %[1]s verify --anchor ISD1-B1-S1.trc ISD1-B1-S2.trc ISD1-B1-S3.trc`, pather.CommandPath()),
 		Long: `'verify' verifies a TRC chain based on a trusted anchor point.
 
 The anchor can either be a collection of trusted certificates bundled in a PEM
diff --git a/go/scion/BUILD.bazel b/go/scion/BUILD.bazel
index 862ae30d5..abcd60c74 100644
--- a/go/scion/BUILD.bazel
+++ b/go/scion/BUILD.bazel
@@ -4,18 +4,15 @@ load("//:scion.bzl", "scion_go_binary")
 go_library(
     name = "go_default_library",
     srcs = [
-        "completion.go",
         "features.go",
         "ping.go",
         "scion.go",
         "showpaths.go",
-        "version.go",
     ],
     importpath = "github.com/scionproto/scion/go/scion",
     visibility = ["//visibility:private"],
     deps = [
         "//go/lib/addr:go_default_library",
-        "//go/lib/env:go_default_library",
         "//go/lib/log:go_default_library",
         "//go/lib/sciond:go_default_library",
         "//go/lib/serrors:go_default_library",
@@ -23,6 +20,7 @@ go_library(
         "//go/lib/snet/addrutil:go_default_library",
         "//go/lib/sock/reliable:go_default_library",
         "//go/pkg/app:go_default_library",
+        "//go/pkg/command:go_default_library",
         "//go/pkg/ping:go_default_library",
         "//go/pkg/showpaths:go_default_library",
         "@com_github_spf13_cobra//:go_default_library",
diff --git a/go/scion/completion.go b/go/scion/completion.go
deleted file mode 100644
index d1c51c72d..000000000
--- a/go/scion/completion.go
+++ /dev/null
@@ -1,58 +0,0 @@
-// Copyright 2020 Anapaya Systems
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//   http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package main
-
-import (
-	"os"
-
-	"github.com/spf13/cobra"
-
-	"github.com/scionproto/scion/go/lib/serrors"
-)
-
-func newCompletion() *cobra.Command {
-	var flags struct {
-		shell string
-	}
-
-	var cmd = &cobra.Command{
-		Use:   "completion",
-		Short: "Generates bash completion scripts",
-		Long: `'completion' outputs the autocomplete configuration for some shells.
-For example, you can add autocompletion for your current bash session using:
-
-    . <( scion completion )
-
-To permanently add bash autocompletion, run:
-
-    scion completion > /etc/bash_completion.d/scion
-`,
-		RunE: func(cmd *cobra.Command, args []string) error {
-			switch flags.shell {
-			case "bash":
-				return cmd.Root().GenBashCompletion(os.Stdout)
-			case "zsh":
-				return cmd.Root().GenZshCompletion(os.Stdout)
-			case "fish":
-				return cmd.Root().GenFishCompletion(os.Stdout, true)
-			default:
-				return serrors.New("unknown shell", "input", flags.shell)
-			}
-		},
-	}
-	cmd.Flags().StringVar(&flags.shell, "shell", "bash",
-		"Shell type (bash|zsh|fish)")
-	return cmd
-}
diff --git a/go/scion/ping.go b/go/scion/ping.go
index f4f462069..4db20fa1d 100644
--- a/go/scion/ping.go
+++ b/go/scion/ping.go
@@ -34,7 +34,7 @@ import (
 	"github.com/scionproto/scion/go/pkg/ping"
 )
 
-func newPing() *cobra.Command {
+func newPing(pather CommandPather) *cobra.Command {
 	var flags struct {
 		count       uint16
 		interval    time.Duration
@@ -53,7 +53,7 @@ func newPing() *cobra.Command {
 	var cmd = &cobra.Command{
 		Use:     "ping [flags] <remote>",
 		Short:   "Test connectivity to a remote SCION host using SCMP echo packets",
-		Example: "  scion ping 1-ff00:0:110,10.0.0.1",
+		Example: fmt.Sprintf("  %[1]s ping 1-ff00:0:110,10.0.0.1", pather.CommandPath()),
 		Args:    cobra.ExactArgs(1),
 		RunE: func(cmd *cobra.Command, args []string) error {
 			remote, err := snet.ParseUDPAddr(args[0])
diff --git a/go/scion/scion.go b/go/scion/scion.go
index 8939ceeb0..50d16a612 100644
--- a/go/scion/scion.go
+++ b/go/scion/scion.go
@@ -19,10 +19,17 @@ import (
 	"os"
 
 	"github.com/spf13/cobra"
+
+	"github.com/scionproto/scion/go/pkg/command"
 )
 
+// CommandPather returns the path to a command.
+type CommandPather interface {
+	CommandPath() string
+}
+
 func main() {
-	root := &cobra.Command{
+	cmd := &cobra.Command{
 		Use:   "scion",
 		Short: "A clean-slate Internet architecture",
 		Args:  cobra.NoArgs,
@@ -35,14 +42,14 @@ func main() {
 		// See https://github.com/spf13/cobra/issues/340#issuecomment-374617413.
 		SilenceErrors: true,
 	}
-	root.AddCommand(
-		newCompletion(),
-		newPing(),
-		newShowpaths(),
-		newVersion(),
+	cmd.AddCommand(
+		command.NewCompletion(cmd),
+		command.NewVersion(cmd),
+		newPing(cmd),
+		newShowpaths(cmd),
 	)
 
-	if err := root.Execute(); err != nil {
+	if err := cmd.Execute(); err != nil {
 		fmt.Fprintf(os.Stderr, "Error: %s\n", err)
 		os.Exit(1)
 	}
diff --git a/go/scion/showpaths.go b/go/scion/showpaths.go
index a26c9b3c0..229a941c5 100644
--- a/go/scion/showpaths.go
+++ b/go/scion/showpaths.go
@@ -16,6 +16,7 @@ package main
 
 import (
 	"context"
+	"fmt"
 	"os"
 	"time"
 
@@ -28,7 +29,7 @@ import (
 	"github.com/scionproto/scion/go/pkg/showpaths"
 )
 
-func newShowpaths() *cobra.Command {
+func newShowpaths(pather CommandPather) *cobra.Command {
 	var flags struct {
 		timeout    time.Duration
 		cfg        showpaths.Config
@@ -41,9 +42,9 @@ func newShowpaths() *cobra.Command {
 		Short:   "Display paths to a SCION AS",
 		Aliases: []string{"sp"},
 		Args:    cobra.ExactArgs(1),
-		Example: `  scion showpaths 1-ff00:0:110 --expiration
-  scion showpaths 1-ff00:0:110 --local 127.0.0.55 --json
-  scion showpaths 1-ff00:0:110 --no-probe`,
+		Example: fmt.Sprintf(`  %[1]s showpaths 1-ff00:0:110 --expiration
+  %[1]s showpaths 1-ff00:0:110 --local 127.0.0.55 --json
+  %[1]s showpaths 1-ff00:0:110 --no-probe`, pather.CommandPath()),
 		Long: `'showpaths' lists available paths between the local and the specified SCION ASe a.
 
 By default, the paths are probed. Paths served from the SCION Deamon's might not
diff --git a/go/scion/version.go b/go/scion/version.go
deleted file mode 100644
index 52d5e578d..000000000
--- a/go/scion/version.go
+++ /dev/null
@@ -1,34 +0,0 @@
-// Copyright 2020 Anapaya Systems
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//   http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package main
-
-import (
-	"fmt"
-
-	"github.com/spf13/cobra"
-
-	"github.com/scionproto/scion/go/lib/env"
-)
-
-func newVersion() *cobra.Command {
-	var cmd = &cobra.Command{
-		Use:   "version",
-		Short: "Show the SCION version information",
-		Run: func(cmd *cobra.Command, args []string) {
-			fmt.Println(env.VersionInfo())
-		},
-	}
-	return cmd
-}
diff --git a/go/sciond/BUILD.bazel b/go/sciond/BUILD.bazel
index 9475bb728..3dcf13eeb 100644
--- a/go/sciond/BUILD.bazel
+++ b/go/sciond/BUILD.bazel
@@ -9,11 +9,7 @@ scion_go_binary(
 
 go_library(
     name = "go_default_library",
-    srcs = [
-        "help_config.go",
-        "main.go",
-        "version.go",
-    ],
+    srcs = ["main.go"],
     importpath = "github.com/scionproto/scion/go/sciond",
     visibility = ["//visibility:private"],
     deps = [
@@ -31,6 +27,7 @@ go_library(
         "//go/lib/revcache:go_default_library",
         "//go/lib/serrors:go_default_library",
         "//go/lib/topology:go_default_library",
+        "//go/pkg/command:go_default_library",
         "//go/pkg/sciond:go_default_library",
         "//go/pkg/sciond/config:go_default_library",
         "//go/pkg/sciond/fetcher:go_default_library",
diff --git a/go/sciond/help_config.go b/go/sciond/help_config.go
deleted file mode 100644
index 480cd35ce..000000000
--- a/go/sciond/help_config.go
+++ /dev/null
@@ -1,34 +0,0 @@
-// Copyright 2020 Anapaya Systems
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//   http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package main
-
-import (
-	"os"
-
-	"github.com/spf13/cobra"
-
-	"github.com/scionproto/scion/go/pkg/sciond/config"
-)
-
-func newHelpConfig() *cobra.Command {
-	cmd := &cobra.Command{
-		Use:   "help-config",
-		Short: "Display sample configuration file",
-		Run: func(cmd *cobra.Command, args []string) {
-			(&config.Config{}).Sample(os.Stdout, nil, nil)
-		},
-	}
-	return cmd
-}
diff --git a/go/sciond/main.go b/go/sciond/main.go
index 8cc2bd85c..1cac68c4c 100644
--- a/go/sciond/main.go
+++ b/go/sciond/main.go
@@ -37,6 +37,7 @@ import (
 	"github.com/scionproto/scion/go/lib/revcache"
 	"github.com/scionproto/scion/go/lib/serrors"
 	"github.com/scionproto/scion/go/lib/topology"
+	"github.com/scionproto/scion/go/pkg/command"
 	"github.com/scionproto/scion/go/pkg/sciond"
 	"github.com/scionproto/scion/go/pkg/sciond/config"
 	"github.com/scionproto/scion/go/pkg/sciond/fetcher"
@@ -45,14 +46,19 @@ import (
 	trustmetrics "github.com/scionproto/scion/go/pkg/trust/metrics"
 )
 
+// CommandPather returns the path to a command.
+type CommandPather interface {
+	CommandPath() string
+}
+
 func main() {
 	var flags struct {
 		config string
 	}
-
-	root := &cobra.Command{
+	cmd := &cobra.Command{
 		Use:           "sciond",
 		Short:         "SCION Daemon",
+		Example:       "  sciond --config sd.toml",
 		SilenceErrors: true,
 		SilenceUsage:  true,
 		Args:          cobra.NoArgs,
@@ -60,14 +66,14 @@ func main() {
 			return run(flags.config)
 		},
 	}
-	root.AddCommand(
-		newVersion(),
-		newHelpConfig(),
+	cmd.AddCommand(
+		command.NewCompletion(cmd),
+		command.NewSample(cmd, command.NewSampleConfig(&config.Config{})),
+		command.NewVersion(cmd),
 	)
-	root.Flags().StringVar(&flags.config, "config", "", "Configuration file (required)")
-	root.MarkFlagRequired("config")
-
-	if err := root.Execute(); err != nil {
+	cmd.Flags().StringVar(&flags.config, "config", "", "Configuration file (required)")
+	cmd.MarkFlagRequired("config")
+	if err := cmd.Execute(); err != nil {
 		fmt.Fprintf(os.Stderr, "Error: %s\n", err)
 		os.Exit(1)
 	}
diff --git a/go/sciond/version.go b/go/sciond/version.go
deleted file mode 100644
index 6887e0b9f..000000000
--- a/go/sciond/version.go
+++ /dev/null
@@ -1,34 +0,0 @@
-// Copyright 2020 Anapaya Systems
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//   http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package main
-
-import (
-	"fmt"
-
-	"github.com/spf13/cobra"
-
-	"github.com/scionproto/scion/go/lib/env"
-)
-
-func newVersion() *cobra.Command {
-	cmd := &cobra.Command{
-		Use:   "version",
-		Short: "Show the SCION version information",
-		Run: func(cmd *cobra.Command, args []string) {
-			fmt.Println(env.VersionInfo())
-		},
-	}
-	return cmd
-}


Test patch:
diff --git a/go/pkg/trust/db_test.go b/go/pkg/trust/db_test.go
index 94b136785..c37aa7b8a 100644
--- a/go/pkg/trust/db_test.go
+++ b/go/pkg/trust/db_test.go
@@ -32,6 +32,7 @@ import (
 	"github.com/scionproto/scion/go/lib/addr"
 	"github.com/scionproto/scion/go/lib/scrypto/cppki"
 	"github.com/scionproto/scion/go/lib/xtest"
+	"github.com/scionproto/scion/go/pkg/command"
 	"github.com/scionproto/scion/go/pkg/trust"
 	"github.com/scionproto/scion/go/scion-pki/testcrypto"
 )
@@ -48,7 +49,7 @@ func TestUpdateCerts(t *testing.T) {
 	dir, cleanF := xtest.MustTempDir("", "tmp")
 	defer cleanF()
 
-	cmd := testcrypto.Cmd()
+	cmd := testcrypto.Cmd(command.StringPather(""))
 	cmd.SetArgs([]string{
 		"-t", "testdata/golden.topo",
 		"-l", "../../../scripts/cryptoplayground/crypto_lib.sh",
diff --git a/go/scion-pki/testcrypto/BUILD.bazel b/go/scion-pki/testcrypto/BUILD.bazel
index e628617ac..cebde0fbf 100644
--- a/go/scion-pki/testcrypto/BUILD.bazel
+++ b/go/scion-pki/testcrypto/BUILD.bazel
@@ -15,6 +15,7 @@ go_library(
         "//go/lib/scrypto/cppki:go_default_library",
         "//go/lib/serrors:go_default_library",
         "//go/lib/util:go_default_library",
+        "//go/pkg/command:go_default_library",
         "//go/scion-pki/conf:go_default_library",
         "//go/scion-pki/trcs:go_default_library",
         "@com_github_spf13_cobra//:go_default_library",
diff --git a/go/scion-pki/testcrypto/testcrypto.go b/go/scion-pki/testcrypto/testcrypto.go
index 697281787..3027cf493 100644
--- a/go/scion-pki/testcrypto/testcrypto.go
+++ b/go/scion-pki/testcrypto/testcrypto.go
@@ -32,13 +32,14 @@ import (
 	"github.com/scionproto/scion/go/lib/addr"
 	"github.com/scionproto/scion/go/lib/serrors"
 	"github.com/scionproto/scion/go/lib/util"
+	"github.com/scionproto/scion/go/pkg/command"
 	"github.com/scionproto/scion/go/scion-pki/conf"
 	"github.com/scionproto/scion/go/scion-pki/trcs"
 )
 
 const defaultCryptoLib = "./scripts/cryptoplayground/crypto_lib.sh"
 
-func Cmd() *cobra.Command {
+func Cmd(pather command.Pather) *cobra.Command {
 	var flags struct {
 		topo      string
 		out       string
@@ -53,7 +54,7 @@ func Cmd() *cobra.Command {
 	cmd := &cobra.Command{
 		Use:     "testcrypto",
 		Short:   "Generate crypto material for test topology",
-		Example: `  scion-pki testcrypto -t testing.topo -o gen`,
+		Example: fmt.Sprintf(`  %[1]s testcrypto -t testing.topo -o gen`, pather.CommandPath()),
 		Hidden:  true,
 		Long: `'testcrypto' generates the crypto material for a test topology.