feat: 优化非 Docker 命令的易用性,修复带 digest 镜像无法按名称查找

- docker save/rmi:镜像名不再拼接 digest({{.Digest}} 对多数镜像输出
  sha256:…,导致 alpine:3.20 被存成 alpine:3.20@sha256:… 而查不到),
  digest 单独保存;同时修正自定义 --format 输出中 IMAGE ID 表头未被过滤
- diagnose:目标不存在时给出相似名称候选,无相似时列出当前存在的对象
- net.pids:无进程占用时向 stderr 提示且 stdout 保持为空,新增 --pid-file
- net.ports:默认输出跨平台一致的端口表格,--raw 保留原生命令输出
- disk.usage:路径可省略(默认当前目录),新增 --depth N 控制层级
This commit is contained in:
2026-11-01 08:37:44 +08:00
parent f5ae80c1a8
commit 6b6f8619df
15 changed files with 1317 additions and 130 deletions
+27 -9
View File
@@ -74,6 +74,7 @@ type imageRef struct {
ID string // image ID, e.g. "sha256:abc..."
CreatedAt string // human-readable creation time, as docker lists it
Size string // human-readable size, as docker lists it
Digest string // content digest, e.g. "sha256:abc..."; may be empty
Repository string
Tag string
}
@@ -92,10 +93,12 @@ func listImages(ctx context.Context) ([]imageRef, error) {
var refs []imageRef
for _, line := range splitLines(out) {
if strings.HasPrefix(line, "REPOSITORY") {
fields := strings.Split(line, "\t")
// With kopsc's own --format the header starts with "IMAGE ID", while
// docker's default header starts with "REPOSITORY"; skip either.
if isImageHeader(line, fields) {
continue
}
fields := strings.Split(line, "\t")
ref := imageRef{ID: strings.TrimSpace(fields[0])}
if len(fields) >= 3 {
repo, tag := strings.TrimSpace(fields[1]), strings.TrimSpace(fields[2])
@@ -111,16 +114,34 @@ func listImages(ctx context.Context) ([]imageRef, error) {
}
ref.Repository = repo
ref.Tag = tag
ref.Name = formatImageName(repo, tag, digest)
ref.Digest = digest
ref.Name = formatImageName(repo, tag)
}
refs = append(refs, ref)
}
return refs, nil
}
// formatImageName renders repository[:tag][@digest]. "<none>" is how docker
// groups untagged images.
func formatImageName(repo, tag, digest string) string {
// isImageHeader reports whether a line is docker's column header rather than
// an image row. Headers never carry a real 12-character hex ID.
func isImageHeader(line string, fields []string) bool {
if strings.HasPrefix(line, "REPOSITORY") || strings.HasPrefix(line, "IMAGE ID") {
return true
}
if len(fields) == 0 {
return false
}
return !looksLikeImageID(fields[0])
}
// formatImageName renders repository[:tag]. "<none>" is how docker groups
// untagged images.
//
// The digest is deliberately not part of the name: `docker save` and friends
// are addressed by repository:tag (alpine:3.20), and appending "@sha256:..." —
// which `docker images --format {{.Digest}}` prints for most pulled images —
// made every such image impossible to look up by the name users type.
func formatImageName(repo, tag string) string {
if repo == "<none>" {
return ""
}
@@ -128,9 +149,6 @@ func formatImageName(repo, tag, digest string) string {
if tag != "" && tag != "<none>" {
name += ":" + tag
}
if digest != "" && digest != "<none>" {
name += "@" + digest
}
return name
}