cmd/go: allow users to specify required fields in JSON output

For #29666

Change-Id: Ibae3d75bb2c19571c8d473cb47d6c4b3a880bba8
Reviewed-on: https://go-review.googlesource.com/c/go/+/381035
Trust: Michael Matloob <matloob@golang.org>
Run-TryBot: Michael Matloob <matloob@golang.org>
Reviewed-by: Bryan Mills <bcmills@google.com>
TryBot-Result: Gopher Robot <gobot@golang.org>
This commit is contained in:
Michael Matloob 2022-01-26 15:31:20 -05:00
parent 6c6a8fb702
commit ec4687f337
3 changed files with 137 additions and 18 deletions

View File

@ -869,7 +869,10 @@
// for the go/build package's Context type.
//
// The -json flag causes the package data to be printed in JSON format
// instead of using the template format.
// instead of using the template format. The JSON flag can optionally be
// provided with a set of comma-separated required field names to be output.
// If so, those required fields will always appear in JSON output, but
// others may be omitted to save work in computing the JSON struct.
//
// The -compiled flag causes list to set CompiledGoFiles to the Go source
// files presented to the compiler. Typically this means that it repeats

View File

@ -13,7 +13,9 @@ import (
"fmt"
"io"
"os"
"reflect"
"sort"
"strconv"
"strings"
"text/template"
@ -157,7 +159,10 @@ For more information about the meaning of these fields see the documentation
for the go/build package's Context type.
The -json flag causes the package data to be printed in JSON format
instead of using the template format.
instead of using the template format. The JSON flag can optionally be
provided with a set of comma-separated required field names to be output.
If so, those required fields will always appear in JSON output, but
others may be omitted to save work in computing the JSON struct.
The -compiled flag causes list to set CompiledGoFiles to the Go source
files presented to the compiler. Typically this means that it repeats
@ -316,29 +321,79 @@ For more about modules, see https://golang.org/ref/mod.
func init() {
CmdList.Run = runList // break init cycle
work.AddBuildFlags(CmdList, work.DefaultBuildFlags)
CmdList.Flag.Var(&listJsonFields, "json", "")
}
var (
listCompiled = CmdList.Flag.Bool("compiled", false, "")
listDeps = CmdList.Flag.Bool("deps", false, "")
listE = CmdList.Flag.Bool("e", false, "")
listExport = CmdList.Flag.Bool("export", false, "")
listFmt = CmdList.Flag.String("f", "", "")
listFind = CmdList.Flag.Bool("find", false, "")
listJson = CmdList.Flag.Bool("json", false, "")
listM = CmdList.Flag.Bool("m", false, "")
listRetracted = CmdList.Flag.Bool("retracted", false, "")
listTest = CmdList.Flag.Bool("test", false, "")
listU = CmdList.Flag.Bool("u", false, "")
listVersions = CmdList.Flag.Bool("versions", false, "")
listCompiled = CmdList.Flag.Bool("compiled", false, "")
listDeps = CmdList.Flag.Bool("deps", false, "")
listE = CmdList.Flag.Bool("e", false, "")
listExport = CmdList.Flag.Bool("export", false, "")
listFmt = CmdList.Flag.String("f", "", "")
listFind = CmdList.Flag.Bool("find", false, "")
listJson bool
listJsonFields jsonFlag // If not empty, only output these fields.
listM = CmdList.Flag.Bool("m", false, "")
listRetracted = CmdList.Flag.Bool("retracted", false, "")
listTest = CmdList.Flag.Bool("test", false, "")
listU = CmdList.Flag.Bool("u", false, "")
listVersions = CmdList.Flag.Bool("versions", false, "")
)
// A StringsFlag is a command-line flag that interprets its argument
// as a space-separated list of possibly-quoted strings.
type jsonFlag map[string]bool
func (v *jsonFlag) Set(s string) error {
if v, err := strconv.ParseBool(s); err == nil {
listJson = v
return nil
}
listJson = true
if *v == nil {
*v = make(map[string]bool)
}
for _, f := range strings.Split(s, ",") {
(*v)[f] = true
}
return nil
}
func (v *jsonFlag) String() string {
var fields []string
for f := range *v {
fields = append(fields, f)
}
sort.Strings(fields)
return strings.Join(fields, ",")
}
func (v *jsonFlag) IsBoolFlag() bool {
return true
}
func (v *jsonFlag) needAll() bool {
return len(*v) == 0
}
func (v *jsonFlag) needAny(fields ...string) bool {
if v.needAll() {
return true
}
for _, f := range fields {
if (*v)[f] {
return true
}
}
return false
}
var nl = []byte{'\n'}
func runList(ctx context.Context, cmd *base.Command, args []string) {
modload.InitWorkfile()
if *listFmt != "" && *listJson == true {
if *listFmt != "" && listJson == true {
base.Fatalf("go list -f cannot be used with -json")
}
@ -357,9 +412,18 @@ func runList(ctx context.Context, cmd *base.Command, args []string) {
}
}
var do func(any)
if *listJson {
var do func(x any)
if listJson {
do = func(x any) {
if !listJsonFields.needAll() {
v := reflect.ValueOf(x).Elem() // do is always called with a non-nil pointer.
// Clear all non-requested fields.
for i := 0; i < v.NumField(); i++ {
if !listJsonFields.needAny(v.Type().Field(i).Name) {
v.Field(i).Set(reflect.Zero(v.Type().Field(i).Type))
}
}
}
b, err := json.MarshalIndent(x, "", "\t")
if err != nil {
out.Flush()
@ -589,7 +653,7 @@ func runList(ctx context.Context, cmd *base.Command, args []string) {
}
// Do we need to run a build to gather information?
needStale := *listJson || strings.Contains(*listFmt, ".Stale")
needStale := (listJson && listJsonFields.needAny("Stale", "StaleReason")) || strings.Contains(*listFmt, ".Stale")
if needStale || *listExport || *listCompiled {
var b work.Builder
b.Init()

View File

@ -0,0 +1,52 @@
# Test using -json flag to specify specific fields.
# Test -json produces "full" output by looking for multiple fields present.
go list -json .
stdout '"Name": "a"'
stdout '"Stale": true'
# Same thing for -json=true
go list -json=true .
stdout '"Name": "a"'
stdout '"Stale": true'
# Test -json=false produces non-json output.
go list -json=false
cmp stdout want-non-json.txt
# Test -json=<field> keeps only that field.
go list -json=Name
cmp stdout want-json-name.txt
# Test -json=<field> with multiple fields.
go list -json=ImportPath,Name,GoFiles,Imports
cmp stdout want-json-multiple.txt
-- go.mod --
module example.com/a
go 1.18
-- a.go --
package a
import "fmt"
func F() {
fmt.Println("hey there")
}
-- want-non-json.txt --
example.com/a
-- want-json-name.txt --
{
"Name": "a"
}
-- want-json-multiple.txt --
{
"ImportPath": "example.com/a",
"Name": "a",
"GoFiles": [
"a.go"
],
"Imports": [
"fmt"
]
}