internal/export/curl/curl.go

// Package curl synthesises a shell-quoted curl(1) invocation from a
// captured request, which is the single most-requested export format on
// the issue tracker.
//
// mercemay.top/src/httptap/
package curl

import (
	"fmt"
	"strings"

	"mercemay.top/httptap/internal/parser"
)

// Options configures the rendered command.
type Options struct {
	Insecure  bool // emit -k
	Compressed bool // emit --compressed
	Indent    string // per-line prefix, e.g. "  "; "" keeps it one line
}

// Default returns sensible defaults.
func Default() Options {
	return Options{Compressed: true, Indent: "  "}
}

// Render returns the curl command string for req.
func Render(req parser.Message, opts Options) string {
	method := parser.Method(req)
	target := parser.URL(req)
	var parts []string
	parts = append(parts, "curl")
	if method != "" && method != "GET" {
		parts = append(parts, "-X", method)
	}
	if opts.Insecure {
		parts = append(parts, "-k")
	}
	if opts.Compressed {
		parts = append(parts, "--compressed")
	}
	for _, kv := range req.Headers {
		if strings.EqualFold(kv[0], "Content-Length") {
			continue
		}
		parts = append(parts, "-H", shellQuote(kv[0]+": "+kv[1]))
	}
	if len(req.Body) > 0 {
		parts = append(parts, "--data-binary", shellQuote(string(req.Body)))
	}
	parts = append(parts, shellQuote(target))
	return joinCmd(parts, opts.Indent)
}

// shellQuote wraps s in single-quotes, escaping embedded quotes using
// the standard Bourne-shell trick.
func shellQuote(s string) string {
	if s == "" {
		return "''"
	}
	if !strings.ContainsAny(s, "\t\n\r '\"\\$`") {
		return s
	}
	return "'" + strings.ReplaceAll(s, "'", `'\''`) + "'"
}

// joinCmd renders parts either on a single line or wrapped with backslash
// continuations when indent is non-empty.
func joinCmd(parts []string, indent string) string {
	if indent == "" {
		return strings.Join(parts, " ")
	}
	var b strings.Builder
	for i, p := range parts {
		switch {
		case i == 0:
			b.WriteString(p)
		case p == "-X" || p == "-H" || p == "--data-binary" ||
			p == "-k" || p == "--compressed":
			b.WriteString(" \\\n")
			b.WriteString(indent)
			b.WriteString(p)
		default:
			b.WriteString(" ")
			b.WriteString(p)
		}
	}
	return b.String()
}

// RenderPretty is a shortcut that applies Default options for REPL usage.
func RenderPretty(req parser.Message) string {
	return fmt.Sprintf("%s", Render(req, Default()))
}