// output.go — 共用輸出:JSON(預設)與 table(人工抽查)。 // // 命令把結果包成 Values(欄位順序即輸出順序),再由 output 依格式輸出。 // 這個設計讓同一份結果可以同時餵 JSON 與 table,不需要各自重組。 package gitea import ( "encoding/json" "fmt" "io" "strings" ) // Values 是一組已排序的欄位值(依 Field 順序)。 type Values []any // Format 是輸出格式。 type Format int const ( // FormatJSON 輸出 JSON(預設)。 FormatJSON Format = iota // FormatTable 輸出對齊的文字表格。 FormatTable ) // ParseFormat 解析 --output 的值;不認得的值回傳錯誤。 func ParseFormat(s string) (Format, error) { switch strings.ToLower(strings.TrimSpace(s)) { case "", "json": return FormatJSON, nil case "table": return FormatTable, nil default: return FormatJSON, fmt.Errorf("invalid output format %q (want json or table)", s) } } // Field 描述 table 輸出的一欄。 type Field struct { // Name 是欄位名(JSON 鍵與 table 表頭)。 Name string // ValueFmt 是值的格式化函式;nil 時用預設(fmt.Sprint + JSON 引號字串)。 ValueFmt func(v any) string } // Printer 依格式輸出結果。 type Printer struct { // Format 是輸出格式。 Format Format // Out 是輸出目標。 Out io.Writer } // PrintList 輸出清單。fields 定義欄位;rows 是每一列的欄位值。 // JSON 模式輸出物件陣列;table 模式輸出對齊表格。 func (p Printer) PrintList(fields []Field, rows []Values) error { if p.Format == FormatTable { return p.printTable(fields, rows) } w := p.Out if len(rows) == 0 { _, err := fmt.Fprintln(w, "[]") return err } if _, err := fmt.Fprint(w, "["); err != nil { return err } for i, row := range rows { if i > 0 { if _, err := fmt.Fprint(w, ","); err != nil { return err } } b, err := json.Marshal(mapOf(fields, row)) if err != nil { return err } if _, err := w.Write(b); err != nil { return err } } _, err := fmt.Fprintln(w, "]") return err } // PrintOne 輸出單一結果(物件);v 已是可 JSON 化的值。 // table 模式輸出 key: value 行。 func (p Printer) PrintOne(fields []Field, row Values) error { if p.Format == FormatTable { return p.printTable(fields, []Values{row}) } b, err := json.Marshal(mapOf(fields, row)) if err != nil { return err } _, err = fmt.Fprintln(p.Out, string(b)) return err } // PrintNull 輸出「沒有結果」的 JSON null(table 模式輸出提示行)。 func (p Printer) PrintNull() error { if p.Format == FormatTable { _, err := fmt.Fprintln(p.Out, "(none)") return err } _, err := fmt.Fprintln(p.Out, "null") return err } // printTable 印出對齊表格:表頭、分隔線、資料列。 func (p Printer) printTable(fields []Field, rows []Values) error { headers := make([]string, len(fields)) cells := make([][]string, len(rows)) for i, f := range fields { headers[i] = f.Name } for r, row := range rows { cells[r] = make([]string, len(fields)) for i := range fields { cells[r][i] = formatValue(fields[i], row[i]) } } widths := make([]int, len(fields)) for i, h := range headers { widths[i] = runeLen(h) } for _, row := range cells { for i, c := range row { if l := runeLen(c); l > widths[i] { widths[i] = l } } } var b strings.Builder for i, h := range headers { fmt.Fprintf(&b, "%-*s", widths[i], h) if i < len(headers)-1 { b.WriteString(" ") } } b.WriteString("\n") for i := range fields { fmt.Fprintf(&b, "%s", strings.Repeat("─", widths[i])) if i < len(fields)-1 { b.WriteString(" ") } } b.WriteString("\n") for _, row := range cells { for i, c := range row { fmt.Fprintf(&b, "%-*s", widths[i], c) if i < len(fields)-1 { b.WriteString(" ") _ = i } } b.WriteString("\n") } _, err := fmt.Fprint(p.Out, b.String()) return err } // mapOf 把欄位與值組成 JSON 物件用的 map(保留欄位順序由 encoding/json // 處理:map 會排序鍵,因此改用自組 slice 結構以保順序——見 marshalObject)。 func mapOf(fields []Field, row Values) any { if len(row) != len(fields) { return nil } return orderedObject{fields, row} } // orderedObject 以欄位順序輸出 JSON 物件(避免 map 排序破壞欄位序)。 type orderedObject struct { fields []Field row Values } // MarshalJSON 按欄位順序輸出。 func (o orderedObject) MarshalJSON() ([]byte, error) { var b strings.Builder b.WriteByte('{') for i, f := range o.fields { if i > 0 { b.WriteByte(',') } k, err := json.Marshal(f.Name) if err != nil { return nil, err } b.Write(k) b.WriteByte(':') v, err := json.Marshal(o.row[i]) if err != nil { return nil, err } b.Write(v) } b.WriteByte('}') return []byte(b.String()), nil } // formatValue 把欄位值格式化為 table 用字串;nil → 空字串。 func formatValue(f Field, v any) string { if v == nil { return "" } if f.ValueFmt != nil { return f.ValueFmt(v) } switch x := v.(type) { case string: return x case fmt.Stringer: return x.String() default: return fmt.Sprint(x) } } // runeLen 回傳字串的 rune 數(表格寬度用,CJK 後續可再調)。 func runeLen(s string) int { n := 0 for range s { n++ } return n }