diff --git a/cmd/oras/internal/option/format.go b/cmd/oras/internal/option/format.go new file mode 100644 index 000000000..4d82084b9 --- /dev/null +++ b/cmd/oras/internal/option/format.go @@ -0,0 +1,53 @@ +/* +Copyright The ORAS 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 option + +import ( + "encoding/json" + "fmt" + + "github.com/spf13/pflag" + "oras.land/oras/cmd/oras/internal/output/format" +) + +// Format option struct. +type Format struct { + format.Flag +} + +func (opts *Format) ApplyFlags(fs *pflag.FlagSet) { + fs.Var(&opts.Flag, "format-stdout", fmt.Sprintf("[Preview] summary output to stdout in the specified format (default %q)", format.Plain)) +} + +// Print prints the Output as JSON. +func (opts *Format) Print(data interface{}, prettify bool) error { + var err error + switch opts.Flag { + case format.Json: + var content []byte + if prettify { + content, err = json.MarshalIndent(data, "", " ") + } else { + content, err = json.Marshal(data) + } + if err != nil { + return err + } + _, err = fmt.Println(string(content)) + return err + + } + return nil +} diff --git a/cmd/oras/internal/output/display/print.go b/cmd/oras/internal/output/display/print.go index 470410fc9..1f89dc46e 100644 --- a/cmd/oras/internal/output/display/print.go +++ b/cmd/oras/internal/output/display/print.go @@ -19,6 +19,7 @@ import ( "context" "fmt" "io" + "os" "sync" ocispec "github.com/opencontainers/image-spec/specs-go/v1" @@ -33,10 +34,31 @@ var printLock sync.Mutex type PrintFunc func(ocispec.Descriptor) error // Print objects to display concurrent-safely. -func Print(a ...any) error { +var Print func(a ...any) error + +func init() { + Print = printStdout +} + +// SwitchToStderr switches Print to stderr. +func SwitchToStderr() { + Print = printStderr +} + +// printStdout displays objects concurrent-safely. +func printStdout(a ...any) error { + return printTo(os.Stderr, a...) +} + +// printStderr displays objects concurrent-safely. +func printStderr(a ...any) error { + return printTo(os.Stderr, a...) +} + +func printTo(w io.Writer, a ...any) error { printLock.Lock() defer printLock.Unlock() - _, err := fmt.Println(a...) + _, err := fmt.Fprintln(w, a...) return err } diff --git a/cmd/oras/internal/output/format/format.go b/cmd/oras/internal/output/format/format.go new file mode 100644 index 000000000..afdf6728d --- /dev/null +++ b/cmd/oras/internal/output/format/format.go @@ -0,0 +1,58 @@ +/* +Copyright The ORAS 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 format + +import ( + "errors" + "strings" +) + +const ( + Plain = "plain" + Json = "json" +) + +// Printer is the interface that wraps the basic String method. +type Printer interface { + Print(prettify bool) error +} + +// Define a custom value type that implements the pflag.Value interface. +type Flag string + +// Set must have pointer receiver so it doesn't change the value of a copy. +func (f *Flag) Set(v string) error { + switch v { + case "": + // default + *f = Plain + case Plain, Json: + *f = Json + default: + return errors.New("invalid format flag, expecting " + f.Type()) + } + return nil +} + +// String is used by pflag to print the default value of a flag. +func (f *Flag) String() string { + return string(*f) +} + +// Type provides optional value used in help text. +func (c *Flag) Type() string { + return strings.Join([]string{Plain, Json}, "|") +} diff --git a/cmd/oras/internal/output/format/json.go b/cmd/oras/internal/output/format/json.go new file mode 100644 index 000000000..5cd1d4289 --- /dev/null +++ b/cmd/oras/internal/output/format/json.go @@ -0,0 +1,47 @@ +/* +Copyright The ORAS 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 format + +import ( + "encoding/json" + "os" +) + +// JSON is a Output that prints the data as JSON. +type JSON struct { + Data interface{} +} + +// NewJSON creates a new Output with the given data +func NewJSON(data interface{}) *JSON { + return &JSON{Data: data} +} + +// Print prints the Output as JSON. +func Print(data interface{}, prettify bool) error { + var content []byte + var err error + if prettify { + content, err = json.MarshalIndent(data, "", " ") + } else { + content, err = json.Marshal(data) + } + if err != nil { + return err + } + _, err = os.Stdout.Write(content) + return err +} diff --git a/cmd/oras/internal/output/pipe/output.go b/cmd/oras/internal/output/pipe/output.go deleted file mode 100644 index 31a7cc135..000000000 --- a/cmd/oras/internal/output/pipe/output.go +++ /dev/null @@ -1,31 +0,0 @@ -package pipe - -import ( - "encoding/json" - "fmt" -) - -// Output is a generic type that can hold any data -type Output struct { - Data interface{} `json:"data"` -} - -// NewOutput creates a new Output with the given data -func NewOutput(data interface{}) *Output { - return &Output{Data: data} -} - -// Print prints the Output as JSON to the standard output -func (o *Output) String() { - // You can use json.MarshalIndent to pretty-print the JSON output - // The second argument is the prefix for each line - // The third argument is the indentation for each level - b, err := json.MarshalIndent(o, "", " ") - if err != nil { - fmt.Println("Error:", err) - return - } - // You can use fmt.Printf or os.Stdout.Write to print the JSON bytes - fmt.Printf("%s\n", b) - // os.Stdout.Write(b) -} diff --git a/cmd/oras/root/attach.go b/cmd/oras/root/attach.go index 746872d74..f4b44098a 100644 --- a/cmd/oras/root/attach.go +++ b/cmd/oras/root/attach.go @@ -28,6 +28,7 @@ import ( "oras.land/oras-go/v2/content/file" "oras.land/oras-go/v2/registry/remote/auth" "oras.land/oras/cmd/oras/internal/option" + "oras.land/oras/cmd/oras/internal/output/display" "oras.land/oras/cmd/oras/internal/output/display/track" "oras.land/oras/internal/graph" "oras.land/oras/internal/registryutil" @@ -37,6 +38,7 @@ type attachOptions struct { option.Common option.Packer option.Target + option.Format artifactType string concurrency int @@ -83,6 +85,7 @@ Example - Attach file to the manifest tagged 'v1' in an OCI image layout folder return nil }, RunE: func(cmd *cobra.Command, args []string) error { + display.SwitchToStderr() return runAttach(cmd.Context(), opts) }, } @@ -177,9 +180,13 @@ func runAttach(ctx context.Context, opts attachOptions) error { if !strings.HasSuffix(opts.RawReference, digest) { opts.RawReference = fmt.Sprintf("%s@%s", opts.Path, subject.Digest) } - fmt.Println("Attached to", opts.AnnotatedReference()) - fmt.Println("Digest:", root.Digest) + display.Print("Attached to", opts.AnnotatedReference()) + display.Print("Digest:", root.Digest) // Export manifest - return opts.ExportManifest(ctx, store, root) + if err = opts.ExportManifest(ctx, store, root); err != nil { + return err + } + // TODO: schema, PRD needed + return opts.Print(root, opts.TTY != nil) } diff --git a/cmd/oras/root/file.go b/cmd/oras/root/file.go index cf52423b5..6e3e037e8 100644 --- a/cmd/oras/root/file.go +++ b/cmd/oras/root/file.go @@ -23,6 +23,7 @@ import ( ocispec "github.com/opencontainers/image-spec/specs-go/v1" "oras.land/oras-go/v2/content/file" "oras.land/oras/cmd/oras/internal/fileref" + "oras.land/oras/cmd/oras/internal/output/display" ) func loadFiles(ctx context.Context, store *file.Store, annotations map[string]map[string]string, fileRefs []string, verbose bool) ([]ocispec.Descriptor, error) { @@ -58,7 +59,7 @@ func loadFiles(ctx context.Context, store *file.Store, annotations map[string]ma files = append(files, file) } if len(files) == 0 { - fmt.Println("Uploading empty artifact") + display.Print("Uploading empty artifact") } return files, nil } diff --git a/cmd/oras/root/push.go b/cmd/oras/root/push.go index c60fe9ace..e00bd1adc 100644 --- a/cmd/oras/root/push.go +++ b/cmd/oras/root/push.go @@ -18,7 +18,6 @@ package root import ( "context" "errors" - "fmt" "os" "strings" "sync" @@ -43,6 +42,7 @@ type pushOptions struct { option.Packer option.ImageSpec option.Target + option.Format extraRefs []string manifestConfigRef string @@ -122,6 +122,7 @@ Example - Push file "hi.txt" into an OCI image layout folder 'layout-dir' with t return nil }, RunE: func(cmd *cobra.Command, args []string) error { + display.SwitchToStderr() return runPush(cmd.Context(), opts) }, } @@ -211,7 +212,7 @@ func runPush(ctx context.Context, opts pushOptions) error { if err != nil { return err } - fmt.Println("Pushed", opts.AnnotatedReference()) + display.Print("Pushed", opts.AnnotatedReference()) if len(opts.extraRefs) != 0 { contentBytes, err := content.FetchAll(ctx, memoryStore, root) @@ -225,10 +226,13 @@ func runPush(ctx context.Context, opts pushOptions) error { } } - fmt.Println("Digest:", root.Digest) + display.Print("Digest:", root.Digest) // Export manifest - return opts.ExportManifest(ctx, memoryStore, root) + if err = opts.ExportManifest(ctx, memoryStore, root); err != nil { + return nil + } + return opts.Print(root, opts.TTY != nil) } func doPush(dst oras.Target, pack packFunc, copy copyFunc) (ocispec.Descriptor, error) {