Skip to content

[breaking] cli: format json always start with a json object #2407

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

51 changes: 51 additions & 0 deletions docs/UPGRADING.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,57 @@ Here you can find a list of migration guides to handle breaking changes between

## 0.36.0

### CLI changed JSON output for some `lib`, `core`, `config`, `board`, and `sketch` commands.

- `arduino-cli lib list --format json` results are now wrapped under `installed_libraries` key

```
{ "installed_libraries": [ {...}, {...} ] }
```

- `arduino-cli lib examples --format json` results are now wrapped under `examples` key

```
{ "examples": [ {...}, {...} ] }
```

- `arduino-cli core search --format json` and `arduino-cli core list --format json` results are now wrapped under
`platforms` key

```
{ "platforms": [ {...}, {...} ] }
```

- `arduino-cli config init --format json` now correctly returns a json object containg the config path

```
{ "config_path": "/home/user/.arduino15/arduino-cli.yaml" }
```

- `arduino-cli config dump --format json` results are now wrapped under `config` key

```
{ "config": { ... } }
```

- `arduino-cli board search --format json` results are now wrapped under `boards` key

```
{ "boards": [ {...}, {...} ] }
```

- `arduino-cli board list --format json` results are now wrapped under `detected_ports` key

```
{ "detected_ports": [ {...}, {...} ] }
```

- `arduino-cli sketch new` now correctly returns a json object containing the sketch path

```
{ "sketch_path": "/tmp/my_sketch" }
```

### The gRPC `cc.arduino.cli.commands.v1.PlatformRelease` has been changed.

We've added a new field called `compatible`. This field indicates if the current platform release is installable or not.
Expand Down
12 changes: 6 additions & 6 deletions internal/cli/board/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -114,27 +114,27 @@ func watchList(inst *rpc.Instance) {
// output from this command requires special formatting, let's create a dedicated
// feedback.Result implementation
type listResult struct {
ports []*result.DetectedPort
Ports []*result.DetectedPort `json:"detected_ports"`
}

func (dr listResult) Data() interface{} {
return dr.ports
return dr
}

func (dr listResult) String() string {
if len(dr.ports) == 0 {
if len(dr.Ports) == 0 {
return tr("No boards found.")
}

sort.Slice(dr.ports, func(i, j int) bool {
x, y := dr.ports[i].Port, dr.ports[j].Port
sort.Slice(dr.Ports, func(i, j int) bool {
x, y := dr.Ports[i].Port, dr.Ports[j].Port
return x.Protocol < y.Protocol ||
(x.Protocol == y.Protocol && x.Address < y.Address)
})

t := table.New()
t.SetHeader(tr("Port"), tr("Protocol"), tr("Type"), tr("Board Name"), tr("FQBN"), tr("Core"))
for _, detectedPort := range dr.ports {
for _, detectedPort := range dr.Ports {
port := detectedPort.Port
protocol := port.Protocol
address := port.Address
Expand Down
6 changes: 3 additions & 3 deletions internal/cli/board/listall.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import (

"github.com/arduino/arduino-cli/commands/board"
"github.com/arduino/arduino-cli/internal/cli/feedback"
fResult "github.com/arduino/arduino-cli/internal/cli/feedback/result"
"github.com/arduino/arduino-cli/internal/cli/feedback/result"
"github.com/arduino/arduino-cli/internal/cli/instance"
rpc "github.com/arduino/arduino-cli/rpc/cc/arduino/cli/commands/v1"
"github.com/arduino/arduino-cli/table"
Expand Down Expand Up @@ -64,13 +64,13 @@ func runListAllCommand(cmd *cobra.Command, args []string) {
feedback.Fatal(tr("Error listing boards: %v", err), feedback.ErrGeneric)
}

feedback.PrintResult(resultAll{fResult.NewBoardListAllResponse(list)})
feedback.PrintResult(resultAll{result.NewBoardListAllResponse(list)})
}

// output from this command requires special formatting, let's create a dedicated
// feedback.Result implementation
type resultAll struct {
list *fResult.BoardListAllResponse
list *result.BoardListAllResponse
}

func (dr resultAll) Data() interface{} {
Expand Down
16 changes: 8 additions & 8 deletions internal/cli/board/search.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import (

"github.com/arduino/arduino-cli/commands/board"
"github.com/arduino/arduino-cli/internal/cli/feedback"
fResult "github.com/arduino/arduino-cli/internal/cli/feedback/result"
"github.com/arduino/arduino-cli/internal/cli/feedback/result"
"github.com/arduino/arduino-cli/internal/cli/instance"
rpc "github.com/arduino/arduino-cli/rpc/cc/arduino/cli/commands/v1"
"github.com/arduino/arduino-cli/table"
Expand Down Expand Up @@ -61,32 +61,32 @@ func runSearchCommand(cmd *cobra.Command, args []string) {
feedback.Fatal(tr("Error searching boards: %v", err), feedback.ErrGeneric)
}

feedback.PrintResult(searchResults{fResult.NewBoardListItems(res.Boards)})
feedback.PrintResult(searchResults{result.NewBoardListItems(res.Boards)})
}

// output from this command requires special formatting so we create a dedicated
// feedback.Result implementation
type searchResults struct {
boards []*fResult.BoardListItem
Boards []*result.BoardListItem `json:"boards"`
}

func (r searchResults) Data() interface{} {
return r.boards
return r
}

func (r searchResults) String() string {
if len(r.boards) == 0 {
if len(r.Boards) == 0 {
return ""
}

t := table.New()
t.SetHeader(tr("Board Name"), tr("FQBN"), tr("Platform ID"), "")

sort.Slice(r.boards, func(i, j int) bool {
return r.boards[i].Name < r.boards[j].Name
sort.Slice(r.Boards, func(i, j int) bool {
return r.Boards[i].Name < r.Boards[j].Name
})

for _, item := range r.boards {
for _, item := range r.Boards {
hidden := ""
if item.IsHidden {
hidden = tr("(hidden)")
Expand Down
31 changes: 4 additions & 27 deletions internal/cli/config/add.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,33 +25,10 @@ import (
"github.com/spf13/cobra"
)

// // TODO: When update to go 1.18 or later, convert to generic
// // to allow uniquify() on any slice that supports
// // `comparable`
// // See https://gosamples.dev/generics-remove-duplicates-slice/
// func uniquify[T comparable](s []T) []T {
// // use a map, which enforces unique keys
// inResult := make(map[T]bool)
// var result []T
// // loop through input slice **in order**,
// // to ensure output retains that order
// // (except that it removes duplicates)
// for i := 0; i < len(s); i++ {
// // attempt to use the element as a key
// if _, ok := inResult[s[i]]; !ok {
// // if key didn't exist in map,
// // add to map and append to result
// inResult[s[i]] = true
// result = append(result, s[i])
// }
// }
// return result
// }

func uniquifyStringSlice(s []string) []string {
func uniquify[T comparable](s []T) []T {
// use a map, which enforces unique keys
inResult := make(map[string]bool)
var result []string
inResult := make(map[T]bool)
var result []T
// loop through input slice **in order**,
// to ensure output retains that order
// (except that it removes duplicates)
Expand Down Expand Up @@ -96,7 +73,7 @@ func runAddCommand(cmd *cobra.Command, args []string) {

v := configuration.Settings.GetStringSlice(key)
v = append(v, args[1:]...)
v = uniquifyStringSlice(v)
v = uniquify(v)
configuration.Settings.Set(key, v)

if err := configuration.Settings.WriteConfig(); err != nil {
Expand Down
6 changes: 3 additions & 3 deletions internal/cli/config/dump.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,15 +45,15 @@ func runDumpCommand(cmd *cobra.Command, args []string) {
// output from this command requires special formatting, let's create a dedicated
// feedback.Result implementation
type dumpResult struct {
data map[string]interface{}
Config map[string]interface{} `json:"config"`
}

func (dr dumpResult) Data() interface{} {
return dr.data
return dr
}

func (dr dumpResult) String() string {
bs, err := yaml.Marshal(dr.data)
bs, err := yaml.Marshal(dr.Config)
if err != nil {
// Should never happen
panic(tr("unable to marshal config to YAML: %v", err))
Expand Down
17 changes: 15 additions & 2 deletions internal/cli/config/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -108,8 +108,21 @@ func runInitCommand(cmd *cobra.Command, args []string) {
if err := newSettings.WriteConfigAs(configFileAbsPath.String()); err != nil {
feedback.Fatal(tr("Cannot create config file: %v", err), feedback.ErrGeneric)
}
feedback.PrintResult(initResult{ConfigFileAbsPath: configFileAbsPath})
}

// output from this command requires special formatting, let's create a dedicated
// feedback.Result implementation
type initResult struct {
ConfigFileAbsPath *paths.Path `json:"config_path"`
}

func (dr initResult) Data() interface{} {
return dr
}

msg := tr("Config file written to: %s", configFileAbsPath.String())
func (dr initResult) String() string {
msg := tr("Config file written to: %s", dr.ConfigFileAbsPath.String())
logrus.Info(msg)
feedback.Print(msg)
return msg
}
2 changes: 1 addition & 1 deletion internal/cli/config/set.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ func runSetCommand(cmd *cobra.Command, args []string) {
var value interface{}
switch kind {
case reflect.Slice:
value = uniquifyStringSlice(args[1:])
value = uniquify(args[1:])
case reflect.String:
value = args[1]
case reflect.Bool:
Expand Down
10 changes: 5 additions & 5 deletions internal/cli/core/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,32 +89,32 @@ func GetList(inst *rpc.Instance, all bool, updatableOnly bool) []*rpc.PlatformSu
func newCoreListResult(in []*rpc.PlatformSummary, updatableOnly bool) *coreListResult {
res := &coreListResult{updatableOnly: updatableOnly}
for _, platformSummary := range in {
res.platforms = append(res.platforms, result.NewPlatformSummary(platformSummary))
res.Platforms = append(res.Platforms, result.NewPlatformSummary(platformSummary))
}
return res
}

type coreListResult struct {
platforms []*result.PlatformSummary
Platforms []*result.PlatformSummary `json:"platforms"`
updatableOnly bool
}

// Data implements Result interface
func (ir coreListResult) Data() interface{} {
return ir.platforms
return ir
}

// String implements Result interface
func (ir coreListResult) String() string {
if len(ir.platforms) == 0 {
if len(ir.Platforms) == 0 {
if ir.updatableOnly {
return tr("All platforms are up to date.")
}
return tr("No platforms installed.")
}
t := table.New()
t.SetHeader(tr("ID"), tr("Installed"), tr("Latest"), tr("Name"))
for _, platform := range ir.platforms {
for _, platform := range ir.Platforms {
latestVersion := platform.LatestVersion.String()
if latestVersion == "" {
latestVersion = "n/a"
Expand Down
12 changes: 6 additions & 6 deletions internal/cli/core/search.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,27 +86,27 @@ func runSearchCommand(cmd *cobra.Command, args []string, allVersions bool) {
// output from this command requires special formatting, let's create a dedicated
// feedback.Result implementation
type searchResults struct {
platforms []*result.PlatformSummary
Platforms []*result.PlatformSummary `json:"platforms"`
allVersions bool
}

func newSearchResult(in []*rpc.PlatformSummary, allVersions bool) *searchResults {
res := &searchResults{
platforms: make([]*result.PlatformSummary, len(in)),
Platforms: make([]*result.PlatformSummary, len(in)),
allVersions: allVersions,
}
for i, platformSummary := range in {
res.platforms[i] = result.NewPlatformSummary(platformSummary)
res.Platforms[i] = result.NewPlatformSummary(platformSummary)
}
return res
}

func (sr searchResults) Data() interface{} {
return sr.platforms
return sr
}

func (sr searchResults) String() string {
if len(sr.platforms) == 0 {
if len(sr.Platforms) == 0 {
return tr("No platforms matching your search.")
}

Expand All @@ -121,7 +121,7 @@ func (sr searchResults) String() string {
t.AddRow(platform.Id, release.Version, release.FormatName())
}

for _, platform := range sr.platforms {
for _, platform := range sr.Platforms {
// When allVersions is not requested we only show the latest compatible version
if !sr.allVersions {
addRow(platform, platform.GetLatestRelease())
Expand Down
8 changes: 4 additions & 4 deletions internal/cli/lib/check_deps.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import (
"github.com/arduino/arduino-cli/commands/lib"
"github.com/arduino/arduino-cli/internal/cli/arguments"
"github.com/arduino/arduino-cli/internal/cli/feedback"
fResult "github.com/arduino/arduino-cli/internal/cli/feedback/result"
"github.com/arduino/arduino-cli/internal/cli/feedback/result"
"github.com/arduino/arduino-cli/internal/cli/instance"
rpc "github.com/arduino/arduino-cli/rpc/cc/arduino/cli/commands/v1"
"github.com/fatih/color"
Expand Down Expand Up @@ -66,13 +66,13 @@ func runDepsCommand(cmd *cobra.Command, args []string) {
feedback.Fatal(tr("Error resolving dependencies for %[1]s: %[2]s", libRef, err), feedback.ErrGeneric)
}

feedback.PrintResult(&checkDepResult{deps: fResult.NewLibraryResolveDependenciesResponse(deps)})
feedback.PrintResult(&checkDepResult{deps: result.NewLibraryResolveDependenciesResponse(deps)})
}

// output from this command requires special formatting, let's create a dedicated
// feedback.Result implementation
type checkDepResult struct {
deps *fResult.LibraryResolveDependenciesResponse
deps *result.LibraryResolveDependenciesResponse
}

func (dr checkDepResult) Data() interface{} {
Expand Down Expand Up @@ -103,7 +103,7 @@ func (dr checkDepResult) String() string {
return res
}

func outputDep(dep *fResult.LibraryDependencyStatus) string {
func outputDep(dep *result.LibraryDependencyStatus) string {
res := ""
green := color.New(color.FgGreen)
red := color.New(color.FgRed)
Expand Down
4 changes: 2 additions & 2 deletions internal/cli/lib/examples.go
Original file line number Diff line number Diff line change
Expand Up @@ -94,11 +94,11 @@ type libraryExamples struct {
}

type libraryExamplesResult struct {
Examples []*libraryExamples
Examples []*libraryExamples `json:"examples"`
}

func (ir libraryExamplesResult) Data() interface{} {
return ir.Examples
return ir
}

func (ir libraryExamplesResult) String() string {
Expand Down
Loading