feat: add docfill mvp cli
This commit is contained in:
96
cmd/docfill/main.go
Normal file
96
cmd/docfill/main.go
Normal file
@@ -0,0 +1,96 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"xlsdoc/internal/docfill"
|
||||
)
|
||||
|
||||
type varFlags map[string]string
|
||||
|
||||
func main() {
|
||||
os.Exit(run(os.Args[1:], os.Stdout, os.Stderr))
|
||||
}
|
||||
|
||||
func run(args []string, stdout, stderr io.Writer) int {
|
||||
if len(args) == 0 {
|
||||
fmt.Fprintln(stderr, "docfill: missing command: run")
|
||||
return 2
|
||||
}
|
||||
if args[0] != "run" {
|
||||
fmt.Fprintf(stderr, "docfill: unknown command: %s\n", args[0])
|
||||
return 2
|
||||
}
|
||||
|
||||
result, code := runFill(args[1:], stderr)
|
||||
if code != 0 {
|
||||
return code
|
||||
}
|
||||
|
||||
fmt.Fprintf(stdout, "generated %d file(s)\n", len(result.Files))
|
||||
for _, file := range result.Files {
|
||||
fmt.Fprintf(stdout, "- %s\n", file)
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func runFill(args []string, stderr io.Writer) (docfill.RunResult, int) {
|
||||
fs := flag.NewFlagSet("docfill run", flag.ContinueOnError)
|
||||
fs.SetOutput(stderr)
|
||||
|
||||
var configPath string
|
||||
var vars varFlags = make(map[string]string)
|
||||
fs.SetOutput(io.Discard)
|
||||
fs.StringVar(&configPath, "config", "", "path to task json")
|
||||
fs.StringVar(&configPath, "c", "", "path to task json")
|
||||
fs.Var(vars, "var", "template variable as key=value; repeatable")
|
||||
|
||||
if err := fs.Parse(args); err != nil {
|
||||
fmt.Fprintf(stderr, "docfill: %v\n", err)
|
||||
return docfill.RunResult{}, 2
|
||||
}
|
||||
if fs.NArg() > 0 {
|
||||
fmt.Fprintf(stderr, "docfill: unexpected argument: %s\n", fs.Arg(0))
|
||||
return docfill.RunResult{}, 2
|
||||
}
|
||||
if vars == nil {
|
||||
vars = make(map[string]string)
|
||||
}
|
||||
if configPath == "" {
|
||||
fmt.Fprintln(stderr, "docfill: missing --config")
|
||||
fs.SetOutput(stderr)
|
||||
fs.Usage()
|
||||
return docfill.RunResult{}, 2
|
||||
}
|
||||
|
||||
result, err := docfill.RunWithOptions(configPath, docfill.RunOptions{Vars: vars})
|
||||
if err != nil {
|
||||
fmt.Fprintf(stderr, "docfill: %v\n", err)
|
||||
return docfill.RunResult{}, 1
|
||||
}
|
||||
return result, 0
|
||||
}
|
||||
|
||||
func (v varFlags) String() string {
|
||||
if len(v) == 0 {
|
||||
return ""
|
||||
}
|
||||
parts := make([]string, 0, len(v))
|
||||
for key, value := range v {
|
||||
parts = append(parts, key+"="+value)
|
||||
}
|
||||
return strings.Join(parts, ",")
|
||||
}
|
||||
|
||||
func (v varFlags) Set(value string) error {
|
||||
key, val, ok := strings.Cut(value, "=")
|
||||
if !ok || strings.TrimSpace(key) == "" {
|
||||
return fmt.Errorf("invalid --var %q, want key=value", value)
|
||||
}
|
||||
v[strings.TrimSpace(key)] = val
|
||||
return nil
|
||||
}
|
||||
125
cmd/docfill/main_test.go
Normal file
125
cmd/docfill/main_test.go
Normal file
@@ -0,0 +1,125 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/xuri/excelize/v2"
|
||||
)
|
||||
|
||||
func TestRunCommandAcceptsShortConfigVarsAndPrintsSummary(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
dataPath := filepath.Join(dir, "data.xlsx")
|
||||
templatePath := filepath.Join(dir, "template.xlsx")
|
||||
configPath := filepath.Join(dir, "task.json")
|
||||
|
||||
writeCLIWorkbook(t, dataPath, "Daily", [][]string{
|
||||
{"name", "done"},
|
||||
{"张三", "完成日报"},
|
||||
})
|
||||
writeCLIWorkbook(t, templatePath, "日报", [][]string{
|
||||
{"批次", "{{batch}}"},
|
||||
{"员工", "{{name}}"},
|
||||
{"完成", "{{done}}"},
|
||||
})
|
||||
writeCLIJSON(t, configPath, map[string]any{
|
||||
"data": map[string]any{
|
||||
"type": "excel",
|
||||
"path": dataPath,
|
||||
"sheet": "Daily",
|
||||
},
|
||||
"template": map[string]any{
|
||||
"path": templatePath,
|
||||
},
|
||||
"output": map[string]any{
|
||||
"path": filepath.Join(dir, "out", "日报_{{batch}}_{{name}}.xlsx"),
|
||||
},
|
||||
})
|
||||
|
||||
var stdout bytes.Buffer
|
||||
var stderr bytes.Buffer
|
||||
exitCode := run([]string{"run", "-c", configPath, "--var", "batch=早班"}, &stdout, &stderr)
|
||||
|
||||
if exitCode != 0 {
|
||||
t.Fatalf("exit code = %d, stderr=%q", exitCode, stderr.String())
|
||||
}
|
||||
if stderr.Len() != 0 {
|
||||
t.Fatalf("expected empty stderr, got %q", stderr.String())
|
||||
}
|
||||
if !strings.Contains(stdout.String(), "generated 1 file(s)") || !strings.Contains(stdout.String(), "日报_早班_张三.xlsx") {
|
||||
t.Fatalf("unexpected stdout: %q", stdout.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunCommandRejectsBadVarSyntax(t *testing.T) {
|
||||
var stdout bytes.Buffer
|
||||
var stderr bytes.Buffer
|
||||
exitCode := run([]string{"run", "-c", "task.json", "--var", "badvar"}, &stdout, &stderr)
|
||||
|
||||
if exitCode != 2 {
|
||||
t.Fatalf("exit code = %d, want 2", exitCode)
|
||||
}
|
||||
if !strings.Contains(stderr.String(), "docfill:") || !strings.Contains(stderr.String(), "invalid --var") {
|
||||
t.Fatalf("unexpected stderr: %q", stderr.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunCommandRejectsUnexpectedArgs(t *testing.T) {
|
||||
var stdout bytes.Buffer
|
||||
var stderr bytes.Buffer
|
||||
exitCode := run([]string{"run", "-c", "task.json", "extra"}, &stdout, &stderr)
|
||||
|
||||
if exitCode != 2 {
|
||||
t.Fatalf("exit code = %d, want 2", exitCode)
|
||||
}
|
||||
if stdout.Len() != 0 {
|
||||
t.Fatalf("expected empty stdout, got %q", stdout.String())
|
||||
}
|
||||
if !strings.Contains(stderr.String(), "unexpected argument") {
|
||||
t.Fatalf("unexpected stderr: %q", stderr.String())
|
||||
}
|
||||
}
|
||||
|
||||
func writeCLIWorkbook(t *testing.T, path, sheet string, rows [][]string) {
|
||||
t.Helper()
|
||||
|
||||
f := excelize.NewFile()
|
||||
defer f.Close()
|
||||
|
||||
defaultSheet := f.GetSheetName(0)
|
||||
if defaultSheet != sheet {
|
||||
if err := f.SetSheetName(defaultSheet, sheet); err != nil {
|
||||
t.Fatalf("set sheet name: %v", err)
|
||||
}
|
||||
}
|
||||
for r, row := range rows {
|
||||
for c, value := range row {
|
||||
cell, err := excelize.CoordinatesToCellName(c+1, r+1)
|
||||
if err != nil {
|
||||
t.Fatalf("cell name: %v", err)
|
||||
}
|
||||
if err := f.SetCellValue(sheet, cell, value); err != nil {
|
||||
t.Fatalf("set cell: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
if err := f.SaveAs(path); err != nil {
|
||||
t.Fatalf("save workbook: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func writeCLIJSON(t *testing.T, path string, value any) {
|
||||
t.Helper()
|
||||
|
||||
data, err := json.MarshalIndent(value, "", " ")
|
||||
if err != nil {
|
||||
t.Fatalf("marshal json: %v", err)
|
||||
}
|
||||
if err := os.WriteFile(path, data, 0o644); err != nil {
|
||||
t.Fatalf("write json: %v", err)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user