Repository: regclient/regclient
Pull request #550 (2023-09-29 20:18:06)

Refactoring cobra commands to remove globals
<!--

Commits must be signed indicating your agreement to the [DCO](https://developercertificate.org/).
See [DCO missing](https://github.com/src-d/guide/blob/master/developer-community/fix-DCO.md) for steps to fix a missing signoff.

-->

### Fixes issue

Testing of cobra CLI's gets a bit complicated and error prone since all options are globals that can be modified by other tests.
<!-- If this is a bug fix, include "fixes #xxxx", or "closes #xxxx" -->

### Describe the change

Moves all initialization into setup functions that can be directly called.
<!-- Include the type of change: bug fix, new feature, breaking change, documentation update -->
<!-- Describe what was changed, why the change was made, and how it was implemented -->

### How to verify it

Hopefully there are no user visible changes, but it's a significant refactoring so no promises.
<!-- Include steps that can be taken to verify the change -->

### Changelog text

- Chore: refactoring CLIs to remove global state.
<!-- If the release changelog should have an entry for this, include it here -->

### Please verify and check that the pull request fulfills the following requirements

<!-- Mark the following with an [X] to verify they are included -->

- [X] Tests have been added or not applicable
- [X] Documentation has been added, updated, or not applicable
- [X] Changes have been rebased to main
- [X] Multiple commits to the same code have been squashed

<!-- markdownlint-disable-file MD041 -->



Patch:
diff --git a/cmd/regbot/main.go b/cmd/regbot/main.go
index acb10aa..29341af 100644
--- a/cmd/regbot/main.go
+++ b/cmd/regbot/main.go
@@ -23,7 +23,8 @@ func main() {
 	}()
 	godbg.SignalTrace()
 
-	if err := rootCmd.ExecuteContext(ctx); err != nil {
+	rootTopCmd := NewRootCmd()
+	if err := rootTopCmd.ExecuteContext(ctx); err != nil {
 		fmt.Fprintf(os.Stderr, "%v\n", err)
 		os.Exit(1)
 	}
diff --git a/cmd/regbot/root.go b/cmd/regbot/root.go
index 94550c3..b8f6fdf 100644
--- a/cmd/regbot/root.go
+++ b/cmd/regbot/root.go
@@ -24,7 +24,7 @@ More details at https://github.com/regclient/regclient`
 	UserAgent = "regclient/regbot"
 )
 
-var rootOpts struct {
+type rootCmd struct {
 	confFile  string
 	dryRun    bool
 	verbosity string
@@ -32,6 +32,7 @@ var rootOpts struct {
 	format    string // for Go template formatting of various commands
 }
 
+// TODO: remove these globals
 var (
 	conf      *Config
 	log       *logrus.Logger
@@ -39,37 +40,6 @@ var (
 	throttleC *throttle.Throttle
 )
 
-var rootCmd = &cobra.Command{
-	Use:           "regbot <cmd>",
-	Short:         "Utility for automating repository actions",
-	Long:          usageDesc,
-	SilenceUsage:  true,
-	SilenceErrors: true,
-}
-var serverCmd = &cobra.Command{
-	Use:   "server",
-	Short: "run the regbot server",
-	Long:  `Runs the various scripts according to their schedule.`,
-	Args:  cobra.RangeArgs(0, 0),
-	RunE:  runServer,
-}
-var onceCmd = &cobra.Command{
-	Use:   "once",
-	Short: "runs each script once",
-	Long: `Each script is executed once ignoring any scheduling. The command
-returns after the last script completes.`,
-	Args: cobra.RangeArgs(0, 0),
-	RunE: runOnce,
-}
-
-var versionCmd = &cobra.Command{
-	Use:   "version",
-	Short: "Show the version",
-	Long:  `Show the version`,
-	Args:  cobra.RangeArgs(0, 0),
-	RunE:  runVersion,
-}
-
 func init() {
 	log = &logrus.Logger{
 		Out:       os.Stderr,
@@ -77,24 +47,60 @@ func init() {
 		Hooks:     make(logrus.LevelHooks),
 		Level:     logrus.InfoLevel,
 	}
-	rootCmd.PersistentFlags().StringVarP(&rootOpts.confFile, "config", "c", "", "Config file")
-	rootCmd.PersistentFlags().BoolVarP(&rootOpts.dryRun, "dry-run", "", false, "Dry Run, skip all external actions")
-	rootCmd.PersistentFlags().StringVarP(&rootOpts.verbosity, "verbosity", "v", logrus.InfoLevel.String(), "Log level (debug, info, warn, error, fatal, panic)")
-	rootCmd.PersistentFlags().StringArrayVar(&rootOpts.logopts, "logopt", []string{}, "Log options")
+}
+
+func NewRootCmd() *cobra.Command {
+	rootOpts := rootCmd{}
+	var rootTopCmd = &cobra.Command{
+		Use:           "regbot <cmd>",
+		Short:         "Utility for automating repository actions",
+		Long:          usageDesc,
+		SilenceUsage:  true,
+		SilenceErrors: true,
+	}
+	var serverCmd = &cobra.Command{
+		Use:   "server",
+		Short: "run the regbot server",
+		Long:  `Runs the various scripts according to their schedule.`,
+		Args:  cobra.RangeArgs(0, 0),
+		RunE:  rootOpts.runServer,
+	}
+	var onceCmd = &cobra.Command{
+		Use:   "once",
+		Short: "runs each script once",
+		Long: `Each script is executed once ignoring any scheduling. The command
+returns after the last script completes.`,
+		Args: cobra.RangeArgs(0, 0),
+		RunE: rootOpts.runOnce,
+	}
+
+	var versionCmd = &cobra.Command{
+		Use:   "version",
+		Short: "Show the version",
+		Long:  `Show the version`,
+		Args:  cobra.RangeArgs(0, 0),
+		RunE:  rootOpts.runVersion,
+	}
+
+	rootTopCmd.PersistentFlags().StringVarP(&rootOpts.confFile, "config", "c", "", "Config file")
+	rootTopCmd.PersistentFlags().BoolVarP(&rootOpts.dryRun, "dry-run", "", false, "Dry Run, skip all external actions")
+	rootTopCmd.PersistentFlags().StringVarP(&rootOpts.verbosity, "verbosity", "v", logrus.InfoLevel.String(), "Log level (debug, info, warn, error, fatal, panic)")
+	rootTopCmd.PersistentFlags().StringArrayVar(&rootOpts.logopts, "logopt", []string{}, "Log options")
 	versionCmd.Flags().StringVarP(&rootOpts.format, "format", "", "{{printPretty .}}", "Format output with go template syntax")
 
-	_ = rootCmd.MarkPersistentFlagFilename("config")
+	_ = rootTopCmd.MarkPersistentFlagFilename("config")
 	_ = serverCmd.MarkPersistentFlagRequired("config")
 	_ = onceCmd.MarkPersistentFlagRequired("config")
 
-	rootCmd.AddCommand(serverCmd)
-	rootCmd.AddCommand(onceCmd)
-	rootCmd.AddCommand(versionCmd)
+	rootTopCmd.AddCommand(serverCmd)
+	rootTopCmd.AddCommand(onceCmd)
+	rootTopCmd.AddCommand(versionCmd)
 
-	rootCmd.PersistentPreRunE = rootPreRun
+	rootTopCmd.PersistentPreRunE = rootOpts.rootPreRun
+	return rootTopCmd
 }
 
-func rootPreRun(cmd *cobra.Command, args []string) error {
+func (rootOpts *rootCmd) rootPreRun(cmd *cobra.Command, args []string) error {
 	lvl, err := logrus.ParseLevel(rootOpts.verbosity)
 	if err != nil {
 		return err
@@ -109,14 +115,14 @@ func rootPreRun(cmd *cobra.Command, args []string) error {
 	return nil
 }
 
-func runVersion(cmd *cobra.Command, args []string) error {
+func (rootOpts *rootCmd) runVersion(cmd *cobra.Command, args []string) error {
 	info := version.GetInfo()
 	return template.Writer(os.Stdout, rootOpts.format, info)
 }
 
 // runOnce processes the file in one pass, ignoring cron
-func runOnce(cmd *cobra.Command, args []string) error {
-	err := loadConf()
+func (rootOpts *rootCmd) runOnce(cmd *cobra.Command, args []string) error {
+	err := rootOpts.loadConf()
 	if err != nil {
 		return err
 	}
@@ -129,7 +135,7 @@ func runOnce(cmd *cobra.Command, args []string) error {
 			wg.Add(1)
 			go func() {
 				defer wg.Done()
-				err := s.process(ctx)
+				err := rootOpts.process(ctx, s)
 				if err != nil {
 					if mainErr == nil {
 						mainErr = err
@@ -138,7 +144,7 @@ func runOnce(cmd *cobra.Command, args []string) error {
 				}
 			}()
 		} else {
-			err := s.process(ctx)
+			err := rootOpts.process(ctx, s)
 			if err != nil {
 				if mainErr == nil {
 					mainErr = err
@@ -151,8 +157,8 @@ func runOnce(cmd *cobra.Command, args []string) error {
 }
 
 // runServer stays running with cron scheduled tasks
-func runServer(cmd *cobra.Command, args []string) error {
-	err := loadConf()
+func (rootOpts *rootCmd) runServer(cmd *cobra.Command, args []string) error {
+	err := rootOpts.loadConf()
 	if err != nil {
 		return err
 	}
@@ -179,7 +185,7 @@ func runServer(cmd *cobra.Command, args []string) error {
 				}).Debug("Running task")
 				wg.Add(1)
 				defer wg.Done()
-				err := s.process(ctx)
+				err := rootOpts.process(ctx, s)
 				if mainErr == nil {
 					mainErr = err
 				}
@@ -214,7 +220,7 @@ func runServer(cmd *cobra.Command, args []string) error {
 	return mainErr
 }
 
-func loadConf() error {
+func (rootOpts *rootCmd) loadConf() error {
 	var err error
 	if rootOpts.confFile == "-" {
 		conf, err = ConfigLoadReader(os.Stdin)
@@ -280,7 +286,7 @@ func loadConf() error {
 }
 
 // process a sync step
-func (s ConfigScript) process(ctx context.Context) error {
+func (rootOpts *rootCmd) process(ctx context.Context, s ConfigScript) error {
 	log.WithFields(logrus.Fields{
 		"script": s.Name,
 	}).Debug("Starting script")
diff --git a/cmd/regctl/artifact.go b/cmd/regctl/artifact.go
index 99e4463..d5337d3 100644
--- a/cmd/regctl/artifact.go
+++ b/cmd/regctl/artifact.go
@@ -50,48 +50,8 @@ var configKnownTypes = []string{
 	"application/vnd.sylabs.sif.config.v1+json",
 }
 
-var artifactCmd = &cobra.Command{
-	Use:   "artifact <cmd>",
-	Short: "manage artifacts",
-}
-var artifactGetCmd = &cobra.Command{
-	Use:       "get <reference>",
-	Aliases:   []string{"pull"},
-	Short:     "download artifacts",
-	Long:      `Download artifacts from the registry.`,
-	Args:      cobra.RangeArgs(0, 1),
-	ValidArgs: []string{}, // do not auto complete repository/tag
-	RunE:      runArtifactGet,
-}
-var artifactListCmd = &cobra.Command{
-	Use:       "list <reference>",
-	Aliases:   []string{"ls"},
-	Short:     "list artifacts that have a subject to the given reference",
-	Long:      `List artifacts that have a subject to the given reference.`,
-	Args:      cobra.ExactArgs(1),
-	ValidArgs: []string{}, // do not auto complete repository/tag
-	RunE:      runArtifactList,
-}
-var artifactPutCmd = &cobra.Command{
-	Use:       "put <reference>",
-	Aliases:   []string{"push"},
-	Short:     "upload artifacts",
-	Long:      `Upload artifacts to the registry.`,
-	Args:      cobra.RangeArgs(0, 1),
-	ValidArgs: []string{}, // do not auto complete repository/tag
-	RunE:      runArtifactPut,
-}
-var artifactTreeCmd = &cobra.Command{
-	Use:       "tree <reference>",
-	Aliases:   []string{},
-	Short:     "tree listing of artifacts",
-	Long:      `Return a graph of manifests and referrers to those manifests.`,
-	Args:      cobra.ExactArgs(1),
-	ValidArgs: []string{}, // do not auto complete repository/tag
-	RunE:      runArtifactTree,
-}
-
-var artifactOpts struct {
+type artifactCmd struct {
+	rootOpts         *rootCmd
 	annotations      []string
 	artifactMT       string
 	artifactType     string
@@ -117,7 +77,52 @@ var artifactOpts struct {
 	subject          string
 }
 
-func init() {
+func NewArtifactCmd(rootOpts *rootCmd) *cobra.Command {
+	artifactOpts := artifactCmd{
+		rootOpts: rootOpts,
+	}
+
+	var artifactTopCmd = &cobra.Command{
+		Use:   "artifact <cmd>",
+		Short: "manage artifacts",
+	}
+	var artifactGetCmd = &cobra.Command{
+		Use:       "get <reference>",
+		Aliases:   []string{"pull"},
+		Short:     "download artifacts",
+		Long:      `Download artifacts from the registry.`,
+		Args:      cobra.RangeArgs(0, 1),
+		ValidArgs: []string{}, // do not auto complete repository/tag
+		RunE:      artifactOpts.runArtifactGet,
+	}
+	var artifactListCmd = &cobra.Command{
+		Use:       "list <reference>",
+		Aliases:   []string{"ls"},
+		Short:     "list artifacts that have a subject to the given reference",
+		Long:      `List artifacts that have a subject to the given reference.`,
+		Args:      cobra.ExactArgs(1),
+		ValidArgs: []string{}, // do not auto complete repository/tag
+		RunE:      artifactOpts.runArtifactList,
+	}
+	var artifactPutCmd = &cobra.Command{
+		Use:       "put <reference>",
+		Aliases:   []string{"push"},
+		Short:     "upload artifacts",
+		Long:      `Upload artifacts to the registry.`,
+		Args:      cobra.RangeArgs(0, 1),
+		ValidArgs: []string{}, // do not auto complete repository/tag
+		RunE:      artifactOpts.runArtifactPut,
+	}
+	var artifactTreeCmd = &cobra.Command{
+		Use:       "tree <reference>",
+		Aliases:   []string{},
+		Short:     "tree listing of artifacts",
+		Long:      `Return a graph of manifests and referrers to those manifests.`,
+		Args:      cobra.ExactArgs(1),
+		ValidArgs: []string{}, // do not auto complete repository/tag
+		RunE:      artifactOpts.runArtifactTree,
+	}
+
 	artifactGetCmd.Flags().StringVar(&artifactOpts.subject, "subject", "", "Get a referrer to the subject reference")
 	artifactGetCmd.Flags().StringVarP(&artifactOpts.platform, "platform", "p", "", "Specify platform of a subject (e.g. linux/amd64 or local)")
 	artifactGetCmd.Flags().StringVar(&artifactOpts.filterAT, "filter-artifact-type", "", "Filter referrers by artifactType")
@@ -177,16 +182,16 @@ func init() {
 	artifactTreeCmd.Flags().StringArrayVar(&artifactOpts.filterAnnot, "filter-annotation", []string{}, "Filter descriptors by annotation (key=value)")
 	artifactTreeCmd.Flags().StringVar(&artifactOpts.formatTree, "format", "{{printPretty .}}", "Format output with go template syntax")
 
-	artifactCmd.AddCommand(artifactGetCmd)
-	artifactCmd.AddCommand(artifactListCmd)
-	artifactCmd.AddCommand(artifactPutCmd)
-	artifactCmd.AddCommand(artifactTreeCmd)
-	rootCmd.AddCommand(artifactCmd)
+	artifactTopCmd.AddCommand(artifactGetCmd)
+	artifactTopCmd.AddCommand(artifactListCmd)
+	artifactTopCmd.AddCommand(artifactPutCmd)
+	artifactTopCmd.AddCommand(artifactTreeCmd)
+	return artifactTopCmd
 }
 
-func runArtifactGet(cmd *cobra.Command, args []string) error {
+func (artifactOpts *artifactCmd) runArtifactGet(cmd *cobra.Command, args []string) error {
 	ctx := cmd.Context()
-	rc := newRegClient()
+	rc := artifactOpts.rootOpts.newRegClient()
 
 	// validate inputs
 	if artifactOpts.refers != "" {
@@ -461,7 +466,7 @@ func runArtifactGet(cmd *cobra.Command, args []string) error {
 	return nil
 }
 
-func runArtifactList(cmd *cobra.Command, args []string) error {
+func (artifactOpts *artifactCmd) runArtifactList(cmd *cobra.Command, args []string) error {
 	ctx := cmd.Context()
 
 	// validate inputs
@@ -473,7 +478,7 @@ func runArtifactList(cmd *cobra.Command, args []string) error {
 		return fmt.Errorf("--latest cannot be used with --sort-annotation")
 	}
 
-	rc := newRegClient()
+	rc := artifactOpts.rootOpts.newRegClient()
 	defer rc.Close(ctx, rSubject)
 
 	matchOpts := types.MatchOpt{
@@ -556,7 +561,7 @@ func runArtifactList(cmd *cobra.Command, args []string) error {
 	return template.Writer(cmd.OutOrStdout(), artifactOpts.formatList, rl)
 }
 
-func runArtifactPut(cmd *cobra.Command, args []string) error {
+func (artifactOpts *artifactCmd) runArtifactPut(cmd *cobra.Command, args []string) error {
 	ctx := cmd.Context()
 	mOpts := []manifest.Opts{}
 	hasConfig := false
@@ -652,7 +657,7 @@ func runArtifactPut(cmd *cobra.Command, args []string) error {
 	}
 
 	// setup regclient
-	rc := newRegClient()
+	rc := artifactOpts.rootOpts.newRegClient()
 	defer rc.Close(ctx, r)
 
 	var subjectDesc *types.Descriptor
@@ -915,7 +920,7 @@ func runArtifactPut(cmd *cobra.Command, args []string) error {
 	return template.Writer(cmd.OutOrStdout(), artifactOpts.formatPut, result)
 }
 
-func runArtifactTree(cmd *cobra.Command, args []string) error {
+func (artifactOpts *artifactCmd) runArtifactTree(cmd *cobra.Command, args []string) error {
 	ctx := cmd.Context()
 
 	// validate inputs
@@ -924,7 +929,7 @@ func runArtifactTree(cmd *cobra.Command, args []string) error {
 		return err
 	}
 
-	rc := newRegClient()
+	rc := artifactOpts.rootOpts.newRegClient()
 	defer rc.Close(ctx, r)
 
 	referrerOpts := []scheme.ReferrerOpts{}
@@ -955,7 +960,7 @@ func runArtifactTree(cmd *cobra.Command, args []string) error {
 	}
 
 	seen := []string{}
-	tr, err := treeAddResult(ctx, rc, r, seen, referrerOpts, tags)
+	tr, err := artifactOpts.treeAddResult(ctx, rc, r, seen, referrerOpts, tags)
 	var twErr error
 	if tr != nil {
 		twErr = template.Writer(cmd.OutOrStdout(), artifactOpts.formatTree, tr)
@@ -966,7 +971,7 @@ func runArtifactTree(cmd *cobra.Command, args []string) error {
 	return twErr
 }
 
-func treeAddResult(ctx context.Context, rc *regclient.RegClient, r ref.Ref, seen []string, rOpts []scheme.ReferrerOpts, tags []string) (*treeResult, error) {
+func (artifactOpts *artifactCmd) treeAddResult(ctx context.Context, rc *regclient.RegClient, r ref.Ref, seen []string, rOpts []scheme.ReferrerOpts, tags []string) (*treeResult, error) {
 	tr := treeResult{
 		Ref: r,
 	}
@@ -1003,7 +1008,7 @@ func treeAddResult(ctx context.Context, rc *regclient.RegClient, r ref.Ref, seen
 			rChild := r
 			rChild.Tag = ""
 			rChild.Digest = d.Digest.String()
-			tChild, err := treeAddResult(ctx, rc, rChild, seen, rOpts, tags)
+			tChild, err := artifactOpts.treeAddResult(ctx, rc, rChild, seen, rOpts, tags)
 			if tChild != nil {
 				tChild.ArtifactType = d.ArtifactType
 				if d.Platform != nil {
@@ -1029,7 +1034,7 @@ func treeAddResult(ctx context.Context, rc *regclient.RegClient, r ref.Ref, seen
 			rReferrer := r
 			rReferrer.Tag = ""
 			rReferrer.Digest = d.Digest.String()
-			tReferrer, err := treeAddResult(ctx, rc, rReferrer, seen, rOpts, tags)
+			tReferrer, err := artifactOpts.treeAddResult(ctx, rc, rReferrer, seen, rOpts, tags)
 			if tReferrer != nil {
 				tReferrer.ArtifactType = d.ArtifactType
 				if d.Platform != nil {
@@ -1055,7 +1060,7 @@ func treeAddResult(ctx context.Context, rc *regclient.RegClient, r ref.Ref, seen
 				rTag := r
 				rTag.Tag = t
 				rTag.Digest = ""
-				tReferrer, err := treeAddResult(ctx, rc, rTag, seen, rOpts, tags)
+				tReferrer, err := artifactOpts.treeAddResult(ctx, rc, rTag, seen, rOpts, tags)
 				if tReferrer != nil {
 					tReferrer.Ref.Tag = t
 					tReferrer.Ref.Digest = ""
diff --git a/cmd/regctl/blob.go b/cmd/regctl/blob.go
index 5fc46ee..068a9d6 100644
--- a/cmd/regctl/blob.go
+++ b/cmd/regctl/blob.go
@@ -23,79 +23,8 @@ import (
 	"github.com/spf13/cobra"
 )
 
-var blobCmd = &cobra.Command{
-	Use:     "blob <cmd>",
-	Aliases: []string{"layer"},
-	Short:   "manage image blobs/layers",
-}
-var blobDiffConfigCmd = &cobra.Command{
-	Use:       "diff-config <repository> <digest> <repository> <digest>",
-	Short:     "diff two image configs",
-	Long:      `This returns the difference between two configs, comparing the contents of each config json.`,
-	Args:      cobra.ExactArgs(4),
-	ValidArgs: []string{}, // do not auto complete repository or digest
-	RunE:      runBlobDiffConfig,
-}
-var blobDiffLayerCmd = &cobra.Command{
-	Use:       "diff-layer <repository> <digest> <repository> <digest>",
-	Short:     "diff two tar layers",
-	Long:      `This returns the difference between two layers, comparing the contents of each tar.`,
-	Args:      cobra.ExactArgs(4),
-	ValidArgs: []string{}, // do not auto complete repository or digest
-	RunE:      runBlobDiffLayer,
-}
-var blobGetCmd = &cobra.Command{
-	Use:     "get <repository> <digest>",
-	Aliases: []string{"pull"},
-	Short:   "download a blob/layer",
-	Long: `Download a blob from the registry. The output is the blob itself which may
-be a compressed tar file, a json config, or any other blob supported by the
-registry. The blob or layer digest can be found in the image manifest.`,
-	Args:      cobra.ExactArgs(2),
-	ValidArgs: []string{}, // do not auto complete repository or digest
-	RunE:      runBlobGet,
-}
-var blobGetFileCmd = &cobra.Command{
-	Use:       "get-file <repository> <digest> <file> [out-file]",
-	Aliases:   []string{"cat"},
-	Short:     "get a file from a layer",
-	Long:      `This returns a requested file from a layer.`,
-	Args:      cobra.RangeArgs(3, 4),
-	ValidArgs: []string{}, // do not auto complete repository, digest, or filenames
-	RunE:      runBlobGetFile,
-}
-var blobHeadCmd = &cobra.Command{
-	Use:       "head <repository> <digest>",
-	Aliases:   []string{"digest"},
-	Short:     "http head request for a blob",
-	Long:      `Shows the headers for a blob head request.`,
-	Args:      cobra.ExactArgs(2),
-	ValidArgs: []string{}, // do not auto complete repository or digest
-	RunE:      runBlobHead,
-}
-var blobPutCmd = &cobra.Command{
-	Use:     "put <repository>",
-	Aliases: []string{"push"},
-	Short:   "upload a blob/layer",
-	Long: `Upload a blob to a repository. Stdin must be the blob contents. The output
-is the digest of the blob.`,
-	Args:      cobra.ExactArgs(1),
-	ValidArgs: []string{}, // do not auto complete repository
-	RunE:      runBlobPut,
-}
-var blobCopyCmd = &cobra.Command{
-	Use:     "copy <src_image_ref> <dst_image_ref> <digest>",
-	Aliases: []string{"cp"},
-	Short:   "copy blob",
-	Long: `Copy a blob between repositories. This works in the same registry only. It
-attempts to mount the layers between repositories. And within the same repository
-it only sends the manifest with the new tag.`,
-	Args:      cobra.ExactArgs(3),
-	ValidArgs: []string{}, // do not auto complete repository or digest
-	RunE:      runBlobCopy,
-}
-
-var blobOpts struct {
+type blobCmd struct {
+	rootOpts       *rootCmd
 	diffCtx        int
 	diffFullCtx    bool
 	diffIgnoreTime bool
@@ -107,7 +36,83 @@ var blobOpts struct {
 	digest         string
 }
 
-func init() {
+func NewBlobCmd(rootOpts *rootCmd) *cobra.Command {
+	blobOpts := blobCmd{
+		rootOpts: rootOpts,
+	}
+
+	var blobTopCmd = &cobra.Command{
+		Use:     "blob <cmd>",
+		Aliases: []string{"layer"},
+		Short:   "manage image blobs/layers",
+	}
+	var blobDiffConfigCmd = &cobra.Command{
+		Use:       "diff-config <repository> <digest> <repository> <digest>",
+		Short:     "diff two image configs",
+		Long:      `This returns the difference between two configs, comparing the contents of each config json.`,
+		Args:      cobra.ExactArgs(4),
+		ValidArgs: []string{}, // do not auto complete repository or digest
+		RunE:      blobOpts.runBlobDiffConfig,
+	}
+	var blobDiffLayerCmd = &cobra.Command{
+		Use:       "diff-layer <repository> <digest> <repository> <digest>",
+		Short:     "diff two tar layers",
+		Long:      `This returns the difference between two layers, comparing the contents of each tar.`,
+		Args:      cobra.ExactArgs(4),
+		ValidArgs: []string{}, // do not auto complete repository or digest
+		RunE:      blobOpts.runBlobDiffLayer,
+	}
+	var blobGetCmd = &cobra.Command{
+		Use:     "get <repository> <digest>",
+		Aliases: []string{"pull"},
+		Short:   "download a blob/layer",
+		Long: `Download a blob from the registry. The output is the blob itself which may
+be a compressed tar file, a json config, or any other blob supported by the
+registry. The blob or layer digest can be found in the image manifest.`,
+		Args:      cobra.ExactArgs(2),
+		ValidArgs: []string{}, // do not auto complete repository or digest
+		RunE:      blobOpts.runBlobGet,
+	}
+	var blobGetFileCmd = &cobra.Command{
+		Use:       "get-file <repository> <digest> <file> [out-file]",
+		Aliases:   []string{"cat"},
+		Short:     "get a file from a layer",
+		Long:      `This returns a requested file from a layer.`,
+		Args:      cobra.RangeArgs(3, 4),
+		ValidArgs: []string{}, // do not auto complete repository, digest, or filenames
+		RunE:      blobOpts.runBlobGetFile,
+	}
+	var blobHeadCmd = &cobra.Command{
+		Use:       "head <repository> <digest>",
+		Aliases:   []string{"digest"},
+		Short:     "http head request for a blob",
+		Long:      `Shows the headers for a blob head request.`,
+		Args:      cobra.ExactArgs(2),
+		ValidArgs: []string{}, // do not auto complete repository or digest
+		RunE:      blobOpts.runBlobHead,
+	}
+	var blobPutCmd = &cobra.Command{
+		Use:     "put <repository>",
+		Aliases: []string{"push"},
+		Short:   "upload a blob/layer",
+		Long: `Upload a blob to a repository. Stdin must be the blob contents. The output
+is the digest of the blob.`,
+		Args:      cobra.ExactArgs(1),
+		ValidArgs: []string{}, // do not auto complete repository
+		RunE:      blobOpts.runBlobPut,
+	}
+	var blobCopyCmd = &cobra.Command{
+		Use:     "copy <src_image_ref> <dst_image_ref> <digest>",
+		Aliases: []string{"cp"},
+		Short:   "copy blob",
+		Long: `Copy a blob between repositories. This works in the same registry only. It
+attempts to mount the layers between repositories. And within the same repository
+it only sends the manifest with the new tag.`,
+		Args:      cobra.ExactArgs(3),
+		ValidArgs: []string{}, // do not auto complete repository or digest
+		RunE:      blobOpts.runBlobCopy,
+	}
+
 	blobDiffConfigCmd.Flags().IntVarP(&blobOpts.diffCtx, "context", "", 3, "Lines of context")
 	blobDiffConfigCmd.Flags().BoolVarP(&blobOpts.diffFullCtx, "context-full", "", false, "Show all lines of context")
 
@@ -141,17 +146,18 @@ func init() {
 	_ = blobPutCmd.RegisterFlagCompletionFunc("digest", completeArgNone)
 	_ = blobPutCmd.Flags().MarkHidden("content-type")
 
-	blobCmd.AddCommand(blobDiffConfigCmd)
-	blobCmd.AddCommand(blobDiffLayerCmd)
-	blobCmd.AddCommand(blobGetCmd)
-	blobCmd.AddCommand(blobGetFileCmd)
-	blobCmd.AddCommand(blobHeadCmd)
-	blobCmd.AddCommand(blobPutCmd)
-	blobCmd.AddCommand(blobCopyCmd)
-	rootCmd.AddCommand(blobCmd)
+	blobTopCmd.AddCommand(blobDiffConfigCmd)
+	blobTopCmd.AddCommand(blobDiffLayerCmd)
+	blobTopCmd.AddCommand(blobGetCmd)
+	blobTopCmd.AddCommand(blobGetFileCmd)
+	blobTopCmd.AddCommand(blobHeadCmd)
+	blobTopCmd.AddCommand(blobPutCmd)
+	blobTopCmd.AddCommand(blobCopyCmd)
+
+	return blobTopCmd
 }
 
-func runBlobDiffConfig(cmd *cobra.Command, args []string) error {
+func (blobOpts *blobCmd) runBlobDiffConfig(cmd *cobra.Command, args []string) error {
 	diffOpts := []diff.Opt{}
 	if blobOpts.diffCtx > 0 {
 		diffOpts = append(diffOpts, diff.WithContext(blobOpts.diffCtx, blobOpts.diffCtx))
@@ -168,7 +174,7 @@ func runBlobDiffConfig(cmd *cobra.Command, args []string) error {
 	if err != nil {
 		return err
 	}
-	rc := newRegClient()
+	rc := blobOpts.rootOpts.newRegClient()
 
 	// open both configs, and output each as formatted json
 	d1, err := digest.Parse(args[1])
@@ -205,7 +211,7 @@ func runBlobDiffConfig(cmd *cobra.Command, args []string) error {
 	// return template.Writer(cmd.OutOrStdout(), blobOpts.format, cDiff)
 }
 
-func runBlobDiffLayer(cmd *cobra.Command, args []string) error {
+func (blobOpts *blobCmd) runBlobDiffLayer(cmd *cobra.Command, args []string) error {
 	diffOpts := []diff.Opt{}
 	if blobOpts.diffCtx > 0 {
 		diffOpts = append(diffOpts, diff.WithContext(blobOpts.diffCtx, blobOpts.diffCtx))
@@ -222,7 +228,7 @@ func runBlobDiffLayer(cmd *cobra.Command, args []string) error {
 	if err != nil {
 		return err
 	}
-	rc := newRegClient()
+	rc := blobOpts.rootOpts.newRegClient()
 
 	// open both blobs, and generate reports of each content
 	d1, err := digest.Parse(args[1])
@@ -242,7 +248,7 @@ func runBlobDiffLayer(cmd *cobra.Command, args []string) error {
 	if err != nil {
 		return err
 	}
-	rep1, err := blobReportLayer(tr1)
+	rep1, err := blobOpts.blobReportLayer(tr1)
 	if err != nil {
 		return err
 	}
@@ -268,7 +274,7 @@ func runBlobDiffLayer(cmd *cobra.Command, args []string) error {
 	if err != nil {
 		return err
 	}
-	rep2, err := blobReportLayer(tr2)
+	rep2, err := blobOpts.blobReportLayer(tr2)
 	if err != nil {
 		return err
 	}
@@ -283,7 +289,7 @@ func runBlobDiffLayer(cmd *cobra.Command, args []string) error {
 	return err
 }
 
-func runBlobGet(cmd *cobra.Command, args []string) error {
+func (blobOpts *blobCmd) runBlobGet(cmd *cobra.Command, args []string) error {
 	ctx := cmd.Context()
 	r, err := ref.New(args[0])
 	if err != nil {
@@ -293,7 +299,7 @@ func runBlobGet(cmd *cobra.Command, args []string) error {
 	if err != nil {
 		return err
 	}
-	rc := newRegClient()
+	rc := blobOpts.rootOpts.newRegClient()
 	defer rc.Close(ctx, r)
 	if blobOpts.mt != "" {
 		log.WithFields(logrus.Fields{
@@ -327,7 +333,7 @@ func runBlobGet(cmd *cobra.Command, args []string) error {
 	return template.Writer(cmd.OutOrStdout(), blobOpts.formatGet, blob)
 }
 
-func runBlobGetFile(cmd *cobra.Command, args []string) error {
+func (blobOpts *blobCmd) runBlobGetFile(cmd *cobra.Command, args []string) error {
 	ctx := cmd.Context()
 	r, err := ref.New(args[0])
 	if err != nil {
@@ -339,7 +345,7 @@ func runBlobGetFile(cmd *cobra.Command, args []string) error {
 	}
 	filename := args[2]
 	filename = strings.TrimPrefix(filename, "/")
-	rc := newRegClient()
+	rc := blobOpts.rootOpts.newRegClient()
 	defer rc.Close(ctx, r)
 
 	log.WithFields(logrus.Fields{
@@ -392,7 +398,7 @@ func runBlobGetFile(cmd *cobra.Command, args []string) error {
 	return nil
 }
 
-func runBlobHead(cmd *cobra.Command, args []string) error {
+func (blobOpts *blobCmd) runBlobHead(cmd *cobra.Command, args []string) error {
 	ctx := cmd.Context()
 	r, err := ref.New(args[0])
 	if err != nil {
@@ -402,7 +408,7 @@ func runBlobHead(cmd *cobra.Command, args []string) error {
 	if err != nil {
 		return err
 	}
-	rc := newRegClient()
+	rc := blobOpts.rootOpts.newRegClient()
 	defer rc.Close(ctx, r)
 
 	log.WithFields(logrus.Fields{
@@ -423,13 +429,13 @@ func runBlobHead(cmd *cobra.Command, args []string) error {
 	return template.Writer(cmd.OutOrStdout(), blobOpts.formatHead, blob)
 }
 
-func runBlobPut(cmd *cobra.Command, args []string) error {
+func (blobOpts *blobCmd) runBlobPut(cmd *cobra.Command, args []string) error {
 	ctx := cmd.Context()
 	r, err := ref.New(args[0])
 	if err != nil {
 		return err
 	}
-	rc := newRegClient()
+	rc := blobOpts.rootOpts.newRegClient()
 
 	if blobOpts.mt != "" {
 		log.WithFields(logrus.Fields{
@@ -458,7 +464,7 @@ func runBlobPut(cmd *cobra.Command, args []string) error {
 	return template.Writer(cmd.OutOrStdout(), blobOpts.formatPut, result)
 }
 
-func runBlobCopy(cmd *cobra.Command, args []string) error {
+func (blobOpts *blobCmd) runBlobCopy(cmd *cobra.Command, args []string) error {
 	ctx := cmd.Context()
 	rSrc, err := ref.New(args[0])
 	if err != nil {
@@ -472,7 +478,7 @@ func runBlobCopy(cmd *cobra.Command, args []string) error {
 	if err != nil {
 		return err
 	}
-	rc := newRegClient()
+	rc := blobOpts.rootOpts.newRegClient()
 	defer rc.Close(ctx, rSrc)
 
 	log.WithFields(logrus.Fields{
@@ -487,7 +493,7 @@ func runBlobCopy(cmd *cobra.Command, args []string) error {
 	return nil
 }
 
-func blobReportLayer(tr *tar.Reader) ([]string, error) {
+func (blobOpts *blobCmd) blobReportLayer(tr *tar.Reader) ([]string, error) {
 	report := []string{}
 	if tr == nil {
 		return report, nil
diff --git a/cmd/regctl/completion.go b/cmd/regctl/completion.go
index 0518c55..54383cd 100644
--- a/cmd/regctl/completion.go
+++ b/cmd/regctl/completion.go
@@ -11,10 +11,11 @@ import (
 	"github.com/spf13/cobra"
 )
 
-var completionCmd = &cobra.Command{
-	Use:   "completion [bash|zsh|fish|powershell]",
-	Short: "Generate completion script",
-	Long: fmt.Sprintf(`To load completions:
+func NewCompletionCmd(rootOpts *rootCmd) *cobra.Command {
+	var completionTopCmd = &cobra.Command{
+		Use:   "completion [bash|zsh|fish|powershell]",
+		Short: "Generate completion script",
+		Long: fmt.Sprintf(`To load completions:
 
 Bash:
 
@@ -52,30 +53,40 @@ PowerShell:
   # To load completions for every new session, run:
   PS> %[1]s completion powershell > %[1]s.ps1
   # and source this file from your PowerShell profile.
-`, rootCmd.Root().Name()),
-	DisableFlagsInUseLine: true,
-	ValidArgs:             []string{"bash", "zsh", "fish", "powershell"},
-	Args:                  cobra.MatchAll(cobra.ExactArgs(1), cobra.OnlyValidArgs),
-	Run: func(cmd *cobra.Command, args []string) {
-		switch args[0] {
-		case "bash":
-			_ = cmd.Root().GenBashCompletionV2(os.Stdout, true)
-		case "zsh":
-			_ = cmd.Root().GenZshCompletion(os.Stdout)
-		case "fish":
-			_ = cmd.Root().GenFishCompletion(os.Stdout, true)
-		case "powershell":
-			_ = cmd.Root().GenPowerShellCompletionWithDesc(os.Stdout)
-		}
-	},
-}
+`, rootOpts.name),
+		DisableFlagsInUseLine: true,
+		ValidArgs:             []string{"bash", "zsh", "fish", "powershell"},
+		Args:                  cobra.MatchAll(cobra.ExactArgs(1), cobra.OnlyValidArgs),
+		Run: func(cmd *cobra.Command, args []string) {
+			switch args[0] {
+			case "bash":
+				_ = cmd.Root().GenBashCompletionV2(os.Stdout, true)
+			case "zsh":
+				_ = cmd.Root().GenZshCompletion(os.Stdout)
+			case "fish":
+				_ = cmd.Root().GenFishCompletion(os.Stdout, true)
+			case "powershell":
+				_ = cmd.Root().GenPowerShellCompletionWithDesc(os.Stdout)
+			}
+		},
+	}
 
-func init() {
-	rootCmd.AddCommand(completionCmd)
+	return completionTopCmd
 }
 
 type completeFunc func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective)
 
+// completeArgList takes a list of completion functions and completes each arg separately
+func completeArgList(funcList []completeFunc) completeFunc {
+	return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
+		pos := len(args)
+		if pos >= len(funcList) {
+			return nil, cobra.ShellCompDirectiveNoFileComp
+		}
+		return funcList[pos](cmd, args, toComplete)
+	}
+}
+
 func completeArgNone(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
 	return nil, cobra.ShellCompDirectiveNoFileComp
 }
@@ -104,18 +115,7 @@ func completeArgMediaTypeManifest(cmd *cobra.Command, args []string, toComplete
 	}, cobra.ShellCompDirectiveNoFileComp
 }
 
-// completeArgList takes a list of completion functions and completes each arg separately
-func completeArgList(funcList []completeFunc) completeFunc {
-	return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
-		pos := len(args)
-		if pos >= len(funcList) {
-			return nil, cobra.ShellCompDirectiveNoFileComp
-		}
-		return funcList[pos](cmd, args, toComplete)
-	}
-}
-
-func completeArgTag(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
+func (rootOpts *rootCmd) completeArgTag(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
 	result := []string{}
 	// TODO: is it possible to expand registry, then repo, then tag?
 	input := strings.TrimRight(toComplete, ":")
@@ -123,7 +123,7 @@ func completeArgTag(cmd *cobra.Command, args []string, toComplete string) ([]str
 	if err != nil || r.Digest != "" {
 		return result, cobra.ShellCompDirectiveNoFileComp
 	}
-	rc := newRegClient()
+	rc := rootOpts.newRegClient()
 	tl, err := rc.TagList(context.Background(), r)
 	if err != nil {
 		return result, cobra.ShellCompDirectiveNoFileComp
diff --git a/cmd/regctl/config.go b/cmd/regctl/config.go
index f249470..7c86fb9 100644
--- a/cmd/regctl/config.go
+++ b/cmd/regctl/config.go
@@ -33,44 +33,49 @@ type Config struct {
 	IncDockerCred *bool                   `json:"incDockerCred,omitempty"`
 }
 
-var configOpts struct {
+type configCmd struct {
+	rootOpts   *rootCmd
 	blobLimit  int64
 	dockerCert bool
 	dockerCred bool
 	format     string
 }
-var configCmd = &cobra.Command{
-	Use:   "config <cmd>",
-	Short: "read/set configuration options",
-}
-var configGetCmd = &cobra.Command{
-	Use:   "get",
-	Short: "show the config",
-	Long:  `Displays the configuration. Passwords are not included in the output.`,
-	Args:  cobra.ExactArgs(0),
-	RunE:  runConfigGet,
-}
-var configSetCmd = &cobra.Command{
-	Use:   "set",
-	Short: "set a configuration option",
-	Long:  `Modifies an option used in future executions.`,
-	Args:  cobra.ExactArgs(0),
-	RunE:  runConfigSet,
-}
 
-func init() {
+func NewConfigCmd(rootOpts *rootCmd) *cobra.Command {
+	configOpts := configCmd{
+		rootOpts: rootOpts,
+	}
+	var configTopCmd = &cobra.Command{
+		Use:   "config <cmd>",
+		Short: "read/set configuration options",
+	}
+	var configGetCmd = &cobra.Command{
+		Use:   "get",
+		Short: "show the config",
+		Long:  `Displays the configuration. Passwords are not included in the output.`,
+		Args:  cobra.ExactArgs(0),
+		RunE:  configOpts.runConfigGet,
+	}
+	var configSetCmd = &cobra.Command{
+		Use:   "set",
+		Short: "set a configuration option",
+		Long:  `Modifies an option used in future executions.`,
+		Args:  cobra.ExactArgs(0),
+		RunE:  configOpts.runConfigSet,
+	}
+
 	configGetCmd.Flags().StringVar(&configOpts.format, "format", "{{ printPretty . }}", "format the output with Go template syntax")
 
 	configSetCmd.Flags().Int64Var(&configOpts.blobLimit, "blob-limit", 0, "limit for blob chunks, this is stored in memory")
 	configSetCmd.Flags().BoolVar(&configOpts.dockerCert, "docker-cert", false, "load certificates from docker")
 	configSetCmd.Flags().BoolVar(&configOpts.dockerCred, "docker-cred", false, "load credentials from docker")
 
-	configCmd.AddCommand(configGetCmd)
-	configCmd.AddCommand(configSetCmd)
-	rootCmd.AddCommand(configCmd)
+	configTopCmd.AddCommand(configGetCmd)
+	configTopCmd.AddCommand(configSetCmd)
+	return configTopCmd
 }
 
-func runConfigGet(cmd *cobra.Command, args []string) error {
+func (configOpts *configCmd) runConfigGet(cmd *cobra.Command, args []string) error {
 	c, err := ConfigLoadDefault()
 	if err != nil {
 		return err
@@ -83,7 +88,7 @@ func runConfigGet(cmd *cobra.Command, args []string) error {
 	return template.Writer(cmd.OutOrStdout(), configOpts.format, c)
 }
 
-func runConfigSet(cmd *cobra.Command, args []string) error {
+func (configOpts *configCmd) runConfigSet(cmd *cobra.Command, args []string) error {
 	c, err := ConfigLoadDefault()
 	if err != nil {
 		return err
diff --git a/cmd/regctl/digest.go b/cmd/regctl/digest.go
index 85cae5b..1d3ed0d 100644
--- a/cmd/regctl/digest.go
+++ b/cmd/regctl/digest.go
@@ -13,20 +13,27 @@ import (
 	"github.com/spf13/cobra"
 )
 
-// TODO: identify a more appropriate location for this command, leave it hidden until then
-var digestCmd = &cobra.Command{
-	Hidden: true,
-	Use:    "digest",
-	Short:  "compute digest on stdin",
-	Args:   cobra.RangeArgs(0, 0),
-	RunE:   runDigest,
+type digestCmd struct {
+	rootOpts *rootCmd
 }
 
-func init() {
-	rootCmd.AddCommand(digestCmd)
+func NewDigestCmd(rootOpts *rootCmd) *cobra.Command {
+	digestOpts := digestCmd{
+		rootOpts: rootOpts,
+	}
+	// TODO: identify a more appropriate location for this command, leave it hidden until then
+	var digestCmd = &cobra.Command{
+		Hidden: true,
+		Use:    "digest",
+		Short:  "compute digest on stdin",
+		Args:   cobra.RangeArgs(0, 0),
+		RunE:   digestOpts.runDigest,
+	}
+
+	return digestCmd
 }
 
-func runDigest(cmd *cobra.Command, args []string) error {
+func (digestOpts *digestCmd) runDigest(cmd *cobra.Command, args []string) error {
 	digester := digest.Canonical.Digester()
 
 	_, err := io.Copy(digester.Hash(), os.Stdin)
diff --git a/cmd/regctl/image.go b/cmd/regctl/image.go
index 16b986b..0a498e3 100644
--- a/cmd/regctl/image.go
+++ b/cmd/regctl/image.go
@@ -27,110 +27,142 @@ import (
 	"github.com/spf13/cobra"
 )
 
-var imageCmd = &cobra.Command{
-	Use:   "image <cmd>",
-	Short: "manage images",
+type imageCmd struct {
+	rootOpts        *rootCmd
+	checkBaseRef    string
+	checkBaseDigest string
+	checkSkipConfig bool
+	create          string
+	exportCompress  bool
+	exportRef       string
+	fastCheck       bool
+	forceRecursive  bool
+	format          string
+	formatFile      string
+	importName      string
+	includeExternal bool
+	digestTags      bool
+	list            bool
+	modOpts         []mod.Opts
+	platform        string
+	platforms       []string
+	referrers       bool
+	replace         bool
+	requireList     bool
 }
-var imageCheckBaseCmd = &cobra.Command{
-	Use:     "check-base <image_ref>",
-	Aliases: []string{},
-	Short:   "check if the base image has changed",
-	Long: `Check the base image (found using annotations or an option).
+
+func NewImageCmd(rootOpts *rootCmd) *cobra.Command {
+	imageOpts := imageCmd{
+		rootOpts: rootOpts,
+	}
+	// TODO: is there a better way to define an alias across parent commands?
+	manifestOpts := manifestCmd{
+		rootOpts: rootOpts,
+	}
+	var imageTopCmd = &cobra.Command{
+		Use:   "image <cmd>",
+		Short: "manage images",
+	}
+	var imageCheckBaseCmd = &cobra.Command{
+		Use:     "check-base <image_ref>",
+		Aliases: []string{},
+		Short:   "check if the base image has changed",
+		Long: `Check the base image (found using annotations or an option).
 If the base name is not provided, annotations will be checked in the image.
 If the digest is available, this checks if that matches the base name.
 If the digest is not available, layers of each manifest are compared.
 If the layers match, the config (history and roots) are optionally compared.	
 If the base image does not match, the command exits with a non-zero status.
 Use "-v info" to see more details.`,
-	Args:              cobra.ExactArgs(1),
-	ValidArgsFunction: completeArgTag,
-	RunE:              runImageCheckBase,
-}
-var imageCopyCmd = &cobra.Command{
-	Use:     "copy <src_image_ref> <dst_image_ref>",
-	Aliases: []string{"cp"},
-	Short:   "copy or retag image",
-	Long: `Copy or retag an image. This works between registries and only pulls layers
+		Args:              cobra.ExactArgs(1),
+		ValidArgsFunction: rootOpts.completeArgTag,
+		RunE:              imageOpts.runImageCheckBase,
+	}
+	var imageCopyCmd = &cobra.Command{
+		Use:     "copy <src_image_ref> <dst_image_ref>",
+		Aliases: []string{"cp"},
+		Short:   "copy or retag image",
+		Long: `Copy or retag an image. This works between registries and only pulls layers
 that do not exist at the target. In the same registry it attempts to mount
 the layers between repositories. And within the same repository it only
 sends the manifest with the new tag.`,
-	Args:              cobra.ExactArgs(2),
-	ValidArgsFunction: completeArgTag,
-	RunE:              runImageCopy,
-}
-var imageDeleteCmd = &cobra.Command{
-	Use:     "delete <image_ref>",
-	Aliases: []string{"del", "rm", "remove"},
-	Short:   "delete image, same as \"manifest delete\"",
-	Long: `Delete a manifest. This will delete the manifest, and all tags pointing to that
+		Args:              cobra.ExactArgs(2),
+		ValidArgsFunction: rootOpts.completeArgTag,
+		RunE:              imageOpts.runImageCopy,
+	}
+	var imageDeleteCmd = &cobra.Command{
+		Use:     "delete <image_ref>",
+		Aliases: []string{"del", "rm", "remove"},
+		Short:   "delete image, same as \"manifest delete\"",
+		Long: `Delete a manifest. This will delete the manifest, and all tags pointing to that
 manifest. You must specify a digest, not a tag on this command (e.g. 
 image_name@sha256:1234abc...). It is up to the registry whether the delete
 API is supported. Additionally, registries may garbage collect the filesystem
 layers (blobs) separately or not at all. See also the "tag delete" command.`,
-	Args:      cobra.ExactArgs(1),
-	ValidArgs: []string{}, // do not auto complete digests
-	RunE:      runManifestDelete,
-}
-var imageDigestCmd = &cobra.Command{
-	Use:               "digest <image_ref>",
-	Short:             "show digest for pinning, same as \"manifest digest\"",
-	Args:              cobra.ExactArgs(1),
-	ValidArgsFunction: completeArgTag,
-	RunE:              runManifestHead,
-}
-var imageExportCmd = &cobra.Command{
-	Use:   "export <image_ref> [filename]",
-	Short: "export image",
-	Long: `Exports an image into a tar file that can be later loaded into a docker
+		Args:      cobra.ExactArgs(1),
+		ValidArgs: []string{}, // do not auto complete digests
+		RunE:      manifestOpts.runManifestDelete,
+	}
+	var imageDigestCmd = &cobra.Command{
+		Use:               "digest <image_ref>",
+		Short:             "show digest for pinning, same as \"manifest digest\"",
+		Args:              cobra.ExactArgs(1),
+		ValidArgsFunction: rootOpts.completeArgTag,
+		RunE:              manifestOpts.runManifestHead,
+	}
+	var imageExportCmd = &cobra.Command{
+		Use:   "export <image_ref> [filename]",
+		Short: "export image",
+		Long: `Exports an image into a tar file that can be later loaded into a docker
 engine with "docker load". The tar file is output to stdout by default.
 Compression is typically not useful since layers are already compressed.
 Example usage: regctl image export registry:5000/yourimg:v1 >yourimg-v1.tar`,
-	Args:              cobra.RangeArgs(1, 2),
-	ValidArgsFunction: completeArgTag,
-	RunE:              runImageExport,
-}
-var imageGetFileCmd = &cobra.Command{
-	Use:               "get-file <image_ref> <filename> [out-file]",
-	Aliases:           []string{"cat"},
-	Short:             "get a file from an image",
-	Long:              `Go through each of the image layers searching for the requested file.`,
-	Args:              cobra.RangeArgs(2, 3),
-	ValidArgsFunction: completeArgList([]completeFunc{completeArgTag, completeArgNone, completeArgNone}),
-	RunE:              runImageGetFile,
-}
-var imageImportCmd = &cobra.Command{
-	Use:   "import <image_ref> <filename>",
-	Short: "import image",
-	Long: `Imports an image from a tar file. This must be either a docker formatted tar
+		Args:              cobra.RangeArgs(1, 2),
+		ValidArgsFunction: rootOpts.completeArgTag,
+		RunE:              imageOpts.runImageExport,
+	}
+	var imageGetFileCmd = &cobra.Command{
+		Use:               "get-file <image_ref> <filename> [out-file]",
+		Aliases:           []string{"cat"},
+		Short:             "get a file from an image",
+		Long:              `Go through each of the image layers searching for the requested file.`,
+		Args:              cobra.RangeArgs(2, 3),
+		ValidArgsFunction: completeArgList([]completeFunc{rootOpts.completeArgTag, completeArgNone, completeArgNone}),
+		RunE:              imageOpts.runImageGetFile,
+	}
+	var imageImportCmd = &cobra.Command{
+		Use:   "import <image_ref> <filename>",
+		Short: "import image",
+		Long: `Imports an image from a tar file. This must be either a docker formatted tar
 from "docker save" or an OCI Layout compatible tar. The output from
 "regctl image export" can be used. Stdin is not permitted for the tar file.`,
-	Args:              cobra.ExactArgs(2),
-	ValidArgsFunction: completeArgList([]completeFunc{completeArgTag, completeArgDefault}),
-	RunE:              runImageImport,
-}
-var imageInspectCmd = &cobra.Command{
-	Use:     "inspect <image_ref>",
-	Aliases: []string{"config"},
-	Short:   "inspect image",
-	Long: `Shows the config json for an image and is equivalent to pulling the image
+		Args:              cobra.ExactArgs(2),
+		ValidArgsFunction: completeArgList([]completeFunc{rootOpts.completeArgTag, completeArgDefault}),
+		RunE:              imageOpts.runImageImport,
+	}
+	var imageInspectCmd = &cobra.Command{
+		Use:     "inspect <image_ref>",
+		Aliases: []string{"config"},
+		Short:   "inspect image",
+		Long: `Shows the config json for an image and is equivalent to pulling the image
 in docker, and inspecting it, but without pulling any of the image layers.`,
-	Args:              cobra.ExactArgs(1),
-	ValidArgsFunction: completeArgTag,
-	RunE:              runImageInspect,
-}
-var imageManifestCmd = &cobra.Command{
-	Use:               "manifest <image_ref>",
-	Short:             "show manifest or manifest list, same as \"manifest get\"",
-	Long:              `Shows the manifest or manifest list of the specified image.`,
-	Args:              cobra.ExactArgs(1),
-	ValidArgsFunction: completeArgTag,
-	RunE:              runManifestGet,
-}
-var imageModCmd = &cobra.Command{
-	Use:   "mod <image_ref>",
-	Short: "modify an image",
-	// TODO: remove EXPERIMENTAL when stable
-	Long: `EXPERIMENTAL: Applies requested modifications to an image
+		Args:              cobra.ExactArgs(1),
+		ValidArgsFunction: rootOpts.completeArgTag,
+		RunE:              imageOpts.runImageInspect,
+	}
+	var imageManifestCmd = &cobra.Command{
+		Use:               "manifest <image_ref>",
+		Short:             "show manifest or manifest list, same as \"manifest get\"",
+		Long:              `Shows the manifest or manifest list of the specified image.`,
+		Args:              cobra.ExactArgs(1),
+		ValidArgsFunction: rootOpts.completeArgTag,
+		RunE:              manifestOpts.runManifestGet,
+	}
+	var imageModCmd = &cobra.Command{
+		Use:   "mod <image_ref>",
+		Short: "modify an image",
+		// TODO: remove EXPERIMENTAL when stable
+		Long: `EXPERIMENTAL: Applies requested modifications to an image
 For time options, the value is a comma separated list of key/value pairs:
   set=${time}: time to set in rfc3339 format, e.g. 2006-01-02T15:04:05Z
   from-label=${label}: label used to extract time in rfc3339 format
@@ -139,45 +171,21 @@ For time options, the value is a comma separated list of key/value pairs:
   base-layers=${count}: number of layers to skip changing (from the base image)
   * set or from-label is required in the time options
 `,
-	Args:              cobra.ExactArgs(1),
-	ValidArgsFunction: completeArgTag,
-	RunE:              runImageMod,
-}
-var imageRateLimitCmd = &cobra.Command{
-	Use:   "ratelimit <image_ref>",
-	Short: "show the current rate limit",
-	Long: `Shows the rate limit using an http head request against the image manifest.
+		Args:              cobra.ExactArgs(1),
+		ValidArgsFunction: rootOpts.completeArgTag,
+		RunE:              imageOpts.runImageMod,
+	}
+	var imageRateLimitCmd = &cobra.Command{
+		Use:   "ratelimit <image_ref>",
+		Short: "show the current rate limit",
+		Long: `Shows the rate limit using an http head request against the image manifest.
 If Set is false, the Remain value was not provided.
 The other values may be 0 if not provided by the registry.`,
-	Args:              cobra.ExactArgs(1),
-	ValidArgsFunction: completeArgTag,
-	RunE:              runImageRateLimit,
-}
-
-var imageOpts struct {
-	checkBaseRef    string
-	checkBaseDigest string
-	checkSkipConfig bool
-	create          string
-	exportCompress  bool
-	exportRef       string
-	fastCheck       bool
-	forceRecursive  bool
-	format          string
-	formatFile      string
-	importName      string
-	includeExternal bool
-	digestTags      bool
-	list            bool
-	modOpts         []mod.Opts
-	platform        string
-	platforms       []string
-	referrers       bool
-	replace         bool
-	requireList     bool
-}
+		Args:              cobra.ExactArgs(1),
+		ValidArgsFunction: rootOpts.completeArgTag,
+		RunE:              imageOpts.runImageRateLimit,
+	}
 
-func init() {
 	imageOpts.modOpts = []mod.Opts{}
 
 	imageCheckBaseCmd.Flags().StringVarP(&imageOpts.checkBaseRef, "base", "", "", "Base image reference (including tag)")
@@ -666,18 +674,18 @@ func init() {
 	imageRateLimitCmd.Flags().StringVarP(&imageOpts.format, "format", "", "{{printPretty .}}", "Format output with go template syntax")
 	_ = imageRateLimitCmd.RegisterFlagCompletionFunc("format", completeArgNone)
 
-	imageCmd.AddCommand(imageCheckBaseCmd)
-	imageCmd.AddCommand(imageCopyCmd)
-	imageCmd.AddCommand(imageDeleteCmd)
-	imageCmd.AddCommand(imageDigestCmd)
-	imageCmd.AddCommand(imageExportCmd)
-	imageCmd.AddCommand(imageGetFileCmd)
-	imageCmd.AddCommand(imageImportCmd)
-	imageCmd.AddCommand(imageInspectCmd)
-	imageCmd.AddCommand(imageManifestCmd)
-	imageCmd.AddCommand(imageModCmd)
-	imageCmd.AddCommand(imageRateLimitCmd)
-	rootCmd.AddCommand(imageCmd)
+	imageTopCmd.AddCommand(imageCheckBaseCmd)
+	imageTopCmd.AddCommand(imageCopyCmd)
+	imageTopCmd.AddCommand(imageDeleteCmd)
+	imageTopCmd.AddCommand(imageDigestCmd)
+	imageTopCmd.AddCommand(imageExportCmd)
+	imageTopCmd.AddCommand(imageGetFileCmd)
+	imageTopCmd.AddCommand(imageImportCmd)
+	imageTopCmd.AddCommand(imageInspectCmd)
+	imageTopCmd.AddCommand(imageManifestCmd)
+	imageTopCmd.AddCommand(imageModCmd)
+	imageTopCmd.AddCommand(imageRateLimitCmd)
+	return imageTopCmd
 }
 
 func imageParseOptTime(s string) (mod.OptTime, map[string]string, error) {
@@ -722,13 +730,13 @@ func imageParseOptTime(s string) (mod.OptTime, map[string]string, error) {
 	return ot, otherFields, nil
 }
 
-func runImageCheckBase(cmd *cobra.Command, args []string) error {
+func (imageOpts *imageCmd) runImageCheckBase(cmd *cobra.Command, args []string) error {
 	ctx := cmd.Context()
 	r, err := ref.New(args[0])
 	if err != nil {
 		return err
 	}
-	rc := newRegClient()
+	rc := imageOpts.rootOpts.newRegClient()
 	defer rc.Close(ctx, r)
 
 	opts := []regclient.ImageOpts{}
@@ -760,7 +768,7 @@ func runImageCheckBase(cmd *cobra.Command, args []string) error {
 	}
 }
 
-func runImageCopy(cmd *cobra.Command, args []string) error {
+func (imageOpts *imageCmd) runImageCopy(cmd *cobra.Command, args []string) error {
 	ctx := cmd.Context()
 	rSrc, err := ref.New(args[0])
 	if err != nil {
@@ -770,7 +778,7 @@ func runImageCopy(cmd *cobra.Command, args []string) error {
 	if err != nil {
 		return err
 	}
-	rc := newRegClient()
+	rc := imageOpts.rootOpts.newRegClient()
 	defer rc.Close(ctx, rSrc)
 	defer rc.Close(ctx, rTgt)
 	if imageOpts.platform != "" {
@@ -985,7 +993,7 @@ func (ip *imageProgress) display(w io.Writer, final bool) {
 	}
 }
 
-func runImageExport(cmd *cobra.Command, args []string) error {
+func (imageOpts *imageCmd) runImageExport(cmd *cobra.Command, args []string) error {
 	ctx := cmd.Context()
 	r, err := ref.New(args[0])
 	if err != nil {
@@ -1000,7 +1008,7 @@ func runImageExport(cmd *cobra.Command, args []string) error {
 	} else {
 		w = cmd.OutOrStdout()
 	}
-	rc := newRegClient()
+	rc := imageOpts.rootOpts.newRegClient()
 	defer rc.Close(ctx, r)
 	opts := []regclient.ImageOpts{}
 	if imageOpts.platform != "" {
@@ -1036,7 +1044,7 @@ func runImageExport(cmd *cobra.Command, args []string) error {
 	return rc.ImageExport(ctx, r, w, opts...)
 }
 
-func runImageGetFile(cmd *cobra.Command, args []string) error {
+func (imageOpts *imageCmd) runImageGetFile(cmd *cobra.Command, args []string) error {
 	ctx := cmd.Context()
 	r, err := ref.New(args[0])
 	if err != nil {
@@ -1044,7 +1052,7 @@ func runImageGetFile(cmd *cobra.Command, args []string) error {
 	}
 	filename := args[1]
 	filename = strings.TrimPrefix(filename, "/")
-	rc := newRegClient()
+	rc := imageOpts.rootOpts.newRegClient()
 	defer rc.Close(ctx, r)
 
 	log.WithFields(logrus.Fields{
@@ -1154,7 +1162,7 @@ func runImageGetFile(cmd *cobra.Command, args []string) error {
 	return types.ErrNotFound
 }
 
-func runImageImport(cmd *cobra.Command, args []string) error {
+func (imageOpts *imageCmd) runImageImport(cmd *cobra.Command, args []string) error {
 	ctx := cmd.Context()
 	r, err := ref.New(args[0])
 	if err != nil {
@@ -1169,7 +1177,7 @@ func runImageImport(cmd *cobra.Command, args []string) error {
 		return err
 	}
 	defer rs.Close()
-	rc := newRegClient()
+	rc := imageOpts.rootOpts.newRegClient()
 	defer rc.Close(ctx, r)
 	log.WithFields(logrus.Fields{
 		"ref":  r.CommonName(),
@@ -1179,13 +1187,13 @@ func runImageImport(cmd *cobra.Command, args []string) error {
 	return rc.ImageImport(ctx, r, rs, opts...)
 }
 
-func runImageInspect(cmd *cobra.Command, args []string) error {
+func (imageOpts *imageCmd) runImageInspect(cmd *cobra.Command, args []string) error {
 	ctx := cmd.Context()
 	r, err := ref.New(args[0])
 	if err != nil {
 		return err
 	}
-	rc := newRegClient()
+	rc := imageOpts.rootOpts.newRegClient()
 	defer rc.Close(ctx, r)
 
 	log.WithFields(logrus.Fields{
@@ -1195,12 +1203,7 @@ func runImageInspect(cmd *cobra.Command, args []string) error {
 		"platform": imageOpts.platform,
 	}).Debug("Image inspect")
 
-	manifestOpts.platform = imageOpts.platform
-	if !flagChanged(cmd, "list") {
-		manifestOpts.list = false
-	}
-
-	m, err := getManifest(ctx, rc, r)
+	m, err := getManifest(ctx, rc, r, imageOpts.platform, imageOpts.list, imageOpts.requireList)
 	if err != nil {
 		return err
 	}
@@ -1228,7 +1231,7 @@ func runImageInspect(cmd *cobra.Command, args []string) error {
 	return template.Writer(cmd.OutOrStdout(), imageOpts.format, blobConfig)
 }
 
-func runImageMod(cmd *cobra.Command, args []string) error {
+func (imageOpts *imageCmd) runImageMod(cmd *cobra.Command, args []string) error {
 	ctx := cmd.Context()
 	rSrc, err := ref.New(args[0])
 	if err != nil {
@@ -1253,7 +1256,7 @@ func runImageMod(cmd *cobra.Command, args []string) error {
 		rTgt.Tag = ""
 	}
 	imageOpts.modOpts = append(imageOpts.modOpts, mod.WithRefTgt(rTgt))
-	rc := newRegClient()
+	rc := imageOpts.rootOpts.newRegClient()
 
 	log.WithFields(logrus.Fields{
 		"ref": rSrc.CommonName(),
@@ -1272,13 +1275,13 @@ func runImageMod(cmd *cobra.Command, args []string) error {
 	return nil
 }
 
-func runImageRateLimit(cmd *cobra.Command, args []string) error {
+func (imageOpts *imageCmd) runImageRateLimit(cmd *cobra.Command, args []string) error {
 	ctx := cmd.Context()
 	r, err := ref.New(args[0])
 	if err != nil {
 		return err
 	}
-	rc := newRegClient()
+	rc := imageOpts.rootOpts.newRegClient()
 
 	log.WithFields(logrus.Fields{
 		"host": r.Registry,
diff --git a/cmd/regctl/index.go b/cmd/regctl/index.go
index 2fb0d26..a55f2d1 100644
--- a/cmd/regctl/index.go
+++ b/cmd/regctl/index.go
@@ -22,42 +22,8 @@ var indexKnownTypes = []string{
 	types.MediaTypeDocker2ManifestList,
 }
 
-var indexCmd = &cobra.Command{
-	Use:   "index <cmd>",
-	Short: "manage manifest lists and OCI index",
-}
-
-var indexAddCmd = &cobra.Command{
-	Use:       "add <image_ref>",
-	Aliases:   []string{"append", "insert"},
-	Short:     "add an index entry",
-	Long:      `Add an entry to a manifest list or OCI Index.`,
-	Args:      cobra.ExactArgs(1),
-	ValidArgs: []string{}, // do not auto complete digests
-	RunE:      runIndexAdd,
-}
-
-var indexCreateCmd = &cobra.Command{
-	Use:       "create <image_ref>",
-	Aliases:   []string{"init", "new"},
-	Short:     "create an index",
-	Long:      `Create a manifest list or OCI Index.`,
-	Args:      cobra.ExactArgs(1),
-	ValidArgs: []string{}, // do not auto complete digests
-	RunE:      runIndexCreate,
-}
-
-var indexDeleteCmd = &cobra.Command{
-	Use:       "delete <image_ref>",
-	Aliases:   []string{"del", "rm", "remove"},
-	Short:     "delete an index entry",
-	Long:      `Delete an entry from a manifest list or OCI Index.`,
-	Args:      cobra.ExactArgs(1),
-	ValidArgs: []string{}, // do not auto complete digests
-	RunE:      runIndexDelete,
-}
-
-var indexOpts struct {
+type indexCmd struct {
+	rootOpts        *rootCmd
 	annotations     []string
 	artifactType    string
 	byDigest        bool
@@ -73,7 +39,45 @@ var indexOpts struct {
 	subject         string
 }
 
-func init() {
+func NewIndexCmd(rootOpts *rootCmd) *cobra.Command {
+	indexOpts := indexCmd{
+		rootOpts: rootOpts,
+	}
+	var indexTopCmd = &cobra.Command{
+		Use:   "index <cmd>",
+		Short: "manage manifest lists and OCI index",
+	}
+
+	var indexAddCmd = &cobra.Command{
+		Use:       "add <image_ref>",
+		Aliases:   []string{"append", "insert"},
+		Short:     "add an index entry",
+		Long:      `Add an entry to a manifest list or OCI Index.`,
+		Args:      cobra.ExactArgs(1),
+		ValidArgs: []string{}, // do not auto complete digests
+		RunE:      indexOpts.runIndexAdd,
+	}
+
+	var indexCreateCmd = &cobra.Command{
+		Use:       "create <image_ref>",
+		Aliases:   []string{"init", "new"},
+		Short:     "create an index",
+		Long:      `Create a manifest list or OCI Index.`,
+		Args:      cobra.ExactArgs(1),
+		ValidArgs: []string{}, // do not auto complete digests
+		RunE:      indexOpts.runIndexCreate,
+	}
+
+	var indexDeleteCmd = &cobra.Command{
+		Use:       "delete <image_ref>",
+		Aliases:   []string{"del", "rm", "remove"},
+		Short:     "delete an index entry",
+		Long:      `Delete an entry from a manifest list or OCI Index.`,
+		Args:      cobra.ExactArgs(1),
+		ValidArgs: []string{}, // do not auto complete digests
+		RunE:      indexOpts.runIndexDelete,
+	}
+
 	indexAddCmd.Flags().StringArrayVar(&indexOpts.descAnnotations, "desc-annotation", []string{}, "Annotation to add to descriptors of new entries")
 	indexAddCmd.Flags().StringVar(&indexOpts.descPlatform, "desc-platform", "", "Platform to set in descriptors of new entries")
 	indexAddCmd.Flags().StringArrayVar(&indexOpts.digests, "digest", []string{}, "Digest to add")
@@ -102,13 +106,13 @@ func init() {
 	indexDeleteCmd.Flags().StringArrayVar(&indexOpts.digests, "digest", []string{}, "Digest to delete")
 	indexDeleteCmd.Flags().StringArrayVar(&indexOpts.platforms, "platform", []string{}, "Platform to delete")
 
-	indexCmd.AddCommand(indexAddCmd)
-	indexCmd.AddCommand(indexCreateCmd)
-	indexCmd.AddCommand(indexDeleteCmd)
-	rootCmd.AddCommand(indexCmd)
+	indexTopCmd.AddCommand(indexAddCmd)
+	indexTopCmd.AddCommand(indexCreateCmd)
+	indexTopCmd.AddCommand(indexDeleteCmd)
+	return indexTopCmd
 }
 
-func runIndexAdd(cmd *cobra.Command, args []string) error {
+func (indexOpts *indexCmd) runIndexAdd(cmd *cobra.Command, args []string) error {
 	ctx := cmd.Context()
 
 	// parse ref
@@ -118,7 +122,7 @@ func runIndexAdd(cmd *cobra.Command, args []string) error {
 	}
 
 	// setup regclient
-	rc := newRegClient()
+	rc := indexOpts.rootOpts.newRegClient()
 	defer rc.Close(ctx, r)
 
 	// pull existing index
@@ -136,7 +140,7 @@ func runIndexAdd(cmd *cobra.Command, args []string) error {
 	}
 
 	// generate a list of descriptors from CLI args
-	descList, err := indexBuildDescList(ctx, rc, r)
+	descList, err := indexOpts.indexBuildDescList(ctx, rc, r)
 	if err != nil {
 		return err
 	}
@@ -170,7 +174,7 @@ func runIndexAdd(cmd *cobra.Command, args []string) error {
 	return template.Writer(cmd.OutOrStdout(), indexOpts.format, result)
 }
 
-func runIndexCreate(cmd *cobra.Command, args []string) error {
+func (indexOpts *indexCmd) runIndexCreate(cmd *cobra.Command, args []string) error {
 	ctx := cmd.Context()
 
 	// validate media type
@@ -185,7 +189,7 @@ func runIndexCreate(cmd *cobra.Command, args []string) error {
 	}
 
 	// setup regclient
-	rc := newRegClient()
+	rc := indexOpts.rootOpts.newRegClient()
 	defer rc.Close(ctx, r)
 
 	// parse annotations
@@ -200,7 +204,7 @@ func runIndexCreate(cmd *cobra.Command, args []string) error {
 	}
 
 	// generate a list of descriptors from CLI args
-	descList, err := indexBuildDescList(ctx, rc, r)
+	descList, err := indexOpts.indexBuildDescList(ctx, rc, r)
 	if err != nil {
 		return err
 	}
@@ -278,7 +282,7 @@ func runIndexCreate(cmd *cobra.Command, args []string) error {
 	return template.Writer(cmd.OutOrStdout(), indexOpts.format, result)
 }
 
-func runIndexDelete(cmd *cobra.Command, args []string) error {
+func (indexOpts *indexCmd) runIndexDelete(cmd *cobra.Command, args []string) error {
 	ctx := cmd.Context()
 
 	// parse ref
@@ -288,7 +292,7 @@ func runIndexDelete(cmd *cobra.Command, args []string) error {
 	}
 
 	// setup regclient
-	rc := newRegClient()
+	rc := indexOpts.rootOpts.newRegClient()
 	defer rc.Close(ctx, r)
 
 	// pull existing index
@@ -364,7 +368,7 @@ func runIndexDelete(cmd *cobra.Command, args []string) error {
 	return template.Writer(cmd.OutOrStdout(), indexOpts.format, result)
 }
 
-func indexBuildDescList(ctx context.Context, rc *regclient.RegClient, r ref.Ref) ([]types.Descriptor, error) {
+func (indexOpts *indexCmd) indexBuildDescList(ctx context.Context, rc *regclient.RegClient, r ref.Ref) ([]types.Descriptor, error) {
 	imgCopyOpts := []regclient.ImageOpts{
 		regclient.ImageWithChild(),
 	}
diff --git a/cmd/regctl/main.go b/cmd/regctl/main.go
index 3646bd5..0be03bb 100644
--- a/cmd/regctl/main.go
+++ b/cmd/regctl/main.go
@@ -24,7 +24,8 @@ func main() {
 	}()
 	godbg.SignalTrace()
 
-	if err := rootCmd.ExecuteContext(ctx); err != nil {
+	rootTopCmd := NewRootCmd()
+	if err := rootTopCmd.ExecuteContext(ctx); err != nil {
 		fmt.Fprintf(os.Stderr, "%v\n", err)
 		// provide tips for common error messages
 		switch {
diff --git a/cmd/regctl/manifest.go b/cmd/regctl/manifest.go
index 35f3ffc..3cc41ec 100644
--- a/cmd/regctl/manifest.go
+++ b/cmd/regctl/manifest.go
@@ -19,64 +19,8 @@ import (
 	"github.com/spf13/cobra"
 )
 
-var manifestCmd = &cobra.Command{
-	Use:   "manifest <cmd>",
-	Short: "manage manifests",
-}
-
-var manifestDeleteCmd = &cobra.Command{
-	Use:     "delete <image_ref>",
-	Aliases: []string{"del", "rm", "remove"},
-	Short:   "delete a manifest",
-	Long: `Delete a manifest. This will delete the manifest, and all tags pointing to that
-manifest. You must specify a digest, not a tag on this command (e.g. 
-image_name@sha256:1234abc...). It is up to the registry whether the delete
-API is supported. Additionally, registries may garbage collect the filesystem
-layers (blobs) separately or not at all. See also the "tag delete" command.`,
-	Args:      cobra.ExactArgs(1),
-	ValidArgs: []string{}, // do not auto complete digests
-	RunE:      runManifestDelete,
-}
-
-var manifestDiffCmd = &cobra.Command{
-	Use:               "diff <image_ref> <image_ref>",
-	Short:             "compare manifests",
-	Args:              cobra.ExactArgs(2),
-	ValidArgsFunction: completeArgTag,
-	RunE:              runManifestDiff,
-}
-
-var manifestGetCmd = &cobra.Command{
-	Use:               "get <image_ref>",
-	Aliases:           []string{"pull"},
-	Short:             "retrieve manifest or manifest list",
-	Long:              `Shows the manifest or manifest list of the specified image.`,
-	Args:              cobra.ExactArgs(1),
-	ValidArgsFunction: completeArgTag,
-	RunE:              runManifestGet,
-}
-
-var manifestHeadCmd = &cobra.Command{
-	Use:               "head <image_ref>",
-	Aliases:           []string{"digest"},
-	Short:             "http head request for manifest",
-	Long:              `Shows the digest or headers from an http manifest head request.`,
-	Args:              cobra.ExactArgs(1),
-	ValidArgsFunction: completeArgTag,
-	RunE:              runManifestHead,
-}
-
-var manifestPutCmd = &cobra.Command{
-	Use:               "put <image_ref>",
-	Aliases:           []string{"push"},
-	Short:             "push manifest or manifest list",
-	Long:              `Pushes a manifest or manifest list to a repository.`,
-	Args:              cobra.ExactArgs(1),
-	ValidArgsFunction: completeArgTag,
-	RunE:              runManifestPut,
-}
-
-var manifestOpts struct {
+type manifestCmd struct {
+	rootOpts      *rootCmd
 	byDigest      bool
 	contentType   string
 	diffCtx       int
@@ -92,7 +36,67 @@ var manifestOpts struct {
 	requireList   bool
 }
 
-func init() {
+func NewManifestCmd(rootOpts *rootCmd) *cobra.Command {
+	manifestOpts := manifestCmd{
+		rootOpts: rootOpts,
+	}
+	var manifestTopCmd = &cobra.Command{
+		Use:   "manifest <cmd>",
+		Short: "manage manifests",
+	}
+
+	var manifestDeleteCmd = &cobra.Command{
+		Use:     "delete <image_ref>",
+		Aliases: []string{"del", "rm", "remove"},
+		Short:   "delete a manifest",
+		Long: `Delete a manifest. This will delete the manifest, and all tags pointing to that
+manifest. You must specify a digest, not a tag on this command (e.g. 
+image_name@sha256:1234abc...). It is up to the registry whether the delete
+API is supported. Additionally, registries may garbage collect the filesystem
+layers (blobs) separately or not at all. See also the "tag delete" command.`,
+		Args:      cobra.ExactArgs(1),
+		ValidArgs: []string{}, // do not auto complete digests
+		RunE:      manifestOpts.runManifestDelete,
+	}
+
+	var manifestDiffCmd = &cobra.Command{
+		Use:               "diff <image_ref> <image_ref>",
+		Short:             "compare manifests",
+		Args:              cobra.ExactArgs(2),
+		ValidArgsFunction: rootOpts.completeArgTag,
+		RunE:              manifestOpts.runManifestDiff,
+	}
+
+	var manifestGetCmd = &cobra.Command{
+		Use:               "get <image_ref>",
+		Aliases:           []string{"pull"},
+		Short:             "retrieve manifest or manifest list",
+		Long:              `Shows the manifest or manifest list of the specified image.`,
+		Args:              cobra.ExactArgs(1),
+		ValidArgsFunction: rootOpts.completeArgTag,
+		RunE:              manifestOpts.runManifestGet,
+	}
+
+	var manifestHeadCmd = &cobra.Command{
+		Use:               "head <image_ref>",
+		Aliases:           []string{"digest"},
+		Short:             "http head request for manifest",
+		Long:              `Shows the digest or headers from an http manifest head request.`,
+		Args:              cobra.ExactArgs(1),
+		ValidArgsFunction: rootOpts.completeArgTag,
+		RunE:              manifestOpts.runManifestHead,
+	}
+
+	var manifestPutCmd = &cobra.Command{
+		Use:               "put <image_ref>",
+		Aliases:           []string{"push"},
+		Short:             "push manifest or manifest list",
+		Long:              `Pushes a manifest or manifest list to a repository.`,
+		Args:              cobra.ExactArgs(1),
+		ValidArgsFunction: rootOpts.completeArgTag,
+		RunE:              manifestOpts.runManifestPut,
+	}
+
 	manifestDeleteCmd.Flags().BoolVarP(&manifestOpts.forceTagDeref, "force-tag-dereference", "", false, "Dereference the a tag to a digest, this is unsafe")
 	manifestDeleteCmd.Flags().BoolVarP(&manifestOpts.referrers, "referrers", "", false, "Check for referrers, recommended when deleting artifacts")
 
@@ -120,97 +124,21 @@ func init() {
 	_ = manifestPutCmd.RegisterFlagCompletionFunc("content-type", completeArgMediaTypeManifest)
 	manifestPutCmd.Flags().StringVarP(&manifestOpts.formatPut, "format", "", "", "Format output with go template syntax")
 
-	manifestCmd.AddCommand(manifestDeleteCmd)
-	manifestCmd.AddCommand(manifestDiffCmd)
-	manifestCmd.AddCommand(manifestHeadCmd)
-	manifestCmd.AddCommand(manifestGetCmd)
-	manifestCmd.AddCommand(manifestPutCmd)
-	rootCmd.AddCommand(manifestCmd)
+	manifestTopCmd.AddCommand(manifestDeleteCmd)
+	manifestTopCmd.AddCommand(manifestDiffCmd)
+	manifestTopCmd.AddCommand(manifestHeadCmd)
+	manifestTopCmd.AddCommand(manifestGetCmd)
+	manifestTopCmd.AddCommand(manifestPutCmd)
+	return manifestTopCmd
 }
 
-func getManifest(ctx context.Context, rc *regclient.RegClient, r ref.Ref) (manifest.Manifest, error) {
-	m, err := rc.ManifestGet(context.Background(), r)
-	if err != nil {
-		return m, err
-	}
-
-	// add warning if not list and list required or platform requested
-	if !m.IsList() && manifestOpts.requireList {
-		log.Warn("Manifest list unavailable")
-		return m, ErrNotFound
-	}
-	if !m.IsList() && manifestOpts.platform != "" {
-		log.Info("Manifest list unavailable, ignoring platform flag")
-	}
-
-	// retrieve the specified platform from the manifest list
-	if m.IsList() && !manifestOpts.list && !manifestOpts.requireList {
-		desc, err := getPlatformDesc(ctx, rc, m)
-		if err != nil {
-			return m, fmt.Errorf("failed to lookup platform specific digest: %w", err)
-		}
-		m, err = rc.ManifestGet(ctx, r, regclient.WithManifestDesc(*desc))
-		if err != nil {
-			return m, fmt.Errorf("failed to pull platform specific digest: %w", err)
-		}
-	}
-	return m, nil
-}
-
-func getPlatformDesc(ctx context.Context, rc *regclient.RegClient, m manifest.Manifest) (*types.Descriptor, error) {
-	var desc *types.Descriptor
-	var err error
-	if !m.IsList() {
-		return desc, fmt.Errorf("%w: manifest is not a list", ErrInvalidInput)
-	}
-	if !m.IsSet() {
-		m, err = rc.ManifestGet(ctx, m.GetRef())
-		if err != nil {
-			return desc, fmt.Errorf("unable to retrieve manifest list: %w", err)
-		}
-	}
-
-	var plat platform.Platform
-	if manifestOpts.platform != "" && manifestOpts.platform != "local" {
-		plat, err = platform.Parse(manifestOpts.platform)
-		if err != nil {
-			log.WithFields(logrus.Fields{
-				"platform": manifestOpts.platform,
-				"err":      err,
-			}).Warn("Could not parse platform")
-		}
-	}
-	if plat.OS == "" {
-		plat = platform.Local()
-	}
-	desc, err = manifest.GetPlatformDesc(m, &plat)
-	if err != nil {
-		pl, _ := manifest.GetPlatformList(m)
-		var ps []string
-		for _, p := range pl {
-			ps = append(ps, p.String())
-		}
-		log.WithFields(logrus.Fields{
-			"platform":  plat,
-			"err":       err,
-			"platforms": strings.Join(ps, ", "),
-		}).Warn("Platform could not be found in manifest list")
-		return desc, ErrNotFound
-	}
-	log.WithFields(logrus.Fields{
-		"platform": plat,
-		"digest":   desc.Digest.String(),
-	}).Debug("Found platform specific digest in manifest list")
-	return desc, nil
-}
-
-func runManifestDelete(cmd *cobra.Command, args []string) error {
+func (manifestOpts *manifestCmd) runManifestDelete(cmd *cobra.Command, args []string) error {
 	ctx := cmd.Context()
 	r, err := ref.New(args[0])
 	if err != nil {
 		return err
 	}
-	rc := newRegClient()
+	rc := manifestOpts.rootOpts.newRegClient()
 	defer rc.Close(ctx, r)
 
 	if r.Digest == "" && manifestOpts.forceTagDeref {
@@ -242,7 +170,7 @@ func runManifestDelete(cmd *cobra.Command, args []string) error {
 	return nil
 }
 
-func runManifestDiff(cmd *cobra.Command, args []string) error {
+func (manifestOpts *manifestCmd) runManifestDiff(cmd *cobra.Command, args []string) error {
 	diffOpts := []diff.Opt{}
 	if manifestOpts.diffCtx > 0 {
 		diffOpts = append(diffOpts, diff.WithContext(manifestOpts.diffCtx, manifestOpts.diffCtx))
@@ -260,7 +188,7 @@ func runManifestDiff(cmd *cobra.Command, args []string) error {
 		return err
 	}
 
-	rc := newRegClient()
+	rc := manifestOpts.rootOpts.newRegClient()
 
 	log.WithFields(logrus.Fields{
 		"ref1": r1.CommonName(),
@@ -293,7 +221,7 @@ func runManifestDiff(cmd *cobra.Command, args []string) error {
 	// return template.Writer(cmd.OutOrStdout(), manifestOpts.format, mDiff)
 }
 
-func runManifestHead(cmd *cobra.Command, args []string) error {
+func (manifestOpts *manifestCmd) runManifestHead(cmd *cobra.Command, args []string) error {
 	ctx := cmd.Context()
 	if manifestOpts.platform != "" && !flagChanged(cmd, "list") {
 		manifestOpts.list = false
@@ -305,7 +233,7 @@ func runManifestHead(cmd *cobra.Command, args []string) error {
 	if err != nil {
 		return err
 	}
-	rc := newRegClient()
+	rc := manifestOpts.rootOpts.newRegClient()
 
 	log.WithFields(logrus.Fields{
 		"host": r.Registry,
@@ -335,7 +263,7 @@ func runManifestHead(cmd *cobra.Command, args []string) error {
 
 	// retrieve the specified platform from the manifest list
 	for m.IsList() && !manifestOpts.list && !manifestOpts.requireList {
-		desc, err := getPlatformDesc(ctx, rc, m)
+		desc, err := getPlatformDesc(ctx, rc, m, manifestOpts.platform)
 		if err != nil {
 			return fmt.Errorf("failed retrieving platform specific digest: %w", err)
 		}
@@ -355,7 +283,7 @@ func runManifestHead(cmd *cobra.Command, args []string) error {
 	return template.Writer(cmd.OutOrStdout(), manifestOpts.formatHead, m)
 }
 
-func runManifestGet(cmd *cobra.Command, args []string) error {
+func (manifestOpts *manifestCmd) runManifestGet(cmd *cobra.Command, args []string) error {
 	ctx := cmd.Context()
 	if manifestOpts.platform != "" && !flagChanged(cmd, "list") {
 		manifestOpts.list = false
@@ -367,10 +295,10 @@ func runManifestGet(cmd *cobra.Command, args []string) error {
 	if err != nil {
 		return err
 	}
-	rc := newRegClient()
+	rc := manifestOpts.rootOpts.newRegClient()
 	defer rc.Close(ctx, r)
 
-	m, err := getManifest(ctx, rc, r)
+	m, err := getManifest(ctx, rc, r, manifestOpts.platform, manifestOpts.list, manifestOpts.requireList)
 	if err != nil {
 		return err
 	}
@@ -386,13 +314,13 @@ func runManifestGet(cmd *cobra.Command, args []string) error {
 	return template.Writer(cmd.OutOrStdout(), manifestOpts.formatGet, m)
 }
 
-func runManifestPut(cmd *cobra.Command, args []string) error {
+func (manifestOpts *manifestCmd) runManifestPut(cmd *cobra.Command, args []string) error {
 	ctx := cmd.Context()
 	r, err := ref.New(args[0])
 	if err != nil {
 		return err
 	}
-	rc := newRegClient()
+	rc := manifestOpts.rootOpts.newRegClient()
 	defer rc.Close(ctx, r)
 
 	raw, err := io.ReadAll(os.Stdin)
@@ -432,3 +360,79 @@ func runManifestPut(cmd *cobra.Command, args []string) error {
 	}
 	return template.Writer(cmd.OutOrStdout(), manifestOpts.formatPut, result)
 }
+
+func getManifest(ctx context.Context, rc *regclient.RegClient, r ref.Ref, pStr string, list, reqList bool) (manifest.Manifest, error) {
+	m, err := rc.ManifestGet(context.Background(), r)
+	if err != nil {
+		return m, err
+	}
+
+	// add warning if not list and list required or platform requested
+	if !m.IsList() && reqList {
+		log.Warn("Manifest list unavailable")
+		return m, ErrNotFound
+	}
+	if !m.IsList() && pStr != "" {
+		log.Info("Manifest list unavailable, ignoring platform flag")
+	}
+
+	// retrieve the specified platform from the manifest list
+	if m.IsList() && !list && !reqList {
+		desc, err := getPlatformDesc(ctx, rc, m, pStr)
+		if err != nil {
+			return m, fmt.Errorf("failed to lookup platform specific digest: %w", err)
+		}
+		m, err = rc.ManifestGet(ctx, r, regclient.WithManifestDesc(*desc))
+		if err != nil {
+			return m, fmt.Errorf("failed to pull platform specific digest: %w", err)
+		}
+	}
+	return m, nil
+}
+
+func getPlatformDesc(ctx context.Context, rc *regclient.RegClient, m manifest.Manifest, pStr string) (*types.Descriptor, error) {
+	var desc *types.Descriptor
+	var err error
+	if !m.IsList() {
+		return desc, fmt.Errorf("%w: manifest is not a list", ErrInvalidInput)
+	}
+	if !m.IsSet() {
+		m, err = rc.ManifestGet(ctx, m.GetRef())
+		if err != nil {
+			return desc, fmt.Errorf("unable to retrieve manifest list: %w", err)
+		}
+	}
+
+	var plat platform.Platform
+	if pStr != "" && pStr != "local" {
+		plat, err = platform.Parse(pStr)
+		if err != nil {
+			log.WithFields(logrus.Fields{
+				"platform": pStr,
+				"err":      err,
+			}).Warn("Could not parse platform")
+		}
+	}
+	if plat.OS == "" {
+		plat = platform.Local()
+	}
+	desc, err = manifest.GetPlatformDesc(m, &plat)
+	if err != nil {
+		pl, _ := manifest.GetPlatformList(m)
+		var ps []string
+		for _, p := range pl {
+			ps = append(ps, p.String())
+		}
+		log.WithFields(logrus.Fields{
+			"platform":  plat,
+			"err":       err,
+			"platforms": strings.Join(ps, ", "),
+		}).Warn("Platform could not be found in manifest list")
+		return desc, ErrNotFound
+	}
+	log.WithFields(logrus.Fields{
+		"platform": plat,
+		"digest":   desc.Digest.String(),
+	}).Debug("Found platform specific digest in manifest list")
+	return desc, nil
+}
diff --git a/cmd/regctl/registry.go b/cmd/regctl/registry.go
index 0f040cf..4bc2045 100644
--- a/cmd/regctl/registry.go
+++ b/cmd/regctl/registry.go
@@ -16,46 +16,8 @@ import (
 	"golang.org/x/term"
 )
 
-var registryCmd = &cobra.Command{
-	Use:   "registry <cmd>",
-	Short: "manage registries",
-}
-var registryConfigCmd = &cobra.Command{
-	Use:   "config [registry]",
-	Short: "show registry config",
-	Long: `Displays the configuration used for a registry. Secrets are not included
-in the output (e.g. passwords, tokens, and TLS keys).`,
-	Args:              cobra.RangeArgs(0, 1),
-	ValidArgsFunction: registryArgListReg,
-	RunE:              runRegistryConfig,
-}
-var registryLoginCmd = &cobra.Command{
-	Use:   "login <registry>",
-	Short: "login to a registry",
-	Long: `Provide login credentials for a registry. This may not be necessary if you
-have already logged in with docker.`,
-	Args:              cobra.RangeArgs(0, 1),
-	ValidArgsFunction: registryArgListReg,
-	RunE:              runRegistryLogin,
-}
-var registryLogoutCmd = &cobra.Command{
-	Use:               "logout <registry>",
-	Short:             "logout of a registry",
-	Long:              `Remove registry credentials from the configuration.`,
-	Args:              cobra.RangeArgs(0, 1),
-	ValidArgsFunction: registryArgListReg,
-	RunE:              runRegistryLogout,
-}
-var registrySetCmd = &cobra.Command{
-	Use:   "set <registry>",
-	Short: "set options on a registry",
-	Long: `Set or modify the configuration of a registry. To pass a certificate, include
-the contents of the file, e.g. --cacert "$(cat reg-ca.crt)"`,
-	Args:              cobra.RangeArgs(0, 1),
-	ValidArgsFunction: registryArgListReg,
-	RunE:              runRegistrySet,
-}
-var registryOpts struct {
+type registryCmd struct {
+	rootOpts             *rootCmd
 	user, pass           string // login opts
 	passStdin            bool
 	credHelper           string
@@ -74,7 +36,50 @@ var registryOpts struct {
 	dns                  []string // TODO: remove
 }
 
-func init() {
+func NewRegistryCmd(rootOpts *rootCmd) *cobra.Command {
+	registryOpts := registryCmd{
+		rootOpts: rootOpts,
+	}
+	var registryTopCmd = &cobra.Command{
+		Use:   "registry <cmd>",
+		Short: "manage registries",
+	}
+	var registryConfigCmd = &cobra.Command{
+		Use:   "config [registry]",
+		Short: "show registry config",
+		Long: `Displays the configuration used for a registry. Secrets are not included
+in the output (e.g. passwords, tokens, and TLS keys).`,
+		Args:              cobra.RangeArgs(0, 1),
+		ValidArgsFunction: registryArgListReg,
+		RunE:              registryOpts.runRegistryConfig,
+	}
+	var registryLoginCmd = &cobra.Command{
+		Use:   "login <registry>",
+		Short: "login to a registry",
+		Long: `Provide login credentials for a registry. This may not be necessary if you
+have already logged in with docker.`,
+		Args:              cobra.RangeArgs(0, 1),
+		ValidArgsFunction: registryArgListReg,
+		RunE:              registryOpts.runRegistryLogin,
+	}
+	var registryLogoutCmd = &cobra.Command{
+		Use:               "logout <registry>",
+		Short:             "logout of a registry",
+		Long:              `Remove registry credentials from the configuration.`,
+		Args:              cobra.RangeArgs(0, 1),
+		ValidArgsFunction: registryArgListReg,
+		RunE:              registryOpts.runRegistryLogout,
+	}
+	var registrySetCmd = &cobra.Command{
+		Use:   "set <registry>",
+		Short: "set options on a registry",
+		Long: `Set or modify the configuration of a registry. To pass a certificate, include
+the contents of the file, e.g. --cacert "$(cat reg-ca.crt)"`,
+		Args:              cobra.RangeArgs(0, 1),
+		ValidArgsFunction: registryArgListReg,
+		RunE:              registryOpts.runRegistrySet,
+	}
+
 	registryLoginCmd.Flags().StringVarP(&registryOpts.user, "user", "u", "", "Username")
 	registryLoginCmd.Flags().StringVarP(&registryOpts.pass, "pass", "p", "", "Password")
 	registryLoginCmd.Flags().BoolVarP(&registryOpts.passStdin, "pass-stdin", "", false, "Read password from stdin")
@@ -117,11 +122,11 @@ func init() {
 	_ = registrySetCmd.Flags().MarkHidden("scheme")
 	_ = registrySetCmd.Flags().MarkHidden("dns")
 
-	registryCmd.AddCommand(registryConfigCmd)
-	registryCmd.AddCommand(registryLoginCmd)
-	registryCmd.AddCommand(registryLogoutCmd)
-	registryCmd.AddCommand(registrySetCmd)
-	rootCmd.AddCommand(registryCmd)
+	registryTopCmd.AddCommand(registryConfigCmd)
+	registryTopCmd.AddCommand(registryLoginCmd)
+	registryTopCmd.AddCommand(registryLogoutCmd)
+	registryTopCmd.AddCommand(registrySetCmd)
+	return registryTopCmd
 }
 
 func registryArgListReg(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
@@ -138,7 +143,7 @@ func registryArgListReg(cmd *cobra.Command, args []string, toComplete string) ([
 	return result, cobra.ShellCompDirectiveNoFileComp
 }
 
-func runRegistryConfig(cmd *cobra.Command, args []string) error {
+func (registryOpts *registryCmd) runRegistryConfig(cmd *cobra.Command, args []string) error {
 	c, err := ConfigLoadDefault()
 	if err != nil {
 		return err
@@ -173,7 +178,7 @@ func runRegistryConfig(cmd *cobra.Command, args []string) error {
 	return nil
 }
 
-func runRegistryLogin(cmd *cobra.Command, args []string) error {
+func (registryOpts *registryCmd) runRegistryLogin(cmd *cobra.Command, args []string) error {
 	c, err := ConfigLoadDefault()
 	if err != nil {
 		return err
@@ -257,7 +262,7 @@ func runRegistryLogin(cmd *cobra.Command, args []string) error {
 	return nil
 }
 
-func runRegistryLogout(cmd *cobra.Command, args []string) error {
+func (registryOpts *registryCmd) runRegistryLogout(cmd *cobra.Command, args []string) error {
 	c, err := ConfigLoadDefault()
 	if err != nil {
 		return err
@@ -289,7 +294,7 @@ func runRegistryLogout(cmd *cobra.Command, args []string) error {
 	return nil
 }
 
-func runRegistrySet(cmd *cobra.Command, args []string) error {
+func (registryOpts *registryCmd) runRegistrySet(cmd *cobra.Command, args []string) error {
 	c, err := ConfigLoadDefault()
 	if err != nil {
 		return err
diff --git a/cmd/regctl/repo.go b/cmd/regctl/repo.go
index 3f02fa1..30fcce4 100644
--- a/cmd/regctl/repo.go
+++ b/cmd/regctl/repo.go
@@ -9,28 +9,32 @@ import (
 	"github.com/spf13/cobra"
 )
 
-var repoCmd = &cobra.Command{
-	Use:   "repo <cmd>",
-	Short: "manage repositories",
-}
-var repoLsCmd = &cobra.Command{
-	Use:     "ls <registry>",
-	Aliases: []string{"list"},
-	Short:   "list repositories in a registry",
-	Long: `List repositories in a registry.
-Note: Docker Hub does not support this API request.`,
-	Args:              cobra.ExactArgs(1),
-	ValidArgsFunction: registryArgListReg,
-	RunE:              runRepoLs,
+type repoCmd struct {
+	rootOpts *rootCmd
+	last     string
+	limit    int
+	format   string
 }
 
-var repoOpts struct {
-	last   string
-	limit  int
-	format string
-}
+func NewRepoCmd(rootOpts *rootCmd) *cobra.Command {
+	repoOpts := repoCmd{
+		rootOpts: rootOpts,
+	}
+	var repoTopCmd = &cobra.Command{
+		Use:   "repo <cmd>",
+		Short: "manage repositories",
+	}
+	var repoLsCmd = &cobra.Command{
+		Use:     "ls <registry>",
+		Aliases: []string{"list"},
+		Short:   "list repositories in a registry",
+		Long: `List repositories in a registry.
+Note: Docker Hub does not support this API request.`,
+		Args:              cobra.ExactArgs(1),
+		ValidArgsFunction: registryArgListReg,
+		RunE:              repoOpts.runRepoLs,
+	}
 
-func init() {
 	repoLsCmd.Flags().StringVarP(&repoOpts.last, "last", "", "", "Specify the last repo from a previous request for pagination")
 	repoLsCmd.Flags().IntVarP(&repoOpts.limit, "limit", "", 0, "Specify the number of repos to retrieve")
 	repoLsCmd.Flags().StringVarP(&repoOpts.format, "format", "", "{{printPretty .}}", "Format output with go template syntax")
@@ -38,11 +42,11 @@ func init() {
 	_ = repoLsCmd.RegisterFlagCompletionFunc("limit", completeArgNone)
 	_ = repoLsCmd.RegisterFlagCompletionFunc("format", completeArgNone)
 
-	repoCmd.AddCommand(repoLsCmd)
-	rootCmd.AddCommand(repoCmd)
+	repoTopCmd.AddCommand(repoLsCmd)
+	return repoTopCmd
 }
 
-func runRepoLs(cmd *cobra.Command, args []string) error {
+func (repoOpts *repoCmd) runRepoLs(cmd *cobra.Command, args []string) error {
 	ctx := cmd.Context()
 	host := args[0]
 	// TODO: use regex to validate hostname + port
@@ -53,7 +57,7 @@ func runRepoLs(cmd *cobra.Command, args []string) error {
 		}).Error("Hostname invalid")
 		return ErrInvalidInput
 	}
-	rc := newRegClient()
+	rc := repoOpts.rootOpts.newRegClient()
 	log.WithFields(logrus.Fields{
 		"host":  host,
 		"last":  repoOpts.last,
diff --git a/cmd/regctl/root.go b/cmd/regctl/root.go
index bc81770..badfca7 100644
--- a/cmd/regctl/root.go
+++ b/cmd/regctl/root.go
@@ -25,30 +25,32 @@ var (
 	log *logrus.Logger
 )
 
-var rootCmd = &cobra.Command{
-	Use:           "regctl <cmd>",
-	Short:         "Utility for accessing docker registries",
-	Long:          usageDesc,
-	SilenceUsage:  true,
-	SilenceErrors: true,
-}
-
-var versionCmd = &cobra.Command{
-	Use:   "version",
-	Short: "Show the version",
-	Long:  `Show the version`,
-	Args:  cobra.ExactArgs(0),
-	RunE:  runVersion,
-}
-
-var rootOpts struct {
+type rootCmd struct {
+	name      string
 	verbosity string
 	logopts   []string
 	format    string // for Go template formatting of various commands
 	userAgent string
 }
 
-func init() {
+func NewRootCmd() *cobra.Command {
+	rootOpts := rootCmd{}
+	var rootTopCmd = &cobra.Command{
+		Use:           "regctl <cmd>",
+		Short:         "Utility for accessing docker registries",
+		Long:          usageDesc,
+		SilenceUsage:  true,
+		SilenceErrors: true,
+	}
+	rootOpts.name = rootTopCmd.Name()
+	var versionCmd = &cobra.Command{
+		Use:   "version",
+		Short: "Show the version",
+		Long:  `Show the version`,
+		Args:  cobra.ExactArgs(0),
+		RunE:  rootOpts.runVersion,
+	}
+
 	log = &logrus.Logger{
 		Out:       os.Stderr,
 		Formatter: new(logrus.TextFormatter),
@@ -56,23 +58,37 @@ func init() {
 		Level:     logrus.WarnLevel,
 	}
 
-	rootCmd.PersistentFlags().StringVarP(&rootOpts.verbosity, "verbosity", "v", logrus.WarnLevel.String(), "Log level (debug, info, warn, error, fatal, panic)")
-	rootCmd.PersistentFlags().StringArrayVar(&rootOpts.logopts, "logopt", []string{}, "Log options")
-	rootCmd.PersistentFlags().StringVarP(&rootOpts.userAgent, "user-agent", "", "", "Override user agent")
+	rootTopCmd.PersistentFlags().StringVarP(&rootOpts.verbosity, "verbosity", "v", logrus.WarnLevel.String(), "Log level (debug, info, warn, error, fatal, panic)")
+	rootTopCmd.PersistentFlags().StringArrayVar(&rootOpts.logopts, "logopt", []string{}, "Log options")
+	rootTopCmd.PersistentFlags().StringVarP(&rootOpts.userAgent, "user-agent", "", "", "Override user agent")
 
-	_ = rootCmd.RegisterFlagCompletionFunc("verbosity", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
+	_ = rootTopCmd.RegisterFlagCompletionFunc("verbosity", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
 		return []string{"debug", "info", "warn", "error", "fatal", "panic"}, cobra.ShellCompDirectiveNoFileComp
 	})
-	_ = rootCmd.RegisterFlagCompletionFunc("logopt", completeArgNone)
+	_ = rootTopCmd.RegisterFlagCompletionFunc("logopt", completeArgNone)
 
 	versionCmd.Flags().StringVarP(&rootOpts.format, "format", "", "{{printPretty .}}", "Format output with go template syntax")
 	_ = versionCmd.RegisterFlagCompletionFunc("format", completeArgNone)
 
-	rootCmd.PersistentPreRunE = rootPreRun
-	rootCmd.AddCommand(versionCmd)
+	rootTopCmd.PersistentPreRunE = rootOpts.rootPreRun
+	rootTopCmd.AddCommand(versionCmd)
+	rootTopCmd.AddCommand(
+		NewArtifactCmd(&rootOpts),
+		NewBlobCmd(&rootOpts),
+		NewCompletionCmd(&rootOpts),
+		NewConfigCmd(&rootOpts),
+		NewDigestCmd(&rootOpts),
+		NewImageCmd(&rootOpts),
+		NewIndexCmd(&rootOpts),
+		NewManifestCmd(&rootOpts),
+		NewRegistryCmd(&rootOpts),
+		NewRepoCmd(&rootOpts),
+		NewTagCmd(&rootOpts),
+	)
+	return rootTopCmd
 }
 
-func rootPreRun(cmd *cobra.Command, args []string) error {
+func (rootOpts *rootCmd) rootPreRun(cmd *cobra.Command, args []string) error {
 	lvl, err := logrus.ParseLevel(rootOpts.verbosity)
 	if err != nil {
 		return err
@@ -86,12 +102,12 @@ func rootPreRun(cmd *cobra.Command, args []string) error {
 	return nil
 }
 
-func runVersion(cmd *cobra.Command, args []string) error {
+func (rootOpts *rootCmd) runVersion(cmd *cobra.Command, args []string) error {
 	info := version.GetInfo()
 	return template.Writer(cmd.OutOrStdout(), rootOpts.format, info)
 }
 
-func newRegClient() *regclient.RegClient {
+func (rootOpts *rootCmd) newRegClient() *regclient.RegClient {
 	conf, err := ConfigLoadDefault()
 	if err != nil {
 		log.WithFields(logrus.Fields{
diff --git a/cmd/regctl/tag.go b/cmd/regctl/tag.go
index 436515e..6ada925 100644
--- a/cmd/regctl/tag.go
+++ b/cmd/regctl/tag.go
@@ -11,46 +11,50 @@ import (
 	"github.com/spf13/cobra"
 )
 
-var tagCmd = &cobra.Command{
-	Use:   "tag <cmd>",
-	Short: "manage tags",
+type tagCmd struct {
+	rootOpts *rootCmd
+	limit    int
+	last     string
+	include  []string
+	exclude  []string
+	format   string
 }
-var tagDeleteCmd = &cobra.Command{
-	Use:     "delete <image_ref>",
-	Aliases: []string{"del", "rm", "remove"},
-	Short:   "delete a tag in a repo",
-	Long: `Delete a tag in a repository.
+
+func NewTagCmd(rootOpts *rootCmd) *cobra.Command {
+	tagOpts := tagCmd{
+		rootOpts: rootOpts,
+	}
+	var tagTopCmd = &cobra.Command{
+		Use:   "tag <cmd>",
+		Short: "manage tags",
+	}
+	var tagDeleteCmd = &cobra.Command{
+		Use:     "delete <image_ref>",
+		Aliases: []string{"del", "rm", "remove"},
+		Short:   "delete a tag in a repo",
+		Long: `Delete a tag in a repository.
 This avoids deleting the manifest when multiple tags reference the same image.
 For registries that do not support the OCI tag delete API, this is implemented
 by pushing a unique dummy manifest and deleting that by digest.
 If the registry does not support the delete API, the dummy manifest will remain.
 `,
-	Args:              cobra.ExactArgs(1),
-	ValidArgsFunction: completeArgTag,
-	RunE:              runTagDelete,
-}
-var tagLsCmd = &cobra.Command{
-	Use:     "ls <repository>",
-	Aliases: []string{"list"},
-	Short:   "list tags in a repo",
-	Long: `List tags in a repository.
+		Args:              cobra.ExactArgs(1),
+		ValidArgsFunction: rootOpts.completeArgTag,
+		RunE:              tagOpts.runTagDelete,
+	}
+	var tagLsCmd = &cobra.Command{
+		Use:     "ls <repository>",
+		Aliases: []string{"list"},
+		Short:   "list tags in a repo",
+		Long: `List tags in a repository.
 Note: many registries ignore the pagination options.
 For an OCI Layout, the index is available as Index (--format "{{.Index}}").
 `,
-	Args:      cobra.ExactArgs(1),
-	ValidArgs: []string{},
-	RunE:      runTagLs,
-}
-
-var tagOpts struct {
-	limit   int
-	last    string
-	include []string
-	exclude []string
-	format  string
-}
+		Args:      cobra.ExactArgs(1),
+		ValidArgs: []string{},
+		RunE:      tagOpts.runTagLs,
+	}
 
-func init() {
 	tagLsCmd.Flags().StringVarP(&tagOpts.last, "last", "", "", "Specify the last tag from a previous request for pagination (depends on registry support)")
 	tagLsCmd.Flags().IntVarP(&tagOpts.limit, "limit", "", 0, "Specify the number of tags to retrieve (depends on registry support)")
 	tagLsCmd.Flags().StringArrayVar(&tagOpts.include, "include", []string{}, "Regexp of tags to include (expression is bound to beginning and ending of tag)")
@@ -61,18 +65,18 @@ func init() {
 	_ = tagLsCmd.RegisterFlagCompletionFunc("filter", completeArgNone)
 	_ = tagLsCmd.RegisterFlagCompletionFunc("format", completeArgNone)
 
-	tagCmd.AddCommand(tagDeleteCmd)
-	tagCmd.AddCommand(tagLsCmd)
-	rootCmd.AddCommand(tagCmd)
+	tagTopCmd.AddCommand(tagDeleteCmd)
+	tagTopCmd.AddCommand(tagLsCmd)
+	return tagTopCmd
 }
 
-func runTagDelete(cmd *cobra.Command, args []string) error {
+func (tagOpts *tagCmd) runTagDelete(cmd *cobra.Command, args []string) error {
 	ctx := cmd.Context()
 	r, err := ref.New(args[0])
 	if err != nil {
 		return err
 	}
-	rc := newRegClient()
+	rc := tagOpts.rootOpts.newRegClient()
 	defer rc.Close(ctx, r)
 	log.WithFields(logrus.Fields{
 		"host":       r.Registry,
@@ -86,7 +90,7 @@ func runTagDelete(cmd *cobra.Command, args []string) error {
 	return nil
 }
 
-func runTagLs(cmd *cobra.Command, args []string) error {
+func (tagOpts *tagCmd) runTagLs(cmd *cobra.Command, args []string) error {
 	ctx := cmd.Context()
 	r, err := ref.New(args[0])
 	if err != nil {
@@ -108,7 +112,7 @@ func runTagLs(cmd *cobra.Command, args []string) error {
 		}
 		reExclude = append(reExclude, re)
 	}
-	rc := newRegClient()
+	rc := tagOpts.rootOpts.newRegClient()
 	defer rc.Close(ctx, r)
 	log.WithFields(logrus.Fields{
 		"host":       r.Registry,
diff --git a/cmd/regsync/main.go b/cmd/regsync/main.go
index acb10aa..29341af 100644
--- a/cmd/regsync/main.go
+++ b/cmd/regsync/main.go
@@ -23,7 +23,8 @@ func main() {
 	}()
 	godbg.SignalTrace()
 
-	if err := rootCmd.ExecuteContext(ctx); err != nil {
+	rootTopCmd := NewRootCmd()
+	if err := rootTopCmd.ExecuteContext(ctx); err != nil {
 		fmt.Fprintf(os.Stderr, "%v\n", err)
 		os.Exit(1)
 	}
diff --git a/cmd/regsync/root.go b/cmd/regsync/root.go
index 2704980..7e4085b 100644
--- a/cmd/regsync/root.go
+++ b/cmd/regsync/root.go
@@ -46,7 +46,7 @@ const (
 	actionMissing
 )
 
-var cliOpts struct {
+type rootCmd struct {
 	confFile  string
 	verbosity string
 	logopts   []string
@@ -54,6 +54,7 @@ var cliOpts struct {
 	missing   bool
 }
 
+// TODO: remove globals
 var (
 	conf      *Config
 	log       *logrus.Logger
@@ -61,92 +62,97 @@ var (
 	throttleC *throttle.Throttle
 )
 
-var rootCmd = &cobra.Command{
-	Use:           "regsync <cmd>",
-	Short:         "Utility for mirroring docker repositories",
-	Long:          usageDesc,
-	SilenceUsage:  true,
-	SilenceErrors: true,
-}
-var serverCmd = &cobra.Command{
-	Use:   "server",
-	Short: "run the regsync server",
-	Long:  `Sync registries according to the configuration.`,
-	Args:  cobra.RangeArgs(0, 0),
-	RunE:  runServer,
+func init() {
+	log = &logrus.Logger{
+		Out:       os.Stderr,
+		Formatter: new(logrus.TextFormatter),
+		Hooks:     make(logrus.LevelHooks),
+		Level:     logrus.InfoLevel,
+	}
 }
-var checkCmd = &cobra.Command{
-	Use:   "check",
-	Short: "processes each sync command once but skip actual copy",
-	Long: `Processes each sync command in the configuration file in order.
+
+func NewRootCmd() *cobra.Command {
+	rootOpts := rootCmd{}
+	var rootTopCmd = &cobra.Command{
+		Use:           "regsync <cmd>",
+		Short:         "Utility for mirroring docker repositories",
+		Long:          usageDesc,
+		SilenceUsage:  true,
+		SilenceErrors: true,
+	}
+	var serverCmd = &cobra.Command{
+		Use:   "server",
+		Short: "run the regsync server",
+		Long:  `Sync registries according to the configuration.`,
+		Args:  cobra.RangeArgs(0, 0),
+		RunE:  rootOpts.runServer,
+	}
+	var checkCmd = &cobra.Command{
+		Use:   "check",
+		Short: "processes each sync command once but skip actual copy",
+		Long: `Processes each sync command in the configuration file in order.
 Manifests are checked to see if a copy is needed, but only log, skip copying.
 No jobs are run in parallel, and the command returns after any error or last
 sync step is finished.`,
-	Args: cobra.RangeArgs(0, 0),
-	RunE: runCheck,
-}
-var onceCmd = &cobra.Command{
-	Use:   "once",
-	Short: "processes each sync command once, ignoring cron schedule",
-	Long: `Processes each sync command in the configuration file in order.
+		Args: cobra.RangeArgs(0, 0),
+		RunE: rootOpts.runCheck,
+	}
+	var onceCmd = &cobra.Command{
+		Use:   "once",
+		Short: "processes each sync command once, ignoring cron schedule",
+		Long: `Processes each sync command in the configuration file in order.
 No jobs are run in parallel, and the command returns after any error or last
 sync step is finished.`,
-	Args: cobra.RangeArgs(0, 0),
-	RunE: runOnce,
-}
-
-var configCmd = &cobra.Command{
-	Use:   "config",
-	Short: "Show the config",
-	Long:  `Show the config`,
-	Args:  cobra.RangeArgs(0, 0),
-	RunE:  runConfig,
-}
+		Args: cobra.RangeArgs(0, 0),
+		RunE: rootOpts.runOnce,
+	}
 
-var versionCmd = &cobra.Command{
-	Use:   "version",
-	Short: "Show the version",
-	Long:  `Show the version`,
-	Args:  cobra.RangeArgs(0, 0),
-	RunE:  runVersion,
-}
+	var configCmd = &cobra.Command{
+		Use:   "config",
+		Short: "Show the config",
+		Long:  `Show the config`,
+		Args:  cobra.RangeArgs(0, 0),
+		RunE:  rootOpts.runConfig,
+	}
 
-func init() {
-	log = &logrus.Logger{
-		Out:       os.Stderr,
-		Formatter: new(logrus.TextFormatter),
-		Hooks:     make(logrus.LevelHooks),
-		Level:     logrus.InfoLevel,
+	var versionCmd = &cobra.Command{
+		Use:   "version",
+		Short: "Show the version",
+		Long:  `Show the version`,
+		Args:  cobra.RangeArgs(0, 0),
+		RunE:  rootOpts.runVersion,
 	}
-	rootCmd.PersistentFlags().StringVarP(&cliOpts.confFile, "config", "c", "", "Config file")
-	rootCmd.PersistentFlags().StringVarP(&cliOpts.verbosity, "verbosity", "v", logrus.InfoLevel.String(), "Log level (debug, info, warn, error, fatal, panic)")
-	rootCmd.PersistentFlags().StringArrayVar(&cliOpts.logopts, "logopt", []string{}, "Log options")
-	versionCmd.Flags().StringVar(&cliOpts.format, "format", "{{printPretty .}}", "Format output with go template syntax")
-	onceCmd.Flags().BoolVar(&cliOpts.missing, "missing", false, "Only copy tags that are missing on target")
 
-	_ = rootCmd.MarkPersistentFlagFilename("config")
+	rootTopCmd.PersistentFlags().StringVarP(&rootOpts.confFile, "config", "c", "", "Config file")
+	rootTopCmd.PersistentFlags().StringVarP(&rootOpts.verbosity, "verbosity", "v", logrus.InfoLevel.String(), "Log level (debug, info, warn, error, fatal, panic)")
+	rootTopCmd.PersistentFlags().StringArrayVar(&rootOpts.logopts, "logopt", []string{}, "Log options")
+	versionCmd.Flags().StringVar(&rootOpts.format, "format", "{{printPretty .}}", "Format output with go template syntax")
+	onceCmd.Flags().BoolVar(&rootOpts.missing, "missing", false, "Only copy tags that are missing on target")
+
+	_ = rootTopCmd.MarkPersistentFlagFilename("config")
 	_ = serverCmd.MarkPersistentFlagRequired("config")
 	_ = checkCmd.MarkPersistentFlagRequired("config")
 	_ = onceCmd.MarkPersistentFlagRequired("config")
 	_ = configCmd.MarkPersistentFlagRequired("config")
 
-	rootCmd.AddCommand(serverCmd)
-	rootCmd.AddCommand(checkCmd)
-	rootCmd.AddCommand(onceCmd)
-	rootCmd.AddCommand(configCmd)
-	rootCmd.AddCommand(versionCmd)
+	rootTopCmd.AddCommand(serverCmd)
+	rootTopCmd.AddCommand(checkCmd)
+	rootTopCmd.AddCommand(onceCmd)
+	rootTopCmd.AddCommand(configCmd)
+	rootTopCmd.AddCommand(versionCmd)
 
-	rootCmd.PersistentPreRunE = rootPreRun
+	rootTopCmd.PersistentPreRunE = rootOpts.rootPreRun
+	return rootTopCmd
 }
 
-func rootPreRun(cmd *cobra.Command, args []string) error {
-	lvl, err := logrus.ParseLevel(cliOpts.verbosity)
+func (rootOpts *rootCmd) rootPreRun(cmd *cobra.Command, args []string) error {
+	lvl, err := logrus.ParseLevel(rootOpts.verbosity)
 	if err != nil {
 		return err
 	}
 	log.SetLevel(lvl)
 	log.Formatter = &logrus.TextFormatter{FullTimestamp: true}
-	for _, opt := range cliOpts.logopts {
+	for _, opt := range rootOpts.logopts {
 		if opt == "json" {
 			log.Formatter = new(logrus.JSONFormatter)
 		}
@@ -154,14 +160,14 @@ func rootPreRun(cmd *cobra.Command, args []string) error {
 	return nil
 }
 
-func runVersion(cmd *cobra.Command, args []string) error {
+func (rootOpts *rootCmd) runVersion(cmd *cobra.Command, args []string) error {
 	info := version.GetInfo()
-	return template.Writer(os.Stdout, cliOpts.format, info)
+	return template.Writer(os.Stdout, rootOpts.format, info)
 }
 
 // runConfig processes the file in one pass, ignoring cron
-func runConfig(cmd *cobra.Command, args []string) error {
-	err := loadConf()
+func (rootOpts *rootCmd) runConfig(cmd *cobra.Command, args []string) error {
+	err := rootOpts.loadConf()
 	if err != nil {
 		return err
 	}
@@ -170,13 +176,13 @@ func runConfig(cmd *cobra.Command, args []string) error {
 }
 
 // runOnce processes the file in one pass, ignoring cron
-func runOnce(cmd *cobra.Command, args []string) error {
-	err := loadConf()
+func (rootOpts *rootCmd) runOnce(cmd *cobra.Command, args []string) error {
+	err := rootOpts.loadConf()
 	if err != nil {
 		return err
 	}
 	action := actionCopy
-	if cliOpts.missing {
+	if rootOpts.missing {
 		action = actionMissing
 	}
 	ctx := cmd.Context()
@@ -188,7 +194,7 @@ func runOnce(cmd *cobra.Command, args []string) error {
 			wg.Add(1)
 			go func() {
 				defer wg.Done()
-				err := s.process(ctx, action)
+				err := rootOpts.process(ctx, s, action)
 				if err != nil {
 					if mainErr == nil {
 						mainErr = err
@@ -197,7 +203,7 @@ func runOnce(cmd *cobra.Command, args []string) error {
 				}
 			}()
 		} else {
-			err := s.process(ctx, action)
+			err := rootOpts.process(ctx, s, action)
 			if err != nil {
 				if mainErr == nil {
 					mainErr = err
@@ -210,8 +216,8 @@ func runOnce(cmd *cobra.Command, args []string) error {
 }
 
 // runServer stays running with cron scheduled tasks
-func runServer(cmd *cobra.Command, args []string) error {
-	err := loadConf()
+func (rootOpts *rootCmd) runServer(cmd *cobra.Command, args []string) error {
+	err := rootOpts.loadConf()
 	if err != nil {
 		return err
 	}
@@ -243,7 +249,7 @@ func runServer(cmd *cobra.Command, args []string) error {
 				}).Debug("Running task")
 				wg.Add(1)
 				defer wg.Done()
-				err := s.process(ctx, actionCopy)
+				err := rootOpts.process(ctx, s, actionCopy)
 				if mainErr == nil {
 					mainErr = err
 				}
@@ -264,7 +270,7 @@ func runServer(cmd *cobra.Command, args []string) error {
 				wg.Add(1)
 				go func() {
 					defer wg.Done()
-					err := s.process(ctx, actionMissing)
+					err := rootOpts.process(ctx, s, actionMissing)
 					if err != nil {
 						if mainErr == nil {
 							mainErr = err
@@ -273,7 +279,7 @@ func runServer(cmd *cobra.Command, args []string) error {
 					}
 				}()
 			} else {
-				err := s.process(ctx, actionMissing)
+				err := rootOpts.process(ctx, s, actionMissing)
 				if err != nil {
 					if mainErr == nil {
 						mainErr = err
@@ -305,15 +311,15 @@ func runServer(cmd *cobra.Command, args []string) error {
 }
 
 // run check is used for a dry-run
-func runCheck(cmd *cobra.Command, args []string) error {
-	err := loadConf()
+func (rootOpts *rootCmd) runCheck(cmd *cobra.Command, args []string) error {
+	err := rootOpts.loadConf()
 	if err != nil {
 		return err
 	}
 	var mainErr error
 	ctx := cmd.Context()
 	for _, s := range conf.Sync {
-		err := s.process(ctx, actionCheck)
+		err := rootOpts.process(ctx, s, actionCheck)
 		if err != nil {
 			if mainErr == nil {
 				mainErr = err
@@ -323,15 +329,15 @@ func runCheck(cmd *cobra.Command, args []string) error {
 	return mainErr
 }
 
-func loadConf() error {
+func (rootOpts *rootCmd) loadConf() error {
 	var err error
-	if cliOpts.confFile == "-" {
+	if rootOpts.confFile == "-" {
 		conf, err = ConfigLoadReader(os.Stdin)
 		if err != nil {
 			return err
 		}
-	} else if cliOpts.confFile != "" {
-		r, err := os.Open(cliOpts.confFile)
+	} else if rootOpts.confFile != "" {
+		r, err := os.Open(rootOpts.confFile)
 		if err != nil {
 			return err
 		}
@@ -392,18 +398,18 @@ func loadConf() error {
 }
 
 // process a sync step
-func (s ConfigSync) process(ctx context.Context, action actionType) error {
+func (rootOpts *rootCmd) process(ctx context.Context, s ConfigSync, action actionType) error {
 	switch s.Type {
 	case "registry":
-		if err := s.processRegistry(ctx, s.Source, s.Target, action); err != nil {
+		if err := rootOpts.processRegistry(ctx, s, s.Source, s.Target, action); err != nil {
 			return err
 		}
 	case "repository":
-		if err := s.processRepo(ctx, s.Source, s.Target, action); err != nil {
+		if err := rootOpts.processRepo(ctx, s, s.Source, s.Target, action); err != nil {
 			return err
 		}
 	case "image":
-		if err := s.processImage(ctx, s.Source, s.Target, action); err != nil {
+		if err := rootOpts.processImage(ctx, s, s.Source, s.Target, action); err != nil {
 			return err
 		}
 	default:
@@ -416,7 +422,7 @@ func (s ConfigSync) process(ctx context.Context, action actionType) error {
 	return nil
 }
 
-func (s ConfigSync) processRegistry(ctx context.Context, src, tgt string, action actionType) error {
+func (rootOpts *rootCmd) processRegistry(ctx context.Context, s ConfigSync, src, tgt string, action actionType) error {
 	last := ""
 	var retErr error
 	for {
@@ -456,7 +462,7 @@ func (s ConfigSync) processRegistry(ctx context.Context, src, tgt string, action
 			return err
 		}
 		for _, repo := range sRepoList {
-			if err := s.processRepo(ctx, fmt.Sprintf("%s/%s", src, repo), fmt.Sprintf("%s/%s", tgt, repo), action); err != nil {
+			if err := rootOpts.processRepo(ctx, s, fmt.Sprintf("%s/%s", src, repo), fmt.Sprintf("%s/%s", tgt, repo), action); err != nil {
 				retErr = err
 			}
 		}
@@ -464,7 +470,7 @@ func (s ConfigSync) processRegistry(ctx context.Context, src, tgt string, action
 	return retErr
 }
 
-func (s ConfigSync) processRepo(ctx context.Context, src, tgt string, action actionType) error {
+func (rootOpts *rootCmd) processRepo(ctx context.Context, s ConfigSync, src, tgt string, action actionType) error {
 	sRepoRef, err := ref.New(src)
 	if err != nil {
 		log.WithFields(logrus.Fields{
@@ -560,14 +566,14 @@ func (s ConfigSync) processRepo(ctx context.Context, src, tgt string, action act
 	}
 	var retErr error
 	for _, tag := range sTagList {
-		if err := s.processImage(ctx, fmt.Sprintf("%s:%s", src, tag), fmt.Sprintf("%s:%s", tgt, tag), action); err != nil {
+		if err := rootOpts.processImage(ctx, s, fmt.Sprintf("%s:%s", src, tag), fmt.Sprintf("%s:%s", tgt, tag), action); err != nil {
 			retErr = err
 		}
 	}
 	return retErr
 }
 
-func (s ConfigSync) processImage(ctx context.Context, src, tgt string, action actionType) error {
+func (rootOpts *rootCmd) processImage(ctx context.Context, s ConfigSync, src, tgt string, action actionType) error {
 	sRef, err := ref.New(src)
 	if err != nil {
 		log.WithFields(logrus.Fields{
@@ -584,7 +590,7 @@ func (s ConfigSync) processImage(ctx context.Context, src, tgt string, action ac
 		}).Error("Failed parsing target")
 		return err
 	}
-	err = s.processRef(ctx, sRef, tRef, action)
+	err = rootOpts.processRef(ctx, s, sRef, tRef, action)
 	if err != nil {
 		log.WithFields(logrus.Fields{
 			"target": tRef.CommonName(),
@@ -602,7 +608,7 @@ func (s ConfigSync) processImage(ctx context.Context, src, tgt string, action ac
 }
 
 // process a sync step
-func (s ConfigSync) processRef(ctx context.Context, src, tgt ref.Ref, action actionType) error {
+func (rootOpts *rootCmd) processRef(ctx context.Context, s ConfigSync, src, tgt ref.Ref, action actionType) error {
 	mSrc, err := rc.ManifestHead(ctx, src, regclient.WithManifestRequireDigest())
 	if err != nil && errors.Is(err, types.ErrUnsupportedAPI) {
 		mSrc, err = rc.ManifestGet(ctx, src)


Test patch:
diff --git a/cmd/regbot/regbot_test.go b/cmd/regbot/regbot_test.go
index 13d2a6a..2801228 100644
--- a/cmd/regbot/regbot_test.go
+++ b/cmd/regbot/regbot_test.go
@@ -154,8 +154,10 @@ func TestRegbot(t *testing.T) {
 	}
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
-			rootOpts.dryRun = tt.dryrun
-			err = tt.script.process(ctx)
+			rootOpts := rootCmd{
+				dryRun: tt.dryrun,
+			}
+			err = rootOpts.process(ctx, tt.script)
 			if tt.expErr != nil {
 				if err == nil {
 					t.Errorf("process did not fail")
diff --git a/cmd/regctl/artifact_test.go b/cmd/regctl/artifact_test.go
index 816b56d..3eebe05 100644
--- a/cmd/regctl/artifact_test.go
+++ b/cmd/regctl/artifact_test.go
@@ -13,7 +13,6 @@ import (
 )
 
 func TestArtifactGet(t *testing.T) {
-	saveArtifactOpts := artifactOpts
 	tt := []struct {
 		name        string
 		args        []string
@@ -54,8 +53,7 @@ func TestArtifactGet(t *testing.T) {
 	}
 	for _, tc := range tt {
 		t.Run(tc.name, func(t *testing.T) {
-			out, err := cobraTest(t, tc.args...)
-			artifactOpts = saveArtifactOpts
+			out, err := cobraTest(t, nil, tc.args...)
 			if tc.expectErr != nil {
 				if err == nil {
 					t.Errorf("did not receive expected error: %v", tc.expectErr)
@@ -76,7 +74,6 @@ func TestArtifactGet(t *testing.T) {
 }
 
 func TestArtifactList(t *testing.T) {
-	saveArtifactOpts := artifactOpts
 	tt := []struct {
 		name        string
 		args        []string
@@ -131,8 +128,7 @@ func TestArtifactList(t *testing.T) {
 	}
 	for _, tc := range tt {
 		t.Run(tc.name, func(t *testing.T) {
-			out, err := cobraTest(t, tc.args...)
-			artifactOpts = saveArtifactOpts
+			out, err := cobraTest(t, nil, tc.args...)
 			if tc.expectErr != nil {
 				if err == nil {
 					t.Errorf("did not receive expected error: %v", tc.expectErr)
@@ -161,7 +157,6 @@ func TestArtifactPut(t *testing.T) {
 		t.Errorf("failed creating test conf: %v", err)
 		return
 	}
-	saveArtifactOpts := artifactOpts
 
 	tt := []struct {
 		name        string
@@ -219,13 +214,11 @@ func TestArtifactPut(t *testing.T) {
 	}
 	for _, tc := range tt {
 		t.Run(tc.name, func(t *testing.T) {
+			cobraOpts := cobraTestOpts{}
 			if tc.in != nil {
-				origIn := rootCmd.InOrStdin()
-				defer rootCmd.SetIn(origIn)
-				rootCmd.SetIn(bytes.NewBuffer(tc.in))
+				cobraOpts.stdin = bytes.NewBuffer(tc.in)
 			}
-			out, err := cobraTest(t, tc.args...)
-			artifactOpts = saveArtifactOpts
+			out, err := cobraTest(t, &cobraOpts, tc.args...)
 			if tc.expectErr != nil {
 				if err == nil {
 					t.Errorf("did not receive expected error: %v", tc.expectErr)
@@ -243,13 +236,9 @@ func TestArtifactPut(t *testing.T) {
 			}
 		})
 	}
-	// reset flags
-	artifactOpts.artifactConfig = ""
-	artifactOpts.subject = ""
 }
 
 func TestArtifactTree(t *testing.T) {
-	saveArtifactOpts := artifactOpts
 	tt := []struct {
 		name        string
 		args        []string
@@ -309,8 +298,7 @@ func TestArtifactTree(t *testing.T) {
 	}
 	for _, tc := range tt {
 		t.Run(tc.name, func(t *testing.T) {
-			out, err := cobraTest(t, tc.args...)
-			artifactOpts = saveArtifactOpts
+			out, err := cobraTest(t, nil, tc.args...)
 			if tc.expectErr != nil {
 				if err == nil {
 					t.Errorf("did not receive expected error: %v", tc.expectErr)
diff --git a/cmd/regctl/blob_test.go b/cmd/regctl/blob_test.go
index e4ff717..902e14e 100644
--- a/cmd/regctl/blob_test.go
+++ b/cmd/regctl/blob_test.go
@@ -8,31 +8,27 @@ import (
 // TODO: implement tests
 
 func TestBlob(t *testing.T) {
-	saveBlobOpts := blobOpts
-	saveManifestOpts := manifestOpts
 	repo := "ocidir://../../testdata/testrepo"
-	digBaseA, err := cobraTest(t, "manifest", "get", repo+":b1", "--platform", "linux/amd64", "--format", "{{(index .Layers 0).Digest}}")
+	digBaseA, err := cobraTest(t, nil, "manifest", "get", repo+":b1", "--platform", "linux/amd64", "--format", "{{(index .Layers 0).Digest}}")
 	if err != nil {
 		t.Errorf("failed getting layer digest: %v", err)
 	}
-	digBaseB, err := cobraTest(t, "manifest", "get", repo+":b3", "--platform", "linux/amd64", "--format", "{{(index .Layers 0).Digest}}")
+	digBaseB, err := cobraTest(t, nil, "manifest", "get", repo+":b3", "--platform", "linux/amd64", "--format", "{{(index .Layers 0).Digest}}")
 	if err != nil {
 		t.Errorf("failed getting layer digest: %v", err)
 	}
-	digConf1, err := cobraTest(t, "manifest", "get", repo+":b1", "--platform", "linux/amd64", "--format", "{{.Config.Digest}}")
+	digConf1, err := cobraTest(t, nil, "manifest", "get", repo+":b1", "--platform", "linux/amd64", "--format", "{{.Config.Digest}}")
 	if err != nil {
 		t.Errorf("failed getting layer digest: %v", err)
 	}
-	digConf3, err := cobraTest(t, "manifest", "get", repo+":b3", "--platform", "linux/amd64", "--format", "{{.Config.Digest}}")
+	digConf3, err := cobraTest(t, nil, "manifest", "get", repo+":b3", "--platform", "linux/amd64", "--format", "{{.Config.Digest}}")
 	if err != nil {
 		t.Errorf("failed getting layer digest: %v", err)
 	}
-	manifestOpts = saveManifestOpts
 
 	t.Run("Get", func(t *testing.T) {
 		// run a get request
-		out, err := cobraTest(t, "blob", "get", "--format", "{{printPretty .}}", repo, digBaseA)
-		blobOpts = saveBlobOpts
+		out, err := cobraTest(t, nil, "blob", "get", "--format", "{{printPretty .}}", repo, digBaseA)
 		if err != nil {
 			t.Errorf("failed to blob get: %v", err)
 		}
@@ -40,8 +36,7 @@ func TestBlob(t *testing.T) {
 			t.Errorf("no blob output received")
 		}
 		// run a head request
-		out, err = cobraTest(t, "blob", "head", "--format", "{{printPretty .}}", repo, digBaseA)
-		blobOpts = saveBlobOpts
+		out, err = cobraTest(t, nil, "blob", "head", "--format", "{{printPretty .}}", repo, digBaseA)
 		if err != nil {
 			t.Errorf("failed to blob head: %v", err)
 		}
@@ -49,8 +44,7 @@ func TestBlob(t *testing.T) {
 			t.Errorf("no blob output received")
 		}
 		// get a file from the blob
-		out, err = cobraTest(t, "blob", "get-file", repo, digBaseA, "base.txt")
-		blobOpts = saveBlobOpts
+		out, err = cobraTest(t, nil, "blob", "get-file", repo, digBaseA, "base.txt")
 		if err != nil {
 			t.Errorf("failed to blob get-file: %v", err)
 		}
@@ -62,19 +56,16 @@ func TestBlob(t *testing.T) {
 	t.Run("Put", func(t *testing.T) {
 		dir := t.TempDir()
 		bufStr := "hello world"
-		buf := bytes.NewBufferString(bufStr)
-		origIn := rootCmd.InOrStdin()
-		rootCmd.SetIn(buf)
-		defer rootCmd.SetIn(origIn)
+		cobraOpts := cobraTestOpts{
+			stdin: bytes.NewBufferString(bufStr),
+		}
 		// put a blob
-		dig, err := cobraTest(t, "blob", "put", "--format", "{{println .Digest}}", "ocidir://"+dir)
-		blobOpts = saveBlobOpts
+		dig, err := cobraTest(t, &cobraOpts, "blob", "put", "--format", "{{println .Digest}}", "ocidir://"+dir)
 		if err != nil {
 			t.Errorf("failed to blob copy: %v", err)
 		}
 		// get the blob from the tempdir
-		out, err := cobraTest(t, "blob", "get", "--format", "{{printPretty .}}", "ocidir://"+dir, dig)
-		blobOpts = saveBlobOpts
+		out, err := cobraTest(t, nil, "blob", "get", "--format", "{{printPretty .}}", "ocidir://"+dir, dig)
 		if err != nil {
 			t.Errorf("failed to blob get: %v", err)
 		}
@@ -86,14 +77,12 @@ func TestBlob(t *testing.T) {
 	t.Run("Copy", func(t *testing.T) {
 		dir := t.TempDir()
 		// copy the blob to the tempdir
-		_, err := cobraTest(t, "blob", "copy", repo, "ocidir://"+dir, digBaseA)
-		blobOpts = saveBlobOpts
+		_, err := cobraTest(t, nil, "blob", "copy", repo, "ocidir://"+dir, digBaseA)
 		if err != nil {
 			t.Errorf("failed to blob copy: %v", err)
 		}
 		// get the blob from the tempdir
-		out, err := cobraTest(t, "blob", "get", "--format", "{{printPretty .}}", "ocidir://"+dir, digBaseA)
-		blobOpts = saveBlobOpts
+		out, err := cobraTest(t, nil, "blob", "get", "--format", "{{printPretty .}}", "ocidir://"+dir, digBaseA)
 		if err != nil {
 			t.Errorf("failed to blob get: %v", err)
 		}
@@ -104,8 +93,7 @@ func TestBlob(t *testing.T) {
 
 	t.Run("Diff", func(t *testing.T) {
 		// diff the layers between two images
-		out, err := cobraTest(t, "blob", "diff-layer", repo, digBaseA, repo, digBaseB)
-		blobOpts = saveBlobOpts
+		out, err := cobraTest(t, nil, "blob", "diff-layer", repo, digBaseA, repo, digBaseB)
 		if err != nil {
 			t.Errorf("failed to diff layers: %v", err)
 		}
@@ -113,8 +101,7 @@ func TestBlob(t *testing.T) {
 			t.Errorf("no output received from diff-layer")
 		}
 		// diff the config between two images
-		out, err = cobraTest(t, "blob", "diff-config", repo, digConf1, repo, digConf3)
-		blobOpts = saveBlobOpts
+		out, err = cobraTest(t, nil, "blob", "diff-config", repo, digConf1, repo, digConf3)
 		if err != nil {
 			t.Errorf("failed to diff config: %v", err)
 		}
diff --git a/cmd/regctl/config_test.go b/cmd/regctl/config_test.go
index 9805ee4..6211961 100644
--- a/cmd/regctl/config_test.go
+++ b/cmd/regctl/config_test.go
@@ -14,11 +14,9 @@ func TestConfig(t *testing.T) {
 		defer os.Setenv(ConfigEnv, origEnv)
 	}
 	os.Setenv(ConfigEnv, filepath.Join(tempDir, "config.json"))
-	saveConfigOpts := configOpts
 
 	// test empty config
-	out, err := cobraTest(t, "config", "get", "--format", "{{ json . }}")
-	configOpts = saveConfigOpts
+	out, err := cobraTest(t, nil, "config", "get", "--format", "{{ json . }}")
 	if err != nil {
 		t.Errorf("failed to run config get: %v", err)
 	}
@@ -28,8 +26,7 @@ func TestConfig(t *testing.T) {
 
 	// set options
 	testLimit := "420000000"
-	out, err = cobraTest(t, "config", "set", "--blob-limit", testLimit, "--docker-cert=false", "--docker-cred=false")
-	configOpts = saveConfigOpts
+	out, err = cobraTest(t, nil, "config", "set", "--blob-limit", testLimit, "--docker-cert=false", "--docker-cred=false")
 	if err != nil {
 		t.Errorf("failed to set config: %v", err)
 	}
@@ -38,24 +35,21 @@ func TestConfig(t *testing.T) {
 	}
 
 	// get changes
-	out, err = cobraTest(t, "config", "get", "--format", "{{ .BlobLimit }}")
-	configOpts = saveConfigOpts
+	out, err = cobraTest(t, nil, "config", "get", "--format", "{{ .BlobLimit }}")
 	if err != nil {
 		t.Errorf("failed to run config get on blob-limit: %v", err)
 	}
 	if out != testLimit {
 		t.Errorf("unexpected output for blob-limit, expected: %s, received: %s", testLimit, out)
 	}
-	out, err = cobraTest(t, "config", "get", "--format", "{{ .IncDockerCert }}")
-	configOpts = saveConfigOpts
+	out, err = cobraTest(t, nil, "config", "get", "--format", "{{ .IncDockerCert }}")
 	if err != nil {
 		t.Errorf("failed to run config get on docker-cert: %v", err)
 	}
 	if out != "false" {
 		t.Errorf("unexpected output for docker-cert, expected: false, received: %s", out)
 	}
-	out, err = cobraTest(t, "config", "get", "--format", "{{ .IncDockerCred }}")
-	configOpts = saveConfigOpts
+	out, err = cobraTest(t, nil, "config", "get", "--format", "{{ .IncDockerCred }}")
 	if err != nil {
 		t.Errorf("failed to run config get on docker-cred: %v", err)
 	}
@@ -64,8 +58,7 @@ func TestConfig(t *testing.T) {
 	}
 
 	// reset back to zero values
-	out, err = cobraTest(t, "config", "set", "--blob-limit", "0", "--docker-cert", "--docker-cred")
-	configOpts = saveConfigOpts
+	out, err = cobraTest(t, nil, "config", "set", "--blob-limit", "0", "--docker-cert", "--docker-cred")
 	if err != nil {
 		t.Errorf("failed to set blob-limit: %v", err)
 	}
@@ -74,8 +67,7 @@ func TestConfig(t *testing.T) {
 	}
 
 	// verify config is empty
-	out, err = cobraTest(t, "config", "get", "--format", "{{ json . }}")
-	configOpts = saveConfigOpts
+	out, err = cobraTest(t, nil, "config", "get", "--format", "{{ json . }}")
 	if err != nil {
 		t.Errorf("failed to run config get: %v", err)
 	}
diff --git a/cmd/regctl/image_test.go b/cmd/regctl/image_test.go
index bb54dc5..55ce2b0 100644
--- a/cmd/regctl/image_test.go
+++ b/cmd/regctl/image_test.go
@@ -11,10 +11,8 @@ func TestImageExportImport(t *testing.T) {
 	exportFile := tmpDir + "/export.tar"
 	exportName := "registry.example.com/repo:v2"
 	importRefA := fmt.Sprintf("ocidir://%s/repo:v2", tmpDir)
-	saveOpts := imageOpts
 
-	out, err := cobraTest(t, "image", "export", "--name", exportName, srcRef, exportFile)
-	imageOpts = saveOpts
+	out, err := cobraTest(t, nil, "image", "export", "--name", exportName, srcRef, exportFile)
 	if err != nil {
 		t.Errorf("failed to run image export: %v", err)
 		return
@@ -23,8 +21,7 @@ func TestImageExportImport(t *testing.T) {
 		t.Errorf("unexpected output: %v", out)
 	}
 
-	out, err = cobraTest(t, "image", "import", importRefA, exportFile)
-	imageOpts = saveOpts
+	out, err = cobraTest(t, nil, "image", "import", importRefA, exportFile)
 	if err != nil {
 		t.Errorf("failed to run image import: %v", err)
 		return
@@ -33,8 +30,7 @@ func TestImageExportImport(t *testing.T) {
 		t.Errorf("unexpected output: %v", out)
 	}
 
-	out, err = cobraTest(t, "image", "export", "--name", exportName, "--platform", "linux/amd64", srcRef, exportFile)
-	imageOpts = saveOpts
+	out, err = cobraTest(t, nil, "image", "export", "--name", exportName, "--platform", "linux/amd64", srcRef, exportFile)
 	if err != nil {
 		t.Errorf("failed to run image export: %v", err)
 		return
@@ -49,10 +45,8 @@ func TestImageMod(t *testing.T) {
 	srcRef := "ocidir://../../testdata/testrepo:v3"
 	baseRef := "ocidir://../../testdata/testrepo:b1"
 	modRef := fmt.Sprintf("ocidir://%s/repo:mod", tmpDir)
-	saveOpts := imageOpts
 
-	out, err := cobraTest(t, "image", "mod", srcRef, "--create", modRef, "--time", "set=2000-01-01T00:00:00Z,base-ref="+baseRef)
-	imageOpts = saveOpts
+	out, err := cobraTest(t, nil, "image", "mod", srcRef, "--create", modRef, "--time", "set=2000-01-01T00:00:00Z,base-ref="+baseRef)
 	if err != nil {
 		t.Errorf("failed to run image mod: %v", err)
 		return
diff --git a/cmd/regctl/index_test.go b/cmd/regctl/index_test.go
index fc38b4b..bbdfc5d 100644
--- a/cmd/regctl/index_test.go
+++ b/cmd/regctl/index_test.go
@@ -10,13 +10,9 @@ func TestIndex(t *testing.T) {
 	latestRef := fmt.Sprintf("ocidir://%s/repo:latest", tmpDir)
 	artifactRef := fmt.Sprintf("ocidir://%s/repo:latest", tmpDir)
 	srcRef := "ocidir://../../testdata/testrepo:v2"
-	saveIndexOpts := indexOpts
-	saveManifestOpts := manifestOpts
-	saveArtifactOpts := artifactOpts
 
 	// create index with 2 platforms from test repo
-	out, err := cobraTest(t, "index", "create", "--ref", srcRef, "--platform", "linux/amd64", "--platform", "linux/arm/v7", latestRef)
-	indexOpts = saveIndexOpts
+	out, err := cobraTest(t, nil, "index", "create", "--ref", srcRef, "--platform", "linux/amd64", "--platform", "linux/arm/v7", latestRef)
 	if err != nil {
 		t.Errorf("failed to run index create: %v", err)
 		return
@@ -25,30 +21,25 @@ func TestIndex(t *testing.T) {
 		t.Errorf("unexpected output: %s", out)
 	}
 	// verify content
-	_, err = cobraTest(t, "manifest", "get", "--platform", "linux/amd64", latestRef)
-	manifestOpts = saveManifestOpts
+	_, err = cobraTest(t, nil, "manifest", "get", "--platform", "linux/amd64", latestRef)
 	if err != nil {
 		t.Errorf("failed to get linux/amd64 entry: %v", err)
 	}
-	_, err = cobraTest(t, "manifest", "get", "--platform", "linux/arm/v7", latestRef)
-	manifestOpts = saveManifestOpts
+	_, err = cobraTest(t, nil, "manifest", "get", "--platform", "linux/arm/v7", latestRef)
 	if err != nil {
 		t.Errorf("failed to get linux/arm/v7 entry: %v", err)
 	}
-	_, err = cobraTest(t, "manifest", "get", "--platform", "linux/arm64", latestRef)
-	manifestOpts = saveManifestOpts
+	_, err = cobraTest(t, nil, "manifest", "get", "--platform", "linux/arm64", latestRef)
 	if err == nil {
 		t.Errorf("found linux/arm64 entry")
 	}
-	_, err = cobraTest(t, "artifact", "get", "--subject", latestRef, "--platform", "linux/amd/v7", "--filter-artifact-type", "application/example.arms")
-	artifactOpts = saveArtifactOpts
+	_, err = cobraTest(t, nil, "artifact", "get", "--subject", latestRef, "--platform", "linux/amd/v7", "--filter-artifact-type", "application/example.arms")
 	if err == nil {
 		t.Errorf("found referrers that were not copied")
 	}
 
 	// add artifact with referrers
-	out, err = cobraTest(t, "index", "add", "--ref", srcRef, "--platform", "linux/arm64", "--referrers", "--digest-tags", latestRef)
-	indexOpts = saveIndexOpts
+	out, err = cobraTest(t, nil, "index", "add", "--ref", srcRef, "--platform", "linux/arm64", "--referrers", "--digest-tags", latestRef)
 	if err != nil {
 		t.Errorf("failed to run index add: %v", err)
 		return
@@ -56,13 +47,11 @@ func TestIndex(t *testing.T) {
 	if out != "" {
 		t.Errorf("unexpected output: %s", out)
 	}
-	_, err = cobraTest(t, "manifest", "get", "--platform", "linux/arm64", latestRef)
-	manifestOpts = saveManifestOpts
+	_, err = cobraTest(t, nil, "manifest", "get", "--platform", "linux/arm64", latestRef)
 	if err != nil {
 		t.Errorf("failed to get linux/arm64 entry: %v", err)
 	}
-	out, err = cobraTest(t, "artifact", "get", "--subject", latestRef, "--platform", "linux/arm64", "--filter-artifact-type", "application/example.arms")
-	artifactOpts = saveArtifactOpts
+	out, err = cobraTest(t, nil, "artifact", "get", "--subject", latestRef, "--platform", "linux/arm64", "--filter-artifact-type", "application/example.arms")
 	if err != nil {
 		t.Errorf("artifact not found: %v", err)
 	}
@@ -73,8 +62,7 @@ func TestIndex(t *testing.T) {
 
 	// create an index that itself is an artifact
 	testArtifactType := "application/example.test"
-	out, err = cobraTest(t, "index", "create", artifactRef, "--subject", "latest", "--artifact-type", testArtifactType, "--ref", srcRef)
-	indexOpts = saveIndexOpts
+	out, err = cobraTest(t, nil, "index", "create", artifactRef, "--subject", "latest", "--artifact-type", testArtifactType, "--ref", srcRef)
 	if err != nil {
 		t.Errorf("failed to run index create for artifact: %v", err)
 		return
@@ -82,8 +70,7 @@ func TestIndex(t *testing.T) {
 	if out != "" {
 		t.Errorf("unexpected output: %s", out)
 	}
-	out, err = cobraTest(t, "manifest", "get", artifactRef, "--format", "{{.ArtifactType}}")
-	manifestOpts = saveManifestOpts
+	out, err = cobraTest(t, nil, "manifest", "get", artifactRef, "--format", "{{.ArtifactType}}")
 	if err != nil {
 		t.Errorf("failed to get artifact type from manifest: %v", err)
 	}
diff --git a/cmd/regctl/main_test.go b/cmd/regctl/main_test.go
index 52f5edf..2a8ed8a 100644
--- a/cmd/regctl/main_test.go
+++ b/cmd/regctl/main_test.go
@@ -2,18 +2,27 @@ package main
 
 import (
 	"bytes"
+	"io"
 	"strings"
 	"testing"
 )
 
-func cobraTest(t *testing.T, args ...string) (string, error) {
+type cobraTestOpts struct {
+	stdin io.Reader
+}
+
+func cobraTest(t *testing.T, opts *cobraTestOpts, args ...string) (string, error) {
 	t.Helper()
 
 	buf := new(bytes.Buffer)
-	rootCmd.SetOut(buf)
-	rootCmd.SetErr(buf)
-	rootCmd.SetArgs(args)
+	rootTopCmd := NewRootCmd()
+	if opts != nil && opts.stdin != nil {
+		rootTopCmd.SetIn(opts.stdin)
+	}
+	rootTopCmd.SetOut(buf)
+	rootTopCmd.SetErr(buf)
+	rootTopCmd.SetArgs(args)
 
-	err := rootCmd.Execute()
+	err := rootTopCmd.Execute()
 	return strings.TrimSpace(buf.String()), err
 }
diff --git a/cmd/regctl/manifest_test.go b/cmd/regctl/manifest_test.go
index 3ee2340..ffaaa57 100644
--- a/cmd/regctl/manifest_test.go
+++ b/cmd/regctl/manifest_test.go
@@ -10,7 +10,6 @@ import (
 )
 
 func TestManifestHead(t *testing.T) {
-	saveManifestOpts := manifestOpts
 	tt := []struct {
 		name        string
 		args        []string
@@ -53,8 +52,7 @@ func TestManifestHead(t *testing.T) {
 	}
 	for _, tc := range tt {
 		t.Run(tc.name, func(t *testing.T) {
-			out, err := cobraTest(t, tc.args...)
-			manifestOpts = saveManifestOpts
+			out, err := cobraTest(t, nil, tc.args...)
 			if tc.expectErr != nil {
 				if err == nil {
 					t.Errorf("did not receive expected error: %v", tc.expectErr)
diff --git a/cmd/regctl/tag_test.go b/cmd/regctl/tag_test.go
index 64dc3d1..8d9d4c2 100644
--- a/cmd/regctl/tag_test.go
+++ b/cmd/regctl/tag_test.go
@@ -11,7 +11,6 @@ import (
 )
 
 func TestTagList(t *testing.T) {
-	saveTagOpts := tagOpts
 	tt := []struct {
 		name        string
 		args        []string
@@ -59,12 +58,9 @@ func TestTagList(t *testing.T) {
 			outContains: true,
 		},
 	}
-	optInit := tagOpts
 	for _, tc := range tt {
 		t.Run(tc.name, func(t *testing.T) {
-			tagOpts = optInit
-			out, err := cobraTest(t, tc.args...)
-			tagOpts = saveTagOpts
+			out, err := cobraTest(t, nil, tc.args...)
 			if tc.expectErr != nil {
 				if err == nil {
 					t.Errorf("did not receive expected error: %v", tc.expectErr)
@@ -82,5 +78,4 @@ func TestTagList(t *testing.T) {
 			}
 		})
 	}
-
 }
diff --git a/cmd/regsync/regsync_test.go b/cmd/regsync/regsync_test.go
index dd4905a..10f184c 100644
--- a/cmd/regsync/regsync_test.go
+++ b/cmd/regsync/regsync_test.go
@@ -632,8 +632,9 @@ func TestProcess(t *testing.T) {
 	for _, tc := range tt {
 		t.Run(tc.name, func(t *testing.T) {
 			// run each test
+			rootOpts := rootCmd{}
 			syncSetDefaults(&tc.sync, conf.Defaults)
-			err = tc.sync.process(ctx, tc.action)
+			err = rootOpts.process(ctx, tc.sync, tc.action)
 			// validate err
 			if tc.expErr != nil {
 				if err == nil {
@@ -732,6 +733,7 @@ func TestProcessRef(t *testing.T) {
 
 	for _, tc := range tt {
 		t.Run(tc.name, func(t *testing.T) {
+			rootOpts := rootCmd{}
 			src, err := ref.New(cs.Source)
 			if err != nil {
 				t.Errorf("failed to create src ref: %v", err)
@@ -744,7 +746,7 @@ func TestProcessRef(t *testing.T) {
 			}
 			src.Tag = tc.src
 			tgt.Tag = tc.tgt
-			err = cs.processRef(ctx, src, tgt, tc.action)
+			err = rootOpts.processRef(ctx, cs, src, tgt, tc.action)
 			// validate err
 			if tc.expErr != nil {
 				if err == nil {