cli/command: modernize with slices and maps packages

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
This commit is contained in:
Sebastiaan van Stijn
2026-09-02 02:50:27 +02:00
parent f838c7c84b
commit ed406be52e
21 changed files with 140 additions and 146 deletions
+4 -3
View File
@@ -3,9 +3,9 @@ package completion
import (
"context"
"errors"
"sort"
"testing"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/moby/moby/api/types/container"
"github.com/moby/moby/api/types/image"
"github.com/moby/moby/api/types/network"
@@ -177,9 +177,10 @@ func TestCompleteEnvVarNames(t *testing.T) {
values, directives := EnvVarNames()(nil, nil, "")
assert.Check(t, is.Equal(directives&cobra.ShellCompDirectiveNoFileComp, cobra.ShellCompDirectiveNoFileComp), "Should not perform file completion")
sort.Strings(values)
expected := []string{"ENV_A", "ENV_B"}
assert.Check(t, is.DeepEqual(values, expected))
assert.Check(t, is.DeepEqual(values, expected, cmpopts.SortSlices(func(a, b string) bool {
return a < b
})))
}
func TestCompleteFileNames(t *testing.T) {
+4 -4
View File
@@ -7,7 +7,6 @@ import (
"os"
"path/filepath"
"runtime"
"sort"
"strings"
"testing"
@@ -15,6 +14,7 @@ import (
"github.com/docker/cli/cli/config/configfile"
"github.com/docker/cli/internal/test"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/moby/moby/api/types/container"
"github.com/moby/moby/api/types/system"
"github.com/moby/moby/client"
@@ -284,12 +284,12 @@ func TestCreateContainerWithProxyConfig(t *testing.T) {
"ALL_PROXY=allProxy",
"all_proxy=allProxy",
}
sort.Strings(expected)
fakeCLI := test.NewFakeCli(&fakeClient{
createContainerFunc: func(options client.ContainerCreateOptions) (client.ContainerCreateResult, error) {
sort.Strings(options.Config.Env)
assert.DeepEqual(t, options.Config.Env, expected)
assert.DeepEqual(t, options.Config.Env, expected, cmpopts.SortSlices(func(a, b string) bool {
return a < b
}))
return client.ContainerCreateResult{}, nil
},
})
+4 -3
View File
@@ -4,11 +4,11 @@ import (
"context"
"errors"
"io"
"sort"
"sync"
"testing"
"github.com/docker/cli/internal/test"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/moby/moby/client"
"gotest.tools/v3/assert"
is "gotest.tools/v3/assert/cmp"
@@ -87,8 +87,9 @@ func TestRestart(t *testing.T) {
} else {
assert.Check(t, is.Nil(err))
}
sort.Strings(restarted)
assert.Check(t, is.DeepEqual(restarted, tc.restarted))
assert.Check(t, is.DeepEqual(restarted, tc.restarted, cmpopts.SortSlices(func(a, b string) bool {
return a < b
})))
})
}
}
+5 -3
View File
@@ -4,11 +4,11 @@ import (
"context"
"errors"
"io"
"sort"
"sync"
"testing"
"github.com/docker/cli/internal/test"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/moby/moby/client"
"gotest.tools/v3/assert"
)
@@ -53,8 +53,10 @@ func TestRemoveForce(t *testing.T) {
assert.NilError(t, err)
}
assert.Equal(t, cli.ErrBuffer().String(), "")
sort.Strings(removed)
assert.DeepEqual(t, removed, []string{"mycontainer", "nosuchcontainer"})
expected := []string{"mycontainer", "nosuchcontainer"}
assert.DeepEqual(t, removed, expected, cmpopts.SortSlices(func(a, b string) bool {
return a < b
}))
})
}
}
+4 -3
View File
@@ -4,11 +4,11 @@ import (
"context"
"errors"
"io"
"sort"
"sync"
"testing"
"github.com/docker/cli/internal/test"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/moby/moby/client"
"gotest.tools/v3/assert"
is "gotest.tools/v3/assert/cmp"
@@ -88,8 +88,9 @@ func TestStop(t *testing.T) {
} else {
assert.Check(t, is.Nil(err))
}
sort.Strings(stopped)
assert.Check(t, is.DeepEqual(stopped, tc.stopped))
assert.Check(t, is.DeepEqual(stopped, tc.stopped, cmpopts.SortSlices(func(a, b string) bool {
return a < b
})))
})
}
}
+16 -13
View File
@@ -1,7 +1,10 @@
// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
//go:build go1.26
package formatter
import (
"sort"
"slices"
"strconv"
"strings"
"time"
@@ -52,20 +55,20 @@ shared: {{.Shared}}
}
func buildCacheSort(buildCache []build.CacheRecord) {
sort.Slice(buildCache, func(i, j int) bool {
lui, luj := buildCache[i].LastUsedAt, buildCache[j].LastUsedAt
slices.SortFunc(buildCache, func(a, b build.CacheRecord) int {
switch {
case lui == nil && luj == nil:
return strings.Compare(buildCache[i].ID, buildCache[j].ID) < 0
case lui == nil:
return true
case luj == nil:
return false
case lui.Equal(*luj):
return strings.Compare(buildCache[i].ID, buildCache[j].ID) < 0
default:
return lui.Before(*luj)
case a.LastUsedAt == nil && b.LastUsedAt == nil:
return strings.Compare(a.ID, b.ID)
case a.LastUsedAt == nil:
return -1
case b.LastUsedAt == nil:
return 1
}
if c := a.LastUsedAt.Compare(*b.LastUsedAt); c != 0 {
return c
}
return strings.Compare(a.ID, b.ID)
})
}
+13 -19
View File
@@ -4,9 +4,10 @@
package formatter
import (
"cmp"
"fmt"
"net"
"sort"
"slices"
"strconv"
"strings"
"time"
@@ -295,7 +296,7 @@ func (c *ContainerContext) Labels() string {
for k, v := range c.c.Labels {
joinLabels = append(joinLabels, k+"="+v)
}
sort.Strings(joinLabels)
slices.Sort(joinLabels)
return strings.Join(joinLabels, ",")
}
@@ -395,9 +396,7 @@ func DisplayablePorts(ports []container.PortSummary) string {
var result []string
var hostMappings []string
var groupMapKeys []string
sort.Slice(ports, func(i, j int) bool {
return comparePorts(ports[i], ports[j])
})
slices.SortFunc(ports, comparePorts)
for _, port := range ports {
current := port.PrivatePort
@@ -452,18 +451,13 @@ func formGroup(key string, start, last uint16) string {
return group + "/" + groupType
}
func comparePorts(i, j container.PortSummary) bool {
if i.PrivatePort != j.PrivatePort {
return i.PrivatePort < j.PrivatePort
}
if i.IP != j.IP {
return i.IP.Less(j.IP)
}
if i.PublicPort != j.PublicPort {
return i.PublicPort < j.PublicPort
}
return i.Type < j.Type
// comparePorts compares ports by private port, IP address, public port,
// and protocol, in that order.
func comparePorts(a, b container.PortSummary) int {
return cmp.Or(
cmp.Compare(a.PrivatePort, b.PrivatePort),
a.IP.Compare(b.IP),
cmp.Compare(a.PublicPort, b.PublicPort),
cmp.Compare(a.Type, b.Type),
)
}
@@ -1,3 +1,6 @@
// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
//go:build go1.26
package git
import (
@@ -7,6 +10,7 @@ import (
"os"
"os/exec"
"path/filepath"
"slices"
"strings"
"github.com/moby/sys/symlink"
@@ -202,7 +206,7 @@ func (repo gitRepo) checkout(root string) (string, error) {
}
func (repo gitRepo) gitWithinDir(dir string, args ...string) ([]byte, error) {
args = append([]string{"-c", "protocol.file.allow=never"}, args...) // Block sneaky repositories from using repos from the filesystem as submodules.
args = slices.Concat([]string{"-c", "protocol.file.allow=never"}, args) // Block sneaky repositories from using repos from the filesystem as submodules.
cmd := exec.Command("git", args...)
cmd.Dir = dir
// Disable unsafe remote protocols.
+2 -2
View File
@@ -8,7 +8,7 @@ import (
"io"
"os"
"path/filepath"
"sort"
"slices"
"testing"
"github.com/docker/cli/cli/streams"
@@ -211,6 +211,6 @@ func (f *fakeBuild) filenames(t *testing.T) []string {
for _, header := range h {
names = append(names, header.Name)
}
sort.Strings(names)
slices.Sort(names)
return names
}
+5 -3
View File
@@ -102,9 +102,11 @@ func TestRunPushRespectsNoColorForAuxNotes(t *testing.T) {
SelectedManifest: ocispec.Descriptor{Digest: "sha256:2222222222222222222222222222222222222222222222222222222222222222"},
})
assert.NilError(t, err)
line := append([]byte(`{"aux":`), aux...)
line = append(line, '}', '\n')
return fakeStreamResult{ReadCloser: io.NopCloser(bytes.NewReader(line))}, nil
var buf bytes.Buffer
buf.WriteString(`{"aux":`)
buf.Write(aux)
buf.WriteString("}\n")
return fakeStreamResult{ReadCloser: io.NopCloser(&buf)}, nil
},
})
cli.Out().SetIsTerminal(true)
+13 -9
View File
@@ -1,8 +1,13 @@
// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
//go:build go1.26
package service
import (
"cmp"
"errors"
"fmt"
"slices"
"sort"
"strconv"
"strings"
@@ -775,17 +780,16 @@ func (c *serviceContext) Ports() string {
return ""
}
pr := portRange{}
ports := []string{}
servicePorts := c.service.Endpoint.Ports
sort.Slice(servicePorts, func(i, j int) bool {
if servicePorts[i].Protocol == servicePorts[j].Protocol {
return servicePorts[i].PublishedPort < servicePorts[j].PublishedPort
}
return servicePorts[i].Protocol < servicePorts[j].Protocol
// Sort by protocol first, then by published port.
slices.SortFunc(c.service.Endpoint.Ports, func(a, b swarm.PortConfig) int {
return cmp.Or(
cmp.Compare(a.Protocol, b.Protocol),
cmp.Compare(a.PublishedPort, b.PublishedPort),
)
})
var pr portRange
var ports []string
for _, p := range c.service.Endpoint.Ports {
if p.PublishMode == swarm.PortConfigPublishModeIngress {
prIsRange := pr.tEnd != pr.tStart
+5 -2
View File
@@ -1,3 +1,6 @@
// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
//go:build go1.26
package service
import (
@@ -6,7 +9,7 @@ import (
"errors"
"fmt"
"io"
"sort"
"slices"
"strconv"
"strings"
@@ -314,7 +317,7 @@ func (lw *logWriter) Write(buf []byte) (int, error) {
d = append(d, k+"="+details[k])
}
// then sort em
sort.Strings(d)
slices.Sort(d)
// then join and append
output = append(output, []byte(strings.Join(d, ","))...)
output = append(output, ' ')
+5 -5
View File
@@ -4,12 +4,12 @@
package service
import (
"cmp"
"context"
"errors"
"fmt"
"net/netip"
"slices"
"sort"
"strconv"
"strings"
"time"
@@ -737,15 +737,15 @@ func (options *serviceOptions) ToService(ctx context.Context, apiClient client.N
}
networks := convertNetworks(options.networks)
for i, net := range networks {
nwID, err := resolveNetworkID(ctx, apiClient, net.Target)
for i := range networks {
nwID, err := resolveNetworkID(ctx, apiClient, networks[i].Target)
if err != nil {
return service, err
}
networks[i].Target = nwID
}
sort.Slice(networks, func(i, j int) bool {
return networks[i].Target < networks[j].Target
slices.SortFunc(networks, func(a, b swarm.NetworkAttachmentConfig) int {
return cmp.Compare(a.Target, b.Target)
})
resources, err := options.resources.ToResourceRequirements(flags)
+20 -38
View File
@@ -4,13 +4,13 @@
package service
import (
"cmp"
"context"
"errors"
"fmt"
"maps"
"net/netip"
"slices"
"sort"
"strings"
"time"
@@ -635,7 +635,7 @@ func updatePlacementConstraints(flags *pflag.FlagSet, placement *swarm.Placement
}
}
// Sort so that result is predictable.
sort.Strings(newConstraints)
slices.Sort(newConstraints)
placement.Constraints = newConstraints
}
@@ -720,7 +720,7 @@ func updateSysCtls(flags *pflag.FlagSet, field *map[string]string) {
}
func updateUlimits(flags *pflag.FlagSet, ulimits []*container.Ulimit) []*container.Ulimit {
newUlimits := make(map[string]*container.Ulimit)
newUlimits := make(map[string]*container.Ulimit, len(ulimits))
for _, ulimit := range ulimits {
newUlimits[ulimit.Name] = ulimit
@@ -737,17 +737,9 @@ func updateUlimits(flags *pflag.FlagSet, ulimits []*container.Ulimit) []*contain
newUlimits[ulimit.Name] = ulimit
}
}
if len(newUlimits) == 0 {
return nil
}
limits := make([]*container.Ulimit, 0, len(newUlimits))
for _, ulimit := range newUlimits {
limits = append(limits, ulimit)
}
sort.SliceStable(limits, func(i, j int) bool {
return limits[i].Name < limits[j].Name
return slices.SortedFunc(maps.Values(newUlimits), func(a, b *container.Ulimit) int {
return cmp.Compare(a.Name, b.Name)
})
return limits
}
func updateEnvironment(flags *pflag.FlagSet, field *[]string) {
@@ -953,14 +945,12 @@ func updateMounts(flags *pflag.FlagSet, mounts *[]mount.Mount) error {
newMounts = append(newMounts, mnt)
}
}
sort.Slice(newMounts, func(i, j int) bool {
a, b := newMounts[i], newMounts[j]
if a.Source == b.Source {
return a.Target < b.Target
}
return a.Source < b.Source
// Sort mounts by source, then by target.
slices.SortFunc(newMounts, func(a, b mount.Mount) int {
return cmp.Or(
cmp.Compare(a.Source, b.Source),
cmp.Compare(a.Target, b.Target),
)
})
*mounts = newMounts
return nil
@@ -980,7 +970,7 @@ func updateGroups(flags *pflag.FlagSet, groups *[]string) error {
}
}
// Sort so that result is predictable.
sort.Strings(newGroups)
slices.Sort(newGroups)
*groups = newGroups
return nil
@@ -1039,7 +1029,7 @@ func updateDNSConfig(flags *pflag.FlagSet, config **swarm.DNSConfig) error {
}
}
// Sort so that result is predictable.
sort.Strings(newConfig.Search)
slices.Sort(newConfig.Search)
options := (*config).Options
if flags.Changed(flagDNSOptionAdd) {
@@ -1054,7 +1044,7 @@ func updateDNSConfig(flags *pflag.FlagSet, config **swarm.DNSConfig) error {
}
}
// Sort so that result is predictable.
sort.Strings(newConfig.Options)
slices.Sort(newConfig.Options)
*config = newConfig
return nil
@@ -1107,12 +1097,8 @@ portLoop:
}
}
// Sort the PortConfig to avoid unnecessary updates
sort.Slice(newPorts, func(i, j int) bool {
// We convert PortConfig into `port/protocol`, e.g., `80/tcp`
// In updatePorts we already filter out with map so there is duplicate entries
return portConfigToString(&newPorts[i]) < portConfigToString(&newPorts[j])
})
// Sort the PortConfig to avoid unnecessary updates.
slices.SortFunc(newPorts, swarm.PortConfig.Compare)
*portConfig = newPorts
return nil
}
@@ -1348,8 +1334,8 @@ func updateNetworks(ctx context.Context, apiClient client.NetworkAPIClient, flag
}
}
sort.Slice(newNetworks, func(i, j int) bool {
return newNetworks[i].Target < newNetworks[j].Target
slices.SortFunc(newNetworks, func(a, b swarm.NetworkAttachmentConfig) int {
return cmp.Compare(a.Target, b.Target)
})
spec.TaskTemplate.Networks = newNetworks
@@ -1523,10 +1509,6 @@ func capsList(caps map[string]bool) []string {
if caps[opts.AllCapabilities] {
return []string{opts.AllCapabilities}
}
out := make([]string, 0, len(caps))
for c := range caps {
out = append(out, c)
}
sort.Strings(out)
return out
return slices.Sorted(maps.Keys(caps))
}
+3 -3
View File
@@ -4,7 +4,7 @@ import (
"context"
"fmt"
"net/netip"
"sort"
"slices"
"strconv"
"testing"
"time"
@@ -141,7 +141,7 @@ func TestUpdateEnvironment(t *testing.T) {
updateEnvironment(flags, &envs)
assert.Assert(t, is.Len(envs, 2))
// Order has been removed in updateEnvironment (map)
sort.Strings(envs)
slices.Sort(envs)
assert.Check(t, is.Equal("toadd=newenv", envs[0]))
assert.Check(t, is.Equal("tokeep=value", envs[1]))
}
@@ -282,7 +282,7 @@ func TestUpdatePorts(t *testing.T) {
assert.Assert(t, is.Len(portConfigs, 2))
// Do a sort to have the order (might have changed by map)
targetPorts := []int{int(portConfigs[0].TargetPort), int(portConfigs[1].TargetPort)}
sort.Ints(targetPorts)
slices.Sort(targetPorts)
assert.Check(t, is.Equal(555, targetPorts[0]))
assert.Check(t, is.Equal(1000, targetPorts[1]))
}
+3 -3
View File
@@ -10,7 +10,7 @@ import (
"os"
"path/filepath"
"runtime"
"sort"
"slices"
"strings"
"github.com/distribution/reference"
@@ -75,9 +75,9 @@ func getDictsFrom(configFiles []composetypes.ConfigFile) []map[string]any {
func propertyWarnings(properties map[string]string) string {
msgs := make([]string, 0, len(properties))
for name, description := range properties {
msgs = append(msgs, fmt.Sprintf("%s: %s", name, description))
msgs = append(msgs, name+": "+description)
}
sort.Strings(msgs)
slices.Sort(msgs)
return strings.Join(msgs, "\n\n")
}
+8 -8
View File
@@ -1,10 +1,14 @@
// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
//go:build go1.26
package stack
import (
"cmp"
"context"
"errors"
"fmt"
"sort"
"slices"
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command"
@@ -96,15 +100,11 @@ func runRemove(ctx context.Context, dockerCli command.Cli, opts removeOptions) e
return errors.Join(errs...)
}
func sortServiceByName(services []swarm.Service) func(i, j int) bool {
return func(i, j int) bool {
return services[i].Spec.Name < services[j].Spec.Name
}
}
func removeServices(ctx context.Context, dockerCLI command.Cli, services []swarm.Service) bool {
slices.SortFunc(services, func(a, b swarm.Service) int {
return cmp.Compare(a.Spec.Name, b.Spec.Name)
})
var hasError bool
sort.Slice(services, sortServiceByName(services))
for _, service := range services {
_, _ = fmt.Fprintln(dockerCLI.Out(), "Removing service", service.Spec.Name)
if _, err := dockerCLI.Client().ServiceRemove(ctx, service.ID, client.ServiceRemoveOptions{}); err != nil {
+8 -9
View File
@@ -1,10 +1,14 @@
// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
//go:build go1.26
package system
import (
"context"
"fmt"
"io"
"sort"
"maps"
"slices"
"strings"
"text/template"
"time"
@@ -133,18 +137,13 @@ func prettyPrintEvent(out io.Writer, event events.Message) error {
_, _ = fmt.Fprintf(out, "%s %s %s", event.Type, event.Action, event.Actor.ID)
if len(event.Actor.Attributes) > 0 {
keys := make([]string, 0, len(event.Actor.Attributes))
for k := range event.Actor.Attributes {
keys = append(keys, k)
}
sort.Strings(keys)
keys := slices.Sorted(maps.Keys(event.Actor.Attributes))
attrs := make([]string, 0, len(keys))
for _, k := range keys {
v := event.Actor.Attributes[k]
attrs = append(attrs, k+"="+v)
attrs = append(attrs, k+"="+event.Actor.Attributes[k])
}
_, _ = fmt.Fprintf(out, " (%s)", strings.Join(attrs, ", "))
}
_, _ = fmt.Fprint(out, "\n")
_, _ = fmt.Fprintln(out)
return nil
}
+3 -3
View File
@@ -8,7 +8,7 @@ import (
"errors"
"fmt"
"io"
"sort"
"slices"
"strings"
"github.com/docker/cli/cli"
@@ -468,11 +468,11 @@ func printSwarmInfo(output io.Writer, info system.Info) {
}
fprintln(output, " Node Address:", info.Swarm.NodeAddr)
if len(info.Swarm.RemoteManagers) > 0 {
managers := []string{}
managers := make([]string, 0, len(info.Swarm.RemoteManagers))
for _, entry := range info.Swarm.RemoteManagers {
managers = append(managers, entry.Addr)
}
sort.Strings(managers)
slices.Sort(managers)
fprintln(output, " Manager Addresses:")
for _, entry := range managers {
fprintf(output, " %s\n", entry)
+6 -7
View File
@@ -1,11 +1,15 @@
// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
//go:build go1.26
package system
import (
"context"
"fmt"
"io"
"maps"
"runtime"
"sort"
"slices"
"strconv"
"text/template"
"time"
@@ -252,10 +256,5 @@ func newVersionTemplate(templateFormat string) (*template.Template, error) {
}
func getDetailsOrder(v system.ComponentVersion) []string {
out := make([]string, 0, len(v.Details))
for k := range v.Details {
out = append(out, k)
}
sort.Strings(out)
return out
return slices.Sorted(maps.Keys(v.Details))
}
+4 -5
View File
@@ -8,11 +8,11 @@ import (
"fmt"
"io"
"maps"
"sort"
"strings"
"testing"
"github.com/docker/cli/internal/test"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/moby/moby/api/types/volume"
"github.com/moby/moby/client"
"gotest.tools/v3/assert"
@@ -230,10 +230,9 @@ func TestVolumeCreateClusterOpts(t *testing.T) {
cli := test.NewFakeCli(&fakeClient{
volumeCreateFunc: func(options client.VolumeCreateOptions) (client.VolumeCreateResult, error) {
sort.SliceStable(options.ClusterVolumeSpec.Secrets, func(i, j int) bool {
return options.ClusterVolumeSpec.Secrets[i].Key < options.ClusterVolumeSpec.Secrets[j].Key
})
assert.DeepEqual(t, options, expectedOptions)
assert.Check(t, is.DeepEqual(options, expectedOptions, cmpopts.SortSlices(func(a, b volume.Secret) bool {
return a.Key < b.Key
})))
return client.VolumeCreateResult{}, nil
},
})