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
+120 -58
View File
@@ -12,18 +12,18 @@ import (
"strings"
)
// runDiskUsage implements `du -x --max-depth=1 -h <path> | sort -hr`.
// runDiskUsage implements `du -x --max-depth=N -h <path> | sort -hr`.
//
// It reports every direct child of the given directory: regular files by their
// size, directories by the recursive size of their regular files. Symbolic
// links are not followed and, where the platform exposes device IDs, nested
// filesystems are not crossed (-x).
// It reports the entries down to N levels below the given directory: regular
// files by their size, directories by the recursive size of their regular
// files. Symbolic links are not followed and, where the platform exposes
// device IDs, nested filesystems are not crossed (-x).
func runDiskUsage(ctx context.Context, req BuiltinRequest) error {
paths, err := parseDiskUsageArgs(req.Args)
opts, err := parseDiskUsageOptions(req.Args)
if err != nil {
return err
}
root := paths[0]
root, depth := opts.path, opts.depth
rootInfo, err := os.Stat(root)
if err != nil {
@@ -33,50 +33,19 @@ func runDiskUsage(ctx context.Context, req BuiltinRequest) error {
return fmt.Errorf("%q 不是目录", root)
}
entries, err := os.ReadDir(root)
if err != nil {
return fmt.Errorf("读取目录 %q 失败: %w", root, err)
}
rootDev, checkDev := deviceOf(rootInfo)
type entrySize struct {
path string
size int64
// Each listed entry is a direct child, or with --depth a deeper one.
// Every file's size is counted exactly once, so the total stays the real
// directory size rather than summing overlapping subtrees.
items := make([]entrySize, 0, 16)
if err := collectEntries(ctx, root, root, 1, depth, rootDev, checkDev, &items); err != nil {
return err
}
items := make([]entrySize, 0, len(entries))
var total int64
for _, entry := range entries {
if err := ctx.Err(); err != nil {
return err
}
path := filepath.Join(root, entry.Name())
info, err := entry.Info()
if err != nil {
// Unreadable entries are skipped rather than failing the whole run.
continue
}
var size int64
switch {
case info.IsDir():
if checkDev {
if dev, ok := deviceOf(info); ok && dev != rootDev {
// -x: a different filesystem contributes nothing.
break
}
}
size, err = dirSize(ctx, path, rootDev, checkDev)
if err != nil {
return err
}
case info.Mode().IsRegular():
size = info.Size()
}
items = append(items, entrySize{path: path, size: size})
total += size
for _, item := range items {
total += item.size
}
sort.SliceStable(items, func(i, j int) bool {
@@ -87,17 +56,91 @@ func runDiskUsage(ctx context.Context, req BuiltinRequest) error {
})
w := req.Stdout
shownRoot := root
for _, item := range items {
fmt.Fprintf(w, "%-8s %s\n", humanSize(item.size), item.path)
fmt.Fprintf(w, "%-8s %s\n", humanSize(item.size), strings.TrimPrefix(item.path, "./"))
}
fmt.Fprintf(w, "%-8s 总计 %s\n", humanSize(total), root)
fmt.Fprintf(w, "%-8s 总计 %s\n", humanSize(total), shownRoot)
return nil
}
// parseDiskUsageArgs accepts either a positional path or --path/--path=value.
// Exactly one path is required.
func parseDiskUsageArgs(args []string) ([]string, error) {
// entrySize is one row of the report.
type entrySize struct {
path string
size int64
}
// collectEntries walks dir down to maxDepth levels. Files are added with their
// own size; a directory at the deepest level is added with its recursive size
// (the "how big is this subtree" question), and shallower directories are
// descended into. Symbolic links are skipped and other filesystems are not
// crossed (the -x behaviour).
func collectEntries(ctx context.Context, dir, root string, level, maxDepth int, rootDev uint64, checkDev bool, out *[]entrySize) error {
entries, err := os.ReadDir(dir)
if err != nil {
if dir == root {
return fmt.Errorf("读取目录 %q 失败: %w", dir, err)
}
return nil // an unreadable subdirectory must not abort the report
}
for _, entry := range entries {
if err := ctx.Err(); err != nil {
return err
}
path := filepath.Join(dir, entry.Name())
info, err := entry.Info()
if err != nil {
continue // unreadable entries are skipped
}
switch {
case info.IsDir():
if checkDev {
if dev, ok := deviceOf(info); ok && dev != rootDev {
// -x: a different filesystem contributes nothing.
continue
}
}
if level >= maxDepth {
size, err := dirSize(ctx, path, rootDev, checkDev)
if err != nil {
return err
}
*out = append(*out, entrySize{path: path, size: size})
continue
}
if err := collectEntries(ctx, path, root, level+1, maxDepth, rootDev, checkDev, out); err != nil {
return err
}
case info.Mode().IsRegular():
*out = append(*out, entrySize{path: path, size: info.Size()})
}
}
return nil
}
// diskUsageOptions are the parsed arguments of `kopsc disk usage`.
type diskUsageOptions struct {
path string
depth int
}
// parseDiskUsageOptions accepts either a positional path or
// --path/--path=value, plus --depth/-d. At most one path may be given;
// without one the current directory is used.
func parseDiskUsageOptions(args []string) (diskUsageOptions, error) {
paths, depth, err := parseDiskUsageArgs(args)
if err != nil {
return diskUsageOptions{}, err
}
return diskUsageOptions{path: paths[0], depth: depth}, nil
}
// parseDiskUsageArgs returns the path and the depth (1 by default).
func parseDiskUsageArgs(args []string) ([]string, int, error) {
var paths []string
depth := 1
positionalOnly := false
for i := 0; i < len(args); i++ {
@@ -107,32 +150,51 @@ func parseDiskUsageArgs(args []string) ([]string, error) {
paths = append(paths, arg)
case arg == "--":
positionalOnly = true
case arg == "--depth" || arg == "-d":
if i+1 >= len(args) {
return nil, 0, fmt.Errorf("%s 缺少层数,例如 --depth 2", arg)
}
i++
n, err := strconv.Atoi(args[i])
if err != nil || n < 1 {
return nil, 0, fmt.Errorf("--depth 需要大于 0 的整数,收到 %q", args[i])
}
depth = n
case strings.HasPrefix(arg, "--depth="):
value := strings.TrimPrefix(arg, "--depth=")
n, err := strconv.Atoi(value)
if err != nil || n < 1 {
return nil, 0, fmt.Errorf("--depth 需要大于 0 的整数,收到 %q", value)
}
depth = n
case arg == "--path":
if i+1 >= len(args) {
return nil, errors.New("--path 缺少参数,例如 --path /var")
return nil, 0, errors.New("--path 缺少参数,例如 --path /var")
}
paths = append(paths, args[i+1])
i++
case strings.HasPrefix(arg, "--path="):
value := strings.TrimPrefix(arg, "--path=")
if value == "" {
return nil, errors.New("--path 缺少参数,例如 --path /var")
return nil, 0, errors.New("--path 缺少参数,例如 --path /var")
}
paths = append(paths, value)
case strings.HasPrefix(arg, "-") && arg != "-":
return nil, fmt.Errorf("未知选项 %q:深度(-d 1)、人类可读(-h)与不跨文件系统(-x)已固定", arg)
return nil, 0, fmt.Errorf("未知选项 %q:人类可读(-h)与不跨文件系统(-x)已固定,深度请用 --depth N", arg)
default:
paths = append(paths, arg)
}
}
// The most common use is "what is taking space right here", so the path
// defaults to the current directory.
if len(paths) == 0 {
return nil, errors.New("必须提供路径,例如 kopsc disk usage /var 或 kopsc disk usage --path /var")
paths = append(paths, ".")
}
if len(paths) > 1 {
return nil, fmt.Errorf("一次只支持一个路径,收到 %d 个", len(paths))
return nil, 0, fmt.Errorf("一次只支持一个路径,收到 %d 个", len(paths))
}
return paths, nil
return paths, depth, nil
}
// dirSize sums the sizes of regular files under dir without following symbolic