Files
dingtalk-workspace-cli/internal/helpers/output.go

196 lines
6.1 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package helpers
import (
"encoding/json"
"fmt"
"io"
"os"
"strings"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/output"
)
// applyGlobalFilter applies the global --jq / --fields output filters to data
// when either is set. Returns handled=true when it wrote the (filtered) output,
// so callers skip their default JSON encoding. The filter helpers live in
// internal/output (the same ones used by `dws api`); the helper Formatter used
// by product commands previously ignored these flags, making them no-ops.
func (f *Formatter) applyGlobalFilter(data any) (handled bool, err error) {
if deps == nil || deps.Caller == nil {
return false, nil
}
jq := strings.TrimSpace(deps.Caller.JQ())
fields := strings.TrimSpace(deps.Caller.Fields())
if jq == "" && fields == "" {
return false, nil
}
format := output.Format(strings.TrimSpace(deps.Caller.Format()))
if format == "" {
format = output.FormatJSON
}
return true, output.WriteFiltered(f.w, format, data, fields, jq)
}
// Formatter provides output formatting compatible with the old Wukong CLI.
type Formatter struct {
w io.Writer
errW io.Writer
}
func NewFormatter() *Formatter {
return NewFormatterWithWriters(os.Stdout, os.Stderr)
}
// NewFormatterWithWriters 是注入式构造函数(B45,WS1 改动点3):数据流 w 与
// 诊断流 errW 均由调用方注入。nil 按默认进程流处理(w→os.Stdout、
// errW→os.Stderr),因此 NewFormatter 的既有行为不变,只是收口到本构造器。
func NewFormatterWithWriters(w, errW io.Writer) *Formatter {
if w == nil {
w = os.Stdout
}
if errW == nil {
errW = os.Stderr
}
return &Formatter{w: w, errW: errW}
}
// SetWriters 运行期替换两个写入目标(B46,WS1 改动点3):deps.Out 的 writer
// 不再是构造期一次性硬编码,而是可注入接缝。nil 侧保留当前值(支持单侧替换)。
// 既有的 deps.Out.w / deps.Out.errW 直接替换习惯不受影响。
func (f *Formatter) SetWriters(w, errW io.Writer) {
if w != nil {
f.w = w
}
if errW != nil {
f.errW = errW
}
}
// PrintJSON serializes data as pretty-printed JSON and writes it to the output stream.
// Go 的 json.Encoder 默认开启 HTML 转义(SetEscapeHTML(true)),会将 &、<、> 分别
// 转义为 \u0026、\u003c、\u003e。对于大多数 CLI 输出场景这是安全的默认行为。
// 如果返回值中包含 URL 等不应被转义的内容,请使用 PrintJSONUnescaped。
func (f *Formatter) PrintJSON(data any) error {
if handled, err := f.applyGlobalFilter(data); handled {
return err
}
enc := json.NewEncoder(f.w)
enc.SetIndent("", " ")
return enc.Encode(data)
}
// PrintJSONUnescaped 与 PrintJSON 功能相同,但禁用了 HTML 转义。
// 适用于返回值中包含带查询参数的 URL(如预签名上传 URL)的场景,
// 避免 & 被转义为 \u0026 导致 URL 无法直接使用。
//
// 使用场景示例:
// - minutes upload create 返回的 presignedUrl 包含多个 & 分隔的查询参数
// - 其他返回值中包含需要原样输出的 URL 的接口
//
// 影响范围:仅在调用方显式选择时生效,不影响全局 PrintJSON 的行为。
func (f *Formatter) PrintJSONUnescaped(data any) error {
if handled, err := f.applyGlobalFilter(data); handled {
return err
}
enc := json.NewEncoder(f.w)
enc.SetIndent("", " ")
enc.SetEscapeHTML(false)
return enc.Encode(data)
}
func (f *Formatter) PrintRaw(text string) {
fmt.Fprint(f.w, text)
if !strings.HasSuffix(text, "\n") {
fmt.Fprintln(f.w)
}
}
func runeWidth(s string) int {
w := 0
for _, r := range s {
if r >= 0x1100 && (r <= 0x115f || r == 0x2329 || r == 0x232a ||
(r >= 0x2e80 && r <= 0xa4cf && r != 0x303f) ||
(r >= 0xac00 && r <= 0xd7a3) ||
(r >= 0xf900 && r <= 0xfaff) ||
(r >= 0xfe10 && r <= 0xfe19) ||
(r >= 0xfe30 && r <= 0xfe6f) ||
(r >= 0xff00 && r <= 0xff60) ||
(r >= 0xffe0 && r <= 0xffe6) ||
(r >= 0x20000 && r <= 0x2fffd) ||
(r >= 0x30000 && r <= 0x3fffd)) {
w += 2
} else {
w++
}
}
return w
}
func padRight(s string, width int) string {
rw := runeWidth(s)
if rw >= width {
return s
}
return s + strings.Repeat(" ", width-rw)
}
// PrintTable preserves the legacy formatter contract. Unified-result commands
// bypass this formatter and use internal/output, so rollout does not silently
// move bytes for commands that have not migrated yet.
func (f *Formatter) PrintTable(headers []string, rows [][]string) {
if len(rows) == 0 {
return
}
widths := make([]int, len(headers))
for i, h := range headers {
widths[i] = runeWidth(h)
}
for _, row := range rows {
for i, cell := range row {
if i < len(widths) && runeWidth(cell) > widths[i] {
widths[i] = runeWidth(cell)
}
}
}
for i, h := range headers {
fmt.Fprintf(f.w, "%s ", padRight(h, widths[i]))
_ = i
}
fmt.Fprintln(f.w)
for _, w := range widths {
fmt.Fprintf(f.w, "%s ", strings.Repeat("-", w))
}
fmt.Fprintln(f.w)
for _, row := range rows {
for i, cell := range row {
if i < len(widths) {
fmt.Fprintf(f.w, "%s ", padRight(cell, widths[i]))
}
}
fmt.Fprintln(f.w)
}
fmt.Fprintf(f.w, "共 %d 条\n", len(rows))
}
func (f *Formatter) PrintSuccess(msg string) { fmt.Fprintf(f.w, "[OK] %s\n", msg) }
func (f *Formatter) PrintError(msg string) { fmt.Fprintf(f.w, "[ERROR] %s\n", msg) }
func (f *Formatter) PrintWarning(msg string) { fmt.Fprintf(f.errW, "[WARN] %s\n", msg) }
func (f *Formatter) PrintInfo(msg string) { fmt.Fprintf(f.w, "[INFO] %s\n", msg) }
func (f *Formatter) PrintProgress(msg string) { fmt.Fprintf(f.errW, "%s\n", msg) }
func (f *Formatter) PrintDim(msg string) { fmt.Fprintf(f.w, " %s\n", msg) }
// printJSONSafeInfo keeps command progress out of stdout when the caller
// requested JSON. A successful JSON command must leave stdout parseable; its
// human-readable progress belongs on stderr instead.
func printJSONSafeInfo(msg string) {
if deps != nil && deps.Caller != nil && strings.EqualFold(strings.TrimSpace(deps.Caller.Format()), "json") {
deps.Out.PrintProgress("[INFO] " + msg)
return
}
deps.Out.PrintInfo(msg)
}
func (f *Formatter) PrintKeyValue(key, value string) {
fmt.Fprintf(f.w, "%-16s%s\n", key+":", value)
}