Repository: docker/compose-cli
Pull request #689 (2020-09-28 15:09:03)

Add json output format to several commands
**What I did**
Add json output format to several commands

- [x] docker context ls - To make the `--json` hidden and implement `--format json`
- [x] docker ps
- [x] docker compose ls
- [x] docker compose ps
- [x] docker secret ls
- [x] docker volume ls
- [x] docker version - To clarify retro compatibility issues
- [x] Add tests

**Related issue**
Resolves https://github.com/docker/compose-cli/issues/607

**(not mandatory) A picture of a cute animal, if possible in relation with what you did**
![format](https://user-images.githubusercontent.com/373485/94085874-953a2d00-fe09-11ea-89bb-f6d9d9fe6370.jpg)



ulyssessouza: @gtardif @rumpl PTAL

Patch:
diff --git a/aci/containers.go b/aci/containers.go
index 570f6afd..93fb03eb 100644
--- a/aci/containers.go
+++ b/aci/containers.go
@@ -53,7 +53,7 @@ func (cs *aciContainerService) List(ctx context.Context, all bool) ([]containers
 	if err != nil {
 		return nil, err
 	}
-	var res []containers.Container
+	res := []containers.Container{}
 	for _, group := range containerGroups {
 		if group.Containers == nil || len(*group.Containers) == 0 {
 			return nil, fmt.Errorf("no containers found in ACI container group %s", *group.Name)
diff --git a/cli/cmd/compose/compose.go b/cli/cmd/compose/compose.go
index 8e821120..30248d25 100644
--- a/cli/cmd/compose/compose.go
+++ b/cli/cmd/compose/compose.go
@@ -33,6 +33,7 @@ type composeOptions struct {
 	WorkingDir  string
 	ConfigPaths []string
 	Environment []string
+	Format      string
 }
 
 func (o *composeOptions) toProjectName() (string, error) {
diff --git a/cli/cmd/compose/list.go b/cli/cmd/compose/list.go
index d44e05bb..806ee535 100644
--- a/cli/cmd/compose/list.go
+++ b/cli/cmd/compose/list.go
@@ -23,8 +23,11 @@ import (
 	"os"
 
 	"github.com/spf13/cobra"
+	"github.com/spf13/pflag"
 
 	"github.com/docker/compose-cli/api/client"
+	"github.com/docker/compose-cli/api/compose"
+	"github.com/docker/compose-cli/formatter"
 )
 
 func listCommand() *cobra.Command {
@@ -35,10 +38,15 @@ func listCommand() *cobra.Command {
 			return runList(cmd.Context(), opts)
 		},
 	}
-	lsCmd.Flags().StringVarP(&opts.Name, "project-name", "p", "", "Project name")
+	addComposeCommonFlags(lsCmd.Flags(), &opts)
 	return lsCmd
 }
 
+func addComposeCommonFlags(f *pflag.FlagSet, opts *composeOptions) {
+	f.StringVarP(&opts.Name, "project-name", "p", "", "Project name")
+	f.StringVar(&opts.Format, "format", "", "Format the output. Values: [pretty | json]. (Default: pretty)")
+}
+
 func runList(ctx context.Context, opts composeOptions) error {
 	c, err := client.New(ctx)
 	if err != nil {
@@ -49,10 +57,26 @@ func runList(ctx context.Context, opts composeOptions) error {
 		return err
 	}
 
-	err = printSection(os.Stdout, func(w io.Writer) {
-		for _, stack := range stackList {
-			fmt.Fprintf(w, "%s\t%s\n", stack.Name, stack.Status)
+	view := viewFromStackList(stackList)
+	return formatter.Print(view, opts.Format, os.Stdout, func(w io.Writer) {
+		for _, stack := range view {
+			_, _ = fmt.Fprintf(w, "%s\t%s\n", stack.Name, stack.Status)
 		}
 	}, "NAME", "STATUS")
-	return err
+}
+
+type stackView struct {
+	Name   string
+	Status string
+}
+
+func viewFromStackList(stackList []compose.Stack) []stackView {
+	retList := make([]stackView, len(stackList))
+	for i, s := range stackList {
+		retList[i] = stackView{
+			Name:   s.Name,
+			Status: s.Status,
+		}
+	}
+	return retList
 }
diff --git a/cli/cmd/compose/ps.go b/cli/cmd/compose/ps.go
index 8ad40055..13327f83 100644
--- a/cli/cmd/compose/ps.go
+++ b/cli/cmd/compose/ps.go
@@ -22,11 +22,12 @@ import (
 	"io"
 	"os"
 	"strings"
-	"text/tabwriter"
 
 	"github.com/spf13/cobra"
 
 	"github.com/docker/compose-cli/api/client"
+	"github.com/docker/compose-cli/api/compose"
+	"github.com/docker/compose-cli/formatter"
 )
 
 func psCommand() *cobra.Command {
@@ -37,10 +38,9 @@ func psCommand() *cobra.Command {
 			return runPs(cmd.Context(), opts)
 		},
 	}
-	psCmd.Flags().StringVarP(&opts.Name, "project-name", "p", "", "Project name")
 	psCmd.Flags().StringVar(&opts.WorkingDir, "workdir", "", "Work dir")
 	psCmd.Flags().StringArrayVarP(&opts.ConfigPaths, "file", "f", []string{}, "Compose configuration files")
-
+	addComposeCommonFlags(psCmd.Flags(), &opts)
 	return psCmd
 }
 
@@ -59,17 +59,34 @@ func runPs(ctx context.Context, opts composeOptions) error {
 		return err
 	}
 
-	err = printSection(os.Stdout, func(w io.Writer) {
-		for _, service := range serviceList {
-			fmt.Fprintf(w, "%s\t%s\t%d/%d\t%s\n", service.ID, service.Name, service.Replicas, service.Desired, strings.Join(service.Ports, ", "))
-		}
-	}, "ID", "NAME", "REPLICAS", "PORTS")
-	return err
+	view := viewFromServiceStatusList(serviceList)
+	return formatter.Print(view, opts.Format, os.Stdout,
+		func(w io.Writer) {
+			for _, service := range view {
+				_, _ = fmt.Fprintf(w, "%s\t%s\t%d/%d\t%s\n", service.ID, service.Name, service.Replicas, service.Desired, strings.Join(service.Ports, ", "))
+			}
+		},
+		"ID", "NAME", "REPLICAS", "PORTS")
 }
 
-func printSection(out io.Writer, printer func(io.Writer), headers ...string) error {
-	w := tabwriter.NewWriter(out, 20, 1, 3, ' ', 0)
-	fmt.Fprintln(w, strings.Join(headers, "\t"))
-	printer(w)
-	return w.Flush()
+type serviceStatusView struct {
+	ID       string
+	Name     string
+	Replicas int
+	Desired  int
+	Ports    []string
+}
+
+func viewFromServiceStatusList(serviceStatusList []compose.ServiceStatus) []serviceStatusView {
+	retList := make([]serviceStatusView, len(serviceStatusList))
+	for i, s := range serviceStatusList {
+		retList[i] = serviceStatusView{
+			ID:       s.ID,
+			Name:     s.Name,
+			Replicas: s.Replicas,
+			Desired:  s.Desired,
+			Ports:    s.Ports,
+		}
+	}
+	return retList
 }
diff --git a/cli/cmd/context/ls.go b/cli/cmd/context/ls.go
index 2cad5c7b..47ca3dea 100644
--- a/cli/cmd/context/ls.go
+++ b/cli/cmd/context/ls.go
@@ -17,13 +17,13 @@
 package context
 
 import (
-	"errors"
 	"fmt"
+	"io"
 	"os"
 	"sort"
 	"strings"
-	"text/tabwriter"
 
+	"github.com/pkg/errors"
 	"github.com/spf13/cobra"
 
 	"github.com/docker/compose-cli/cli/mobycli"
@@ -58,7 +58,8 @@ func listCommand() *cobra.Command {
 	}
 	cmd.Flags().BoolVarP(&opts.quiet, "quiet", "q", false, "Only show context names")
 	cmd.Flags().BoolVar(&opts.json, "json", false, "Format output as JSON")
-	cmd.Flags().StringVar(&opts.format, "format", "", "Format output as JSON")
+	cmd.Flags().StringVar(&opts.format, "format", "", "Format the output. Values: [pretty | json]. (Default: pretty)")
+	_ = cmd.Flags().MarkHidden("json")
 
 	return cmd
 }
@@ -68,7 +69,7 @@ func runList(cmd *cobra.Command, opts lsOpts) error {
 	if err != nil {
 		return err
 	}
-	if opts.format != "" {
+	if opts.format != "" && opts.format != formatter.JSON && opts.format != formatter.PRETTY {
 		mobycli.Exec(cmd.Root())
 		return nil
 	}
@@ -93,35 +94,27 @@ func runList(cmd *cobra.Command, opts lsOpts) error {
 	}
 
 	if opts.json {
-		j, err := formatter.ToStandardJSON(contexts)
-		if err != nil {
-			return err
-		}
-		fmt.Println(j)
-		return nil
-	}
-
-	w := tabwriter.NewWriter(os.Stdout, 20, 1, 3, ' ', 0)
-	fmt.Fprintln(w, "NAME\tTYPE\tDESCRIPTION\tDOCKER ENDPOINT\tKUBERNETES ENDPOINT\tORCHESTRATOR")
-	format := "%s\t%s\t%s\t%s\t%s\t%s\n"
-
-	for _, c := range contexts {
-		contextName := c.Name
-		if c.Name == currentContext {
-			contextName += " *"
-		}
-
-		fmt.Fprintf(w,
-			format,
-			contextName,
-			c.Type(),
-			c.Metadata.Description,
-			getEndpoint("docker", c.Endpoints),
-			getEndpoint("kubernetes", c.Endpoints),
-			c.Metadata.StackOrchestrator)
+		opts.format = formatter.JSON
 	}
 
-	return w.Flush()
+	view := viewFromContextList(contexts, currentContext)
+	return formatter.Print(view, opts.format, os.Stdout,
+		func(w io.Writer) {
+			for _, c := range view {
+				contextName := c.Name
+				if c.Current {
+					contextName += " *"
+				}
+				_, _ = fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\t%s\n",
+					contextName,
+					c.Type,
+					c.Description,
+					c.DockerEndpoint,
+					c.KubernetesEndpoint,
+					c.StackOrchestrator)
+			}
+		},
+		"NAME", "TYPE", "DESCRIPTION", "DOCKER ENDPOINT", "KUBERNETES ENDPOINT", "ORCHESTRATOR")
 }
 
 func getEndpoint(name string, meta map[string]interface{}) string {
@@ -141,3 +134,29 @@ func getEndpoint(name string, meta map[string]interface{}) string {
 
 	return result
 }
+
+type contextView struct {
+	Current            bool
+	Description        string
+	DockerEndpoint     string
+	KubernetesEndpoint string
+	Type               string
+	Name               string
+	StackOrchestrator  string
+}
+
+func viewFromContextList(contextList []*store.DockerContext, currentContext string) []contextView {
+	retList := make([]contextView, len(contextList))
+	for i, c := range contextList {
+		retList[i] = contextView{
+			Current:            c.Name == currentContext,
+			Description:        c.Metadata.Description,
+			DockerEndpoint:     getEndpoint("docker", c.Endpoints),
+			KubernetesEndpoint: getEndpoint("kubernetes", c.Endpoints),
+			Name:               c.Name,
+			Type:               c.Type(),
+			StackOrchestrator:  c.Metadata.StackOrchestrator,
+		}
+	}
+	return retList
+}
diff --git a/cli/cmd/inspect.go b/cli/cmd/inspect.go
index 20997d86..2e964799 100644
--- a/cli/cmd/inspect.go
+++ b/cli/cmd/inspect.go
@@ -56,7 +56,7 @@ func runInspect(ctx context.Context, id string) error {
 	if err != nil {
 		return err
 	}
-	fmt.Println(j)
+	fmt.Print(j)
 
 	return nil
 }
diff --git a/cli/cmd/ps.go b/cli/cmd/ps.go
index 815284de..56d6ab72 100644
--- a/cli/cmd/ps.go
+++ b/cli/cmd/ps.go
@@ -19,9 +19,9 @@ package cmd
 import (
 	"context"
 	"fmt"
+	"io"
 	"os"
 	"strings"
-	"text/tabwriter"
 
 	"github.com/pkg/errors"
 	"github.com/spf13/cobra"
@@ -33,16 +33,10 @@ import (
 )
 
 type psOpts struct {
-	all   bool
-	quiet bool
-	json  bool
-}
-
-func (o psOpts) validate() error {
-	if o.quiet && o.json {
-		return errors.New(`cannot combine "quiet" and "json" options`)
-	}
-	return nil
+	all    bool
+	quiet  bool
+	json   bool
+	format string
 }
 
 // PsCommand lists containers
@@ -59,50 +53,52 @@ func PsCommand() *cobra.Command {
 	cmd.Flags().BoolVarP(&opts.quiet, "quiet", "q", false, "Only display IDs")
 	cmd.Flags().BoolVarP(&opts.all, "all", "a", false, "Show all containers (default shows just running)")
 	cmd.Flags().BoolVar(&opts.json, "json", false, "Format output as JSON")
+	cmd.Flags().StringVar(&opts.format, "format", "", "Format the output. Values: [pretty | json]. (Default: pretty)")
+	_ = cmd.Flags().MarkHidden("json") // Legacy. This is used by VSCode Docker extension
 
 	return cmd
 }
 
+func (o psOpts) validate() error {
+	if o.quiet && o.json {
+		return errors.New(`cannot combine "quiet" and "json" options`)
+	}
+	return nil
+}
+
 func runPs(ctx context.Context, opts psOpts) error {
 	err := opts.validate()
 	if err != nil {
 		return err
 	}
-
 	c, err := client.New(ctx)
 	if err != nil {
 		return errors.Wrap(err, "cannot connect to backend")
 	}
 
-	containers, err := c.ContainerService().List(ctx, opts.all)
+	containerList, err := c.ContainerService().List(ctx, opts.all)
 	if err != nil {
 		return errors.Wrap(err, "fetch containers")
 	}
 
 	if opts.quiet {
-		for _, c := range containers {
+		for _, c := range containerList {
 			fmt.Println(c.ID)
 		}
 		return nil
 	}
 
 	if opts.json {
-		j, err := formatter2.ToStandardJSON(containers)
-		if err != nil {
-			return err
-		}
-		fmt.Println(j)
-		return nil
+		opts.format = formatter2.JSON
 	}
 
-	w := tabwriter.NewWriter(os.Stdout, 20, 1, 3, ' ', 0)
-	fmt.Fprintf(w, "CONTAINER ID\tIMAGE\tCOMMAND\tSTATUS\tPORTS\n")
-	format := "%s\t%s\t%s\t%s\t%s\n"
-	for _, container := range containers {
-		fmt.Fprintf(w, format, container.ID, container.Image, container.Command, container.Status, strings.Join(formatter.PortsToStrings(container.Ports, fqdn(container)), ", "))
-	}
-
-	return w.Flush()
+	view := viewFromContainerList(containerList)
+	return formatter2.Print(view, opts.format, os.Stdout, func(w io.Writer) {
+		for _, c := range view {
+			_, _ = fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\n", c.ID, c.Image, c.Command, c.Status,
+				strings.Join(c.Ports, ", "))
+		}
+	}, "CONTAINER ID", "IMAGE", "COMMAND", "STATUS", "PORTS")
 }
 
 func fqdn(container containers.Container) string {
@@ -112,3 +108,25 @@ func fqdn(container containers.Container) string {
 	}
 	return fqdn
 }
+
+type containerView struct {
+	ID      string
+	Image   string
+	Status  string
+	Command string
+	Ports   []string
+}
+
+func viewFromContainerList(containerList []containers.Container) []containerView {
+	retList := make([]containerView, len(containerList))
+	for i, c := range containerList {
+		retList[i] = containerView{
+			ID:      c.ID,
+			Image:   c.Image,
+			Status:  c.Status,
+			Command: c.Command,
+			Ports:   formatter.PortsToStrings(c.Ports, fqdn(c)),
+		}
+	}
+	return retList
+}
diff --git a/cli/cmd/secrets.go b/cli/cmd/secrets.go
index cce9141f..f372376b 100644
--- a/cli/cmd/secrets.go
+++ b/cli/cmd/secrets.go
@@ -20,13 +20,12 @@ import (
 	"fmt"
 	"io"
 	"os"
-	"strings"
-	"text/tabwriter"
 
 	"github.com/spf13/cobra"
 
 	"github.com/docker/compose-cli/api/client"
 	"github.com/docker/compose-cli/api/secrets"
+	"github.com/docker/compose-cli/formatter"
 )
 
 type createSecretOptions struct {
@@ -105,7 +104,12 @@ func inspectSecret() *cobra.Command {
 	return cmd
 }
 
+type listSecretsOpts struct {
+	format string
+}
+
 func listSecrets() *cobra.Command {
+	var opts listSecretsOpts
 	cmd := &cobra.Command{
 		Use:     "list",
 		Aliases: []string{"ls"},
@@ -115,17 +119,40 @@ func listSecrets() *cobra.Command {
 			if err != nil {
 				return err
 			}
-			list, err := c.SecretsService().ListSecrets(cmd.Context())
+			secretsList, err := c.SecretsService().ListSecrets(cmd.Context())
 			if err != nil {
 				return err
 			}
-			printList(os.Stdout, list)
-			return nil
+			view := viewFromSecretList(secretsList)
+			return formatter.Print(view, opts.format, os.Stdout, func(w io.Writer) {
+				for _, secret := range view {
+					_, _ = fmt.Fprintf(w, "%s\t%s\t%s\n", secret.ID, secret.Name, secret.Description)
+				}
+			}, "ID", "NAME", "DESCRIPTION")
 		},
 	}
+	cmd.Flags().StringVar(&opts.format, "format", "", "Format the output. Values: [pretty | json]. (Default: pretty)")
 	return cmd
 }
 
+type secretView struct {
+	ID          string
+	Name        string
+	Description string
+}
+
+func viewFromSecretList(secretList []secrets.Secret) []secretView {
+	retList := make([]secretView, len(secretList))
+	for i, s := range secretList {
+		retList[i] = secretView{
+			ID:          s.ID,
+			Name:        s.Name,
+			Description: s.Description,
+		}
+	}
+	return retList
+}
+
 type deleteSecretOptions struct {
 	recover bool
 }
@@ -148,18 +175,3 @@ func deleteSecret() *cobra.Command {
 	cmd.Flags().BoolVar(&opts.recover, "recover", false, "Enable recovery.")
 	return cmd
 }
-
-func printList(out io.Writer, secrets []secrets.Secret) {
-	printSection(out, func(w io.Writer) {
-		for _, secret := range secrets {
-			fmt.Fprintf(w, "%s\t%s\t%s\n", secret.ID, secret.Name, secret.Description) // nolint:errcheck
-		}
-	}, "ID", "NAME", "DESCRIPTION")
-}
-
-func printSection(out io.Writer, printer func(io.Writer), headers ...string) {
-	w := tabwriter.NewWriter(out, 20, 1, 3, ' ', 0)
-	fmt.Fprintln(w, strings.Join(headers, "\t")) // nolint:errcheck
-	printer(w)
-	w.Flush() // nolint:errcheck
-}
diff --git a/cli/cmd/version.go b/cli/cmd/version.go
index 70a7cd0a..3a50f179 100644
--- a/cli/cmd/version.go
+++ b/cli/cmd/version.go
@@ -18,43 +18,96 @@ package cmd
 
 import (
 	"fmt"
+	"os"
 	"strings"
 
 	"github.com/spf13/cobra"
 
 	"github.com/docker/compose-cli/cli/cmd/mobyflags"
 	"github.com/docker/compose-cli/cli/mobycli"
+	"github.com/docker/compose-cli/formatter"
 )
 
+const formatOpt = "format"
+
 // VersionCommand command to display version
 func VersionCommand(version string) *cobra.Command {
 	cmd := &cobra.Command{
 		Use:   "version",
 		Short: "Show the Docker version information",
 		Args:  cobra.MaximumNArgs(0),
-		RunE: func(cmd *cobra.Command, _ []string) error {
-			return runVersion(cmd, version)
+		Run: func(cmd *cobra.Command, _ []string) {
+			runVersion(cmd, version)
 		},
 	}
 	// define flags for backward compatibility with com.docker.cli
 	flags := cmd.Flags()
-	flags.StringP("format", "f", "", "Format the output using the given Go template")
+	flags.StringP(formatOpt, "f", "", "Format the output. Values: [pretty | json]. (Default: pretty)")
 	flags.String("kubeconfig", "", "Kubernetes config file")
 	mobyflags.AddMobyFlagsForRetrocompatibility(flags)
 
 	return cmd
 }
 
-func runVersion(cmd *cobra.Command, version string) error {
+func runVersion(cmd *cobra.Command, version string) {
+	var versionString string
+	format := strings.ToLower(strings.ReplaceAll(cmd.Flag(formatOpt).Value.String(), " ", ""))
 	displayedVersion := strings.TrimPrefix(version, "v")
-	versionResult, _ := mobycli.ExecSilent(cmd.Context())
+	// Replace is preferred in this case to keep the order.
+	switch format {
+	case formatter.PRETTY, "":
+		versionString = strings.Replace(getOutFromMoby(cmd, fixedPrettyArgs(os.Args[1:])...),
+			"\n Version:", "\n Cloud integration:  "+displayedVersion+"\n Version:", 1)
+	case formatter.JSON, "{{json.}}": // Try to catch full JSON formats
+		versionString = strings.Replace(getOutFromMoby(cmd, fixedJSONArgs(os.Args[1:])...),
+			`"Version":`, fmt.Sprintf(`"CloudIntegration":%q,"Version":`, displayedVersion), 1)
+	default:
+		versionString = getOutFromMoby(cmd)
+	}
+
+	fmt.Print(versionString)
+}
+
+func getOutFromMoby(cmd *cobra.Command, args ...string) string {
+	versionResult, _ := mobycli.ExecSilent(cmd.Context(), args...)
 	// we don't want to fail on error, there is an error if the engine is not available but it displays client version info
 	// Still, technically the [] byte versionResult could be nil, just let the original command display what it has to display
 	if versionResult == nil {
 		mobycli.Exec(cmd.Root())
-		return nil
+		return ""
 	}
-	var s string = string(versionResult)
-	fmt.Print(strings.Replace(s, "\n Version:", "\n Cloud integration  "+displayedVersion+"\n Version:", 1))
-	return nil
+	return string(versionResult)
+}
+
+func fixedPrettyArgs(oArgs []string) []string {
+	args := make([]string, 0)
+	for i := 0; i < len(oArgs); i++ {
+		if isFormatOpt(oArgs[i]) &&
+			len(oArgs) > i &&
+			(strings.ToLower(oArgs[i+1]) == formatter.PRETTY || oArgs[i+1] == "") {
+			i++
+			continue
+		}
+		args = append(args, oArgs[i])
+	}
+	return args
+}
+
+func fixedJSONArgs(oArgs []string) []string {
+	args := make([]string, 0)
+	for i := 0; i < len(oArgs); i++ {
+		if isFormatOpt(oArgs[i]) &&
+			len(oArgs) > i &&
+			strings.ToLower(oArgs[i+1]) == formatter.JSON {
+			args = append(args, oArgs[i], "{{json .}}")
+			i++
+			continue
+		}
+		args = append(args, oArgs[i])
+	}
+	return args
+}
+
+func isFormatOpt(o string) bool {
+	return o == "--format" || o == "-f"
 }
diff --git a/cli/cmd/volume/list.go b/cli/cmd/volume/list.go
index 9ab3313a..b279e912 100644
--- a/cli/cmd/volume/list.go
+++ b/cli/cmd/volume/list.go
@@ -20,16 +20,20 @@ import (
 	"fmt"
 	"io"
 	"os"
-	"strings"
-	"text/tabwriter"
 
 	"github.com/spf13/cobra"
 
 	"github.com/docker/compose-cli/api/client"
 	"github.com/docker/compose-cli/api/volumes"
+	"github.com/docker/compose-cli/formatter"
 )
 
+type listVolumeOpts struct {
+	format string
+}
+
 func listVolume() *cobra.Command {
+	var opts listVolumeOpts
 	cmd := &cobra.Command{
 		Use:   "ls",
 		Short: "list available volumes in context.",
@@ -43,24 +47,30 @@ func listVolume() *cobra.Command {
 			if err != nil {
 				return err
 			}
-			printList(os.Stdout, vols)
-			return nil
+			view := viewFromVolumeList(vols)
+			return formatter.Print(view, opts.format, os.Stdout, func(w io.Writer) {
+				for _, vol := range view {
+					_, _ = fmt.Fprintf(w, "%s\t%s\n", vol.ID, vol.Description)
+				}
+			}, "ID", "DESCRIPTION")
 		},
 	}
+	cmd.Flags().StringVar(&opts.format, "format", formatter.PRETTY, "Format the output. Values: [pretty | json]. (Default: pretty)")
 	return cmd
 }
 
-func printList(out io.Writer, volumes []volumes.Volume) {
-	printSection(out, func(w io.Writer) {
-		for _, vol := range volumes {
-			_, _ = fmt.Fprintf(w, "%s\t%s\n", vol.ID, vol.Description)
-		}
-	}, "ID", "DESCRIPTION")
+type volumeView struct {
+	ID          string
+	Description string
 }
 
-func printSection(out io.Writer, printer func(io.Writer), headers ...string) {
-	w := tabwriter.NewWriter(out, 20, 1, 3, ' ', 0)
-	_, _ = fmt.Fprintln(w, strings.Join(headers, "\t"))
-	printer(w)
-	_ = w.Flush()
+func viewFromVolumeList(volumeList []volumes.Volume) []volumeView {
+	retList := make([]volumeView, len(volumeList))
+	for i, v := range volumeList {
+		retList[i] = volumeView{
+			ID:          v.ID,
+			Description: v.Description,
+		}
+	}
+	return retList
 }
diff --git a/cli/mobycli/exec.go b/cli/mobycli/exec.go
index 665de45b..5b89cd1c 100644
--- a/cli/mobycli/exec.go
+++ b/cli/mobycli/exec.go
@@ -112,7 +112,10 @@ func IsDefaultContextCommand(dockerCommand string) bool {
 }
 
 // ExecSilent executes a command and do redirect output to stdOut, return output
-func ExecSilent(ctx context.Context) ([]byte, error) {
-	cmd := exec.CommandContext(ctx, ComDockerCli, os.Args[1:]...)
+func ExecSilent(ctx context.Context, args ...string) ([]byte, error) {
+	if len(args) == 0 {
+		args = os.Args[1:]
+	}
+	cmd := exec.CommandContext(ctx, ComDockerCli, args...)
 	return cmd.CombinedOutput()
 }
diff --git a/formatter/formatter.go b/formatter/formatter.go
new file mode 100644
index 00000000..52cd9d0d
--- /dev/null
+++ b/formatter/formatter.go
@@ -0,0 +1,58 @@
+/*
+   Copyright 2020 Docker Compose CLI authors
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
+*/
+
+package formatter
+
+import (
+	"fmt"
+	"io"
+	"reflect"
+	"strings"
+
+	"github.com/pkg/errors"
+
+	"github.com/docker/compose-cli/errdefs"
+)
+
+// Print prints formatted lists in different formats
+func Print(toJSON interface{}, format string, outWriter io.Writer, writerFn func(w io.Writer), headers ...string) error {
+	switch strings.ToLower(format) {
+	case PRETTY, "":
+		return PrintPrettySection(outWriter, writerFn, headers...)
+	case JSON:
+		switch reflect.TypeOf(toJSON).Kind() {
+		case reflect.Slice:
+			s := reflect.ValueOf(toJSON)
+			for i := 0; i < s.Len(); i++ {
+				obj := s.Index(i).Interface()
+				jsonLine, err := ToJSON(obj, "", "")
+				if err != nil {
+					return err
+				}
+				_, _ = fmt.Fprint(outWriter, jsonLine)
+			}
+		default:
+			outJSON, err := ToStandardJSON(toJSON)
+			if err != nil {
+				return err
+			}
+			_, _ = fmt.Fprintln(outWriter, outJSON)
+		}
+	default:
+		return errors.Wrapf(errdefs.ErrParsingFailed, "format value %q could not be parsed", format)
+	}
+	return nil
+}
diff --git a/formatter/json.go b/formatter/json.go
index bb9b6078..afadb0c8 100644
--- a/formatter/json.go
+++ b/formatter/json.go
@@ -16,15 +16,24 @@
 
 package formatter
 
-import "encoding/json"
+import (
+	"bytes"
+	"encoding/json"
+)
 
 const standardIndentation = "    "
 
 // ToStandardJSON return a string with the JSON representation of the interface{}
 func ToStandardJSON(i interface{}) (string, error) {
-	b, err := json.MarshalIndent(i, "", standardIndentation)
-	if err != nil {
-		return "", err
-	}
-	return string(b), nil
+	return ToJSON(i, "", standardIndentation)
+}
+
+// ToJSON return a string with the JSON representation of the interface{}
+func ToJSON(i interface{}, prefix string, indentation string) (string, error) {
+	buffer := &bytes.Buffer{}
+	encoder := json.NewEncoder(buffer)
+	encoder.SetEscapeHTML(false)
+	encoder.SetIndent(prefix, indentation)
+	err := encoder.Encode(i)
+	return buffer.String(), err
 }
diff --git a/go.mod b/go.mod
index f0cf60a4..aef8bdfa 100644
--- a/go.mod
+++ b/go.mod
@@ -62,5 +62,6 @@ require (
 	google.golang.org/protobuf v1.25.0
 	gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect
 	gopkg.in/ini.v1 v1.61.0
+	gotest.tools v2.2.0+incompatible
 	gotest.tools/v3 v3.0.2
 )
diff --git a/go.sum b/go.sum
index 378b6d0d..1239c69e 100644
--- a/go.sum
+++ b/go.sum
@@ -480,6 +480,7 @@ go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
 go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
 go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
 go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
+go.uber.org/zap v1.10.0 h1:ORx85nbTijNz8ljznvCMR1ZBIPKFn3jQrag10X2AsuM=
 go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
 golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
diff --git a/utils/formatter/container.go b/utils/formatter/container.go
index dc5c3fdb..5dcdf342 100644
--- a/utils/formatter/container.go
+++ b/utils/formatter/container.go
@@ -33,8 +33,8 @@ type portGroup struct {
 // PortsToStrings returns a human readable published ports
 func PortsToStrings(ports []containers.Port, fqdn string) []string {
 	groupMap := make(map[string]*portGroup)
+	result := []string{}
 	var (
-		result       []string
 		hostMappings []string
 		groupMapKeys []string
 	)


Test patch:
diff --git a/cli/cmd/version_test.go b/cli/cmd/version_test.go
new file mode 100644
index 00000000..bdcf2435
--- /dev/null
+++ b/cli/cmd/version_test.go
@@ -0,0 +1,190 @@
+/*
+   Copyright 2020 Docker Compose CLI authors
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
+*/
+
+package cmd
+
+import (
+	"testing"
+
+	"gotest.tools/assert"
+)
+
+type caze struct {
+	Actual   []string
+	Expected []string
+}
+
+func TestVersionFormat(t *testing.T) {
+	jsonCases := []caze{
+		{
+			Actual:   fixedJSONArgs([]string{}),
+			Expected: []string{},
+		},
+		{
+			Actual: fixedJSONArgs([]string{
+				"docker",
+				"version",
+			}),
+			Expected: []string{
+				"docker",
+				"version",
+			},
+		},
+		{
+			Actual: fixedJSONArgs([]string{
+				"docker",
+				"version",
+				"--format",
+				"json",
+			}),
+			Expected: []string{
+				"docker",
+				"version",
+				"--format",
+				"{{json .}}",
+			},
+		},
+		{
+			Actual: fixedJSONArgs([]string{
+				"docker",
+				"version",
+				"--format",
+				"jSoN",
+			}),
+			Expected: []string{
+				"docker",
+				"version",
+				"--format",
+				"{{json .}}",
+			},
+		},
+		{
+			Actual: fixedJSONArgs([]string{
+				"docker",
+				"version",
+				"--format",
+				"json",
+				"--kubeconfig",
+				"myKubeConfig",
+			}),
+			Expected: []string{
+				"docker",
+				"version",
+				"--format",
+				"{{json .}}",
+				"--kubeconfig",
+				"myKubeConfig",
+			},
+		},
+		{
+			Actual: fixedJSONArgs([]string{
+				"--format",
+				"json",
+			}),
+			Expected: []string{
+				"--format",
+				"{{json .}}",
+			},
+		},
+	}
+	prettyCases := []caze{
+		{
+			Actual:   fixedPrettyArgs([]string{}),
+			Expected: []string{},
+		},
+		{
+			Actual: fixedPrettyArgs([]string{
+				"docker",
+				"version",
+			}),
+			Expected: []string{
+				"docker",
+				"version",
+			},
+		},
+		{
+			Actual: fixedPrettyArgs([]string{
+				"docker",
+				"version",
+				"--format",
+				"pretty",
+			}),
+			Expected: []string{
+				"docker",
+				"version",
+			},
+		},
+		{
+			Actual: fixedPrettyArgs([]string{
+				"docker",
+				"version",
+				"--format",
+				"pRettY",
+			}),
+			Expected: []string{
+				"docker",
+				"version",
+			},
+		},
+		{
+			Actual: fixedPrettyArgs([]string{
+				"docker",
+				"version",
+				"--format",
+				"",
+			}),
+			Expected: []string{
+				"docker",
+				"version",
+			},
+		},
+		{
+			Actual: fixedPrettyArgs([]string{
+				"docker",
+				"version",
+				"--format",
+				"pretty",
+				"--kubeconfig",
+				"myKubeConfig",
+			}),
+			Expected: []string{
+				"docker",
+				"version",
+				"--kubeconfig",
+				"myKubeConfig",
+			},
+		},
+		{
+			Actual: fixedPrettyArgs([]string{
+				"--format",
+				"pretty",
+			}),
+			Expected: []string{},
+		},
+	}
+
+	t.Run("json", func(t *testing.T) {
+		for _, c := range jsonCases {
+			assert.DeepEqual(t, c.Actual, c.Expected)
+		}
+	})
+
+	t.Run("pretty", func(t *testing.T) {
+		for _, c := range prettyCases {
+			assert.DeepEqual(t, c.Actual, c.Expected)
+		}
+	})
+}
diff --git a/cli/cmd/volume/testdata/volumes-out.golden b/cli/cmd/volume/testdata/volumes-out.golden
deleted file mode 100644
index 61c99679..00000000
--- a/cli/cmd/volume/testdata/volumes-out.golden
+++ /dev/null
@@ -1,2 +0,0 @@
-ID                  DESCRIPTION
-volume/123          volume 123
diff --git a/cli/cmd/secrets_test.go b/formatter/consts.go
similarity index 61%
rename from cli/cmd/secrets_test.go
rename to formatter/consts.go
index 9624c0c5..1ca9ebcd 100644
--- a/cli/cmd/secrets_test.go
+++ b/formatter/consts.go
@@ -14,26 +14,11 @@
    limitations under the License.
 */
 
-package cmd
+package formatter
 
-import (
-	"bytes"
-	"testing"
-
-	"gotest.tools/v3/golden"
-
-	"github.com/docker/compose-cli/api/secrets"
+const (
+	// JSON is the constant for Json formats on list commands
+	JSON = "json"
+	// PRETTY is the constant for default formats on list commands
+	PRETTY = "pretty"
 )
-
-func TestPrintList(t *testing.T) {
-	secrets := []secrets.Secret{
-		{
-			ID:          "123",
-			Name:        "secret123",
-			Description: "secret 1,2,3",
-		},
-	}
-	out := &bytes.Buffer{}
-	printList(out, secrets)
-	golden.Assert(t, out.String(), "secrets-out.golden")
-}
diff --git a/formatter/formatter_test.go b/formatter/formatter_test.go
new file mode 100644
index 00000000..6657a015
--- /dev/null
+++ b/formatter/formatter_test.go
@@ -0,0 +1,63 @@
+/*
+   Copyright 2020 Docker Compose CLI authors
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
+*/
+
+package formatter
+
+import (
+	"bytes"
+	"fmt"
+	"io"
+	"testing"
+
+	"gotest.tools/assert"
+)
+
+type testStruct struct {
+	Name   string
+	Status string
+}
+
+// Print prints formatted lists in different formats
+func TestPrint(t *testing.T) {
+	testList := []testStruct{
+		{
+			Name:   "myName1",
+			Status: "myStatus1",
+		},
+		{
+			Name:   "myName2",
+			Status: "myStatus2",
+		},
+	}
+
+	b := &bytes.Buffer{}
+	assert.NilError(t, Print(testList, PRETTY, b, func(w io.Writer) {
+		for _, t := range testList {
+			_, _ = fmt.Fprintf(w, "%s\t%s\n", t.Name, t.Status)
+		}
+	}, "NAME", "STATUS"))
+	assert.Equal(t, b.String(), "NAME                STATUS\nmyName1             myStatus1\nmyName2             myStatus2\n")
+
+	b.Reset()
+	assert.NilError(t, Print(testList, JSON, b, func(w io.Writer) {
+		for _, t := range testList {
+			_, _ = fmt.Fprintf(w, "%s\t%s\n", t.Name, t.Status)
+		}
+	}, "NAME", "STATUS"))
+	assert.Equal(t, b.String(), `{"Name":"myName1","Status":"myStatus1"}
+{"Name":"myName2","Status":"myStatus2"}
+`)
+}
diff --git a/cli/cmd/volume/list_test.go b/formatter/pretty.go
similarity index 63%
rename from cli/cmd/volume/list_test.go
rename to formatter/pretty.go
index f88716e9..bb85dede 100644
--- a/cli/cmd/volume/list_test.go
+++ b/formatter/pretty.go
@@ -14,25 +14,19 @@
    limitations under the License.
 */
 
-package volume
+package formatter
 
 import (
-	"bytes"
-	"testing"
-
-	"gotest.tools/v3/golden"
-
-	"github.com/docker/compose-cli/api/volumes"
+	"fmt"
+	"io"
+	"strings"
+	"text/tabwriter"
 )
 
-func TestPrintList(t *testing.T) {
-	secrets := []volumes.Volume{
-		{
-			ID:          "volume/123",
-			Description: "volume 123",
-		},
-	}
-	out := &bytes.Buffer{}
-	printList(out, secrets)
-	golden.Assert(t, out.String(), "volumes-out.golden")
+// PrintPrettySection prints a tabbed section on the writer parameter
+func PrintPrettySection(out io.Writer, printer func(writer io.Writer), headers ...string) error {
+	w := tabwriter.NewWriter(out, 20, 1, 3, ' ', 0)
+	_, _ = fmt.Fprintln(w, strings.Join(headers, "\t"))
+	printer(w)
+	return w.Flush()
 }
diff --git a/tests/e2e/e2e_test.go b/tests/e2e/e2e_test.go
index 4b33c8e8..8211a5f8 100644
--- a/tests/e2e/e2e_test.go
+++ b/tests/e2e/e2e_test.go
@@ -75,6 +75,12 @@ func TestContextDefault(t *testing.T) {
 	t.Run("ls", func(t *testing.T) {
 		res := c.RunDockerCmd("context", "ls")
 		golden.Assert(t, res.Stdout(), GoldenFile("ls-out-default"))
+
+		res = c.RunDockerCmd("context", "ls", "--format", "pretty")
+		golden.Assert(t, res.Stdout(), GoldenFile("ls-out-default"))
+
+		res = c.RunDockerCmd("context", "ls", "--format", "json")
+		golden.Assert(t, res.Stdout(), GoldenFile("ls-out-json"))
 	})
 
 	t.Run("inspect", func(t *testing.T) {
@@ -407,6 +413,26 @@ func TestVersion(t *testing.T) {
 		res.Assert(t, icmd.Expected{Out: `"Client":`})
 	})
 
+	t.Run("format cloud integration", func(t *testing.T) {
+		res := c.RunDockerCmd("version", "-f", "pretty")
+		res.Assert(t, icmd.Expected{Out: `Cloud integration:`})
+		res = c.RunDockerCmd("version", "-f", "")
+		res.Assert(t, icmd.Expected{Out: `Cloud integration:`})
+
+		res = c.RunDockerCmd("version", "-f", "json")
+		res.Assert(t, icmd.Expected{Out: `"CloudIntegration":`})
+		res = c.RunDockerCmd("version", "-f", "{{ json . }}")
+		res.Assert(t, icmd.Expected{Out: `"CloudIntegration":`})
+		res = c.RunDockerCmd("version", "--format", "{{json .}}")
+		res.Assert(t, icmd.Expected{Out: `"CloudIntegration":`})
+		res = c.RunDockerCmd("version", "--format", "{{json . }}")
+		res.Assert(t, icmd.Expected{Out: `"CloudIntegration":`})
+		res = c.RunDockerCmd("version", "--format", "{{ json .}}")
+		res.Assert(t, icmd.Expected{Out: `"CloudIntegration":`})
+		res = c.RunDockerCmd("version", "--format", "{{ json . }}")
+		res.Assert(t, icmd.Expected{Out: `"CloudIntegration":`})
+	})
+
 	t.Run("delegate version flag", func(t *testing.T) {
 		c.RunDockerCmd("context", "create", "example", "test-example")
 		c.RunDockerCmd("context", "use", "test-example")
@@ -431,6 +457,12 @@ func TestMockBackend(t *testing.T) {
 	t.Run("ps", func(t *testing.T) {
 		res := c.RunDockerCmd("ps")
 		golden.Assert(t, res.Stdout(), "ps-out-example.golden")
+
+		res = c.RunDockerCmd("ps", "--format", "pretty")
+		golden.Assert(t, res.Stdout(), "ps-out-example.golden")
+
+		res = c.RunDockerCmd("ps", "--format", "json")
+		golden.Assert(t, res.Stdout(), "ps-out-example-json.golden")
 	})
 
 	t.Run("ps quiet", func(t *testing.T) {
diff --git a/tests/e2e/testdata/ls-out-json.golden b/tests/e2e/testdata/ls-out-json.golden
new file mode 100644
index 00000000..34d53cb0
--- /dev/null
+++ b/tests/e2e/testdata/ls-out-json.golden
@@ -0,0 +1,1 @@
+{"Current":true,"Description":"Current DOCKER_HOST based configuration","DockerEndpoint":"unix:///var/run/docker.sock","KubernetesEndpoint":"","Type":"moby","Name":"default","StackOrchestrator":"swarm"}
diff --git a/tests/e2e/testdata/ps-out-example-json.golden b/tests/e2e/testdata/ps-out-example-json.golden
new file mode 100644
index 00000000..2f7ddef9
--- /dev/null
+++ b/tests/e2e/testdata/ps-out-example-json.golden
@@ -0,0 +1,2 @@
+{"ID":"id","Image":"nginx","Status":"","Command":"","Ports":[]}
+{"ID":"1234","Image":"alpine","Status":"","Command":"","Ports":[]}