From d4e85844a4168dd9d3d7e1977e0dfa447db5c642 Mon Sep 17 00:00:00 2001 From: Justin Lin Date: Fri, 25 Jul 2025 13:19:06 +1000 Subject: [PATCH] feat: add raw func and allow using funcs in output name --- examples/hledger_monthly.md | 45 +++++++++++++++++++ examples/system-info.md | 29 ++++++++++++ main.go | 8 ++-- render.go | 88 ++++++++++++++++++++++++++++++++++--- types.go | 50 --------------------- 5 files changed, 158 insertions(+), 62 deletions(-) create mode 100644 examples/hledger_monthly.md create mode 100644 examples/system-info.md diff --git a/examples/hledger_monthly.md b/examples/hledger_monthly.md new file mode 100644 index 0000000..1e4ebd5 --- /dev/null +++ b/examples/hledger_monthly.md @@ -0,0 +1,45 @@ +--- +output: "monthly_report_{{.b}}.md" +params: + b: + required: true + default: 2011-01 + usage: | + Required. + The begin time of report. + Examples: + 2025 + 2025-07 + 2025-07-15 + Jul + e: + required: true + default: 2011-02 + usage: | + Required. + The end time of report. + Same format as -b + f: + default: examples/ledger.j + usage: The ledger file to parse +--- + +# Finance Report from {{.b}} to {{.e}} + +## Previous status: + +{{ shell "hledger -f $f balancesheet -e $b" }} + +## Expenses: + +### Expense Summary by Category + +{{ shell "hledger -f $f bal expenses -b $b -e $e --depth 3 --sort amount --budget" }} + +## Net Income: + +{{shell "hledger -f $f income -b $b -e $e --monthly"}} + +## Ending balance: + +{{shell "hledger -f $f balancesheet -e $e"}} diff --git a/examples/system-info.md b/examples/system-info.md new file mode 100644 index 0000000..fa01ad3 --- /dev/null +++ b/examples/system-info.md @@ -0,0 +1,29 @@ +--- +output: sys-report-{{ raw "date --iso-8601" }}.md +--- + +# 💥 System Information Report for {{ sh "hostname" }} + +* **Hostname**: {{ sh "hostname" }} +* **Username**: {{ sh "whoami" }} +* **Uptime**: {{ sh "uptime -p" }} +* **System**: {{ sh "uname -a" }} +* **CPU**: {{ sh "uname -m" }} — {{ sh "nproc" }} cores +* **IP Address**: {{ sh "hostname -I || ip a | grep inet" }} +* **Default Gateway**: {{ sh "ip route | grep default || netstat -rn | grep default" }} + +--- + +## 🧠 Memory Usage + +{{ shell "free -h" }} + +--- + +## 📉 Disk Usage + +{{ shell "df -h /" }} + +--- + +Generated on: {{ sh "date" }} diff --git a/main.go b/main.go index 88894fc..576183a 100644 --- a/main.go +++ b/main.go @@ -33,14 +33,12 @@ func main() { ctx, err := parseParams(templateMeta) handleError(err) - data := render(body, ctx) - output, err := templateMeta.OutputFile(ctx) + output, err := ctx.OutputFile(templateMeta) handleError(err) + defer output.Close() - err = os.WriteFile(output, data, 0644) + err = ctx.render(body, output) handleError(err) - - fmt.Printf("Result saved to %s\n", output) } func loadTemplate() (string, error) { diff --git a/render.go b/render.go index 88f51d8..b07f6c3 100644 --- a/render.go +++ b/render.go @@ -2,22 +2,96 @@ package main import ( "bytes" + "errors" + "fmt" + "io" + "os" + "os/exec" + "strings" "text/template" ) -func render(tmpl string, ctx TemplateContext) []byte { - funcMap := template.FuncMap{ +func (ctx TemplateContext) run(command string) (string, error) { + cmd := exec.Command("sh", "-c", command) + var stdout bytes.Buffer + cmd.Stdout = &stdout + cmd.Stderr = &stdout + for k, v := range ctx { + cmd.Env = append(cmd.Env, k+"="+v) + } + err := cmd.Run() + if err != nil { + return "", errors.New(stdout.String()) + } + return stdout.String(), nil +} + +func (ctx TemplateContext) raw(command string) string { + out, err := ctx.run(command) + if err != nil { + fmt.Println(err) + return "error" + } + return strings.TrimSpace(out) +} + +func (ctx TemplateContext) codeBlock(command string) string { + out, err := ctx.run(command) + if err != nil { + return fmt.Sprintf("> Failed to execute the command:\n> `%s`\n> %s\n", command, err.Error()) + } + return fmt.Sprintf("\n```\n%s\n```\n", out) +} + +func (ctx TemplateContext) inline(command string) string { + out, err := ctx.run(command) + if err != nil { + return fmt.Sprintf("`Failed to execute the command: '%s'. %s`", command, err.Error()) + } + return fmt.Sprintf("`%s`", strings.Trim(out, "\n ")) +} + +func (ctx TemplateContext) funcMap() template.FuncMap { + return template.FuncMap{ "sh": ctx.inline, "shell": ctx.codeBlock, + "raw": ctx.raw, } - t, err := template.New("foo").Funcs(funcMap).Parse(tmpl) +} + +func (ctx TemplateContext) render(tmpl string, output io.Writer) error { + t, err := template.New("foo").Funcs(ctx.funcMap()).Parse(tmpl) if err != nil { - panic(err) + return err + } + err = t.Execute(output, ctx) + if err != nil { + return err + } + return nil +} + +func (ctx TemplateContext) OutputFile(meta TemplateMeta) (output io.WriteCloser, err error) { + if meta.Output == "" { + return os.Stdout, nil + } + tmpl, err := template.New("output").Funcs(ctx.funcMap()).Parse(meta.Output) + if err != nil { + return nil, fmt.Errorf("Failed to parse output name: %w", err) } var buf bytes.Buffer - err = t.Execute(&buf, ctx) + err = tmpl.Execute(&buf, ctx) if err != nil { - panic(err) + return nil, fmt.Errorf("Failed to execute output name: %w", err) } - return buf.Bytes() + + filename := buf.String() + + fmt.Printf("Saving result to %s\n", filename) + + file, err := os.OpenFile(filename, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644) + if err != nil { + return nil, fmt.Errorf("Failed to open the file: %w", err) + } + return file, nil } diff --git a/types.go b/types.go index 81abd7f..0fa3a6d 100644 --- a/types.go +++ b/types.go @@ -1,13 +1,7 @@ package main import ( - "bytes" "encoding/json" - "errors" - "fmt" - "os/exec" - "strings" - "text/template" ) type TemplateMeta struct { @@ -23,19 +17,6 @@ func (meta TemplateMeta) String() string { return string(data) } -func (meta TemplateMeta) OutputFile(ctx TemplateContext) (output string, err error) { - tmpl, err := template.New("output").Parse(meta.Output) - if err != nil { - return "", fmt.Errorf("Failed to parse output name: %w", err) - } - var buf bytes.Buffer - err = tmpl.Execute(&buf, ctx) - if err != nil { - return "", fmt.Errorf("Failed to execute output name: %w", err) - } - return buf.String(), nil -} - type ParamSpec struct { Required bool `yaml:"required"` Default string `yaml:"default"` @@ -43,34 +24,3 @@ type ParamSpec struct { } type TemplateContext map[string]string - -func (ctx TemplateContext) run(command string) (string, error) { - cmd := exec.Command("sh", "-c", command) - var stdout bytes.Buffer - cmd.Stdout = &stdout - cmd.Stderr = &stdout - for k, v := range ctx { - cmd.Env = append(cmd.Env, k+"="+v) - } - err := cmd.Run() - if err != nil { - return "", errors.New(stdout.String()) - } - return stdout.String(), nil -} - -func (ctx TemplateContext) codeBlock(command string) string { - out, err := ctx.run(command) - if err != nil { - return fmt.Sprintf("> Failed to execute the command:\n> `%s`\n> %s\n", command, err.Error()) - } - return fmt.Sprintf("\n```\n%s\n```\n", out) -} - -func (ctx TemplateContext) inline(command string) string { - out, err := ctx.run(command) - if err != nil { - return fmt.Sprintf("`Failed to execute the command: '%s'. %s`", command, err.Error()) - } - return fmt.Sprintf("`%s`", strings.Trim(out, "\n ")) -}