UNPKG

curlconverter

Version:

convert curl commands to Python, JavaScript, Go, PHP and more

270 lines (249 loc) 8.15 kB
import { CCError } from "../utils.js"; import { Word } from "../shell/Word.js"; import { parse, getFirst, COMMON_SUPPORTED_ARGS } from "../parse.js"; import type { Request, Warnings } from "../parse.js"; import { reprStr as pyreprStr } from "./python/python.js"; export const supportedArgs = new Set([ ...COMMON_SUPPORTED_ARGS, "insecure", "no-insecure", "compressed", "no-compressed", "max-time", "form", "form-string", ]); const IF_ERR = "\tif err != nil {\n" + "\t\tlog.Fatal(err)\n" + "\t}\n"; // https://go.dev/ref/spec#String_literals function reprMaybeBacktick(s: Word, vars: Vars, imports: Set<string>): string { return s.isString() && s.includes('"') ? reprBacktick(s, vars, imports) : repr(s, vars, imports); } function reprBacktick(s: Word, vars: Vars, imports: Set<string>): string { return s.isString() && !s.includes("`") && !s.includes("\r") ? "`" + s.toString() + "`" : repr(s, vars, imports); } function reprStr(s: string): string { return pyreprStr(s, '"'); } type Vars = { [key: string]: string }; function repr(w: Word, vars: Vars, imports: Set<string>): string { const args: string[] = []; for (const t of w.tokens) { if (typeof t === "string") { args.push(reprStr(t)); } else if (t.type === "variable") { args.push("os.Getenv(" + reprStr(t.value) + ")"); imports.add("os"); } else { // TODO: Command takes a list of arguments .Command("ls", "-l", "-a") // TODO: if there's only one command, it would be nice to name the variable "cmd" const execCall = "exec.Command(" + reprStr(t.value) + ").Output()"; let i = 1; let varName = "cmd" + i; // We need to check because we often try to represent the same // token twice and discard one of the attempts. // This is linear time but hopefully there's not that many subcommands. while (varName in vars && vars[varName] !== execCall) { i++; varName = "cmd" + i; if (i > Number.MAX_SAFE_INTEGER) { throw new CCError("lol"); } } vars[varName] = execCall; args.push(varName); imports.add("os/exec"); } } return args.join(" + "); } function timeoutAtoi(w: Word, vars: Vars, imports: Set<string>): string { if (w.isString()) { const asStr = w.toString(); // TODO: check using curl's syntax and convert to Go's float syntax if (/^\d+\.?\d*$/.test(asStr)) { return asStr; } } vars["timeout"] = "strconv.Atoi(" + repr(w, vars, imports) + ")"; imports.add("strconv"); return "timeout"; } export function _toGo(requests: Request[], warnings: Warnings = []): string { const request = getFirst(requests, warnings); const imports = new Set<string>(["fmt", "io", "log", "net/http"]); const vars: Vars = {}; let goCode = ""; if (request.multipartUploads) { goCode += "\tform := new(bytes.Buffer)\n"; goCode += "\twriter := multipart.NewWriter(form)\n"; imports.add("bytes"); imports.add("mime/multipart"); let firstFile = true; let firstField = true; for (const m of request.multipartUploads) { if ("contentFile" in m) { const op = firstFile ? ":=" : "="; firstFile = false; // TODO: Go sends name=<filename> but curl always sends name="data" goCode += `\tfw, err ${op} writer.CreateFormFile(${repr( m.name, vars, imports, )}, filepath.Base(${repr( m.filename ?? m.contentFile, vars, imports, )}))\n`; goCode += IF_ERR; imports.add("path/filepath"); goCode += `\tfd, err ${op} os.Open(${repr( m.contentFile, vars, imports, )})\n`; goCode += IF_ERR; imports.add("os"); goCode += "\tdefer fd.Close()\n"; goCode += "\t_, err = io.Copy(fw, fd)\n"; goCode += IF_ERR; } else { const op = firstField ? ":=" : "="; firstField = false; goCode += `\tformField, err ${op} writer.CreateFormField(${repr( m.name, vars, imports, )})\n`; goCode += IF_ERR; goCode += `\t_, err = formField.Write([]byte(${reprMaybeBacktick( m.content, vars, imports, )}))\n`; } goCode += "\n"; } goCode += "\twriter.Close()\n"; goCode += "\n"; request.headers.delete("content-type"); } if (request.insecure || request.compressed === false) { goCode += "\ttr := &http.Transport{\n"; if (request.insecure) { goCode += "\t\tTLSClientConfig: &tls.Config{InsecureSkipVerify: true},\n"; imports.add("crypto/tls"); } if (request.compressed === false) { goCode += "\t\tDisableCompression: true,\n"; } goCode += "\t}\n"; } goCode += "\tclient := &http.Client{"; if (request.timeout) { goCode += "\n"; if (request.insecure || request.compressed === false) { goCode += "\t\tTransport: tr,\n"; } goCode += "\t\tTimeout: " + timeoutAtoi(request.timeout, vars, imports) + " * time.Second,\n"; goCode += "\t"; imports.add("time"); } else if (request.insecure || request.compressed === false) { goCode += "Transport: tr"; } goCode += "}\n"; if (request.data && !request.multipartUploads) { goCode += "\tvar data = strings.NewReader(" + reprBacktick(request.data, vars, imports) + ")\n"; imports.add("strings"); } goCode += "\treq, err := http.NewRequest(" + repr(request.urls[0].method, vars, imports) + ", " + repr(request.urls[0].url, vars, imports); goCode += ", " + (request.multipartUploads ? "form" : request.data ? "data" : "nil") + ")\n"; goCode += IF_ERR; if (request.headers.length) { for (const [headerName, headerValue] of request.headers) { let start = "\t"; if ( headerName.toLowerCase().toString() === "accept-encoding" && // By default Go will automatically decompress gzip, // unless you set DisableCompression to true on the Transport // or pass a custom Accept-Encoding header. // By default curl won't automatically decompress gzip unless // you pass --compressed, but we comment out the header in that // case (request.compressed = undefined) too. request.compressed !== false ) { start += "// "; } goCode += start + "req.Header.Set(" + repr(headerName, vars, imports) + ", " + reprMaybeBacktick(headerValue || new Word(), vars, imports) + ")\n"; } } if (request.multipartUploads) { goCode += '\treq.Header.Set("Content-Type", writer.FormDataContentType())\n'; } if (request.urls[0].auth && request.authType === "basic") { const [user, password] = request.urls[0].auth; goCode += "\treq.SetBasicAuth(" + repr(user, vars, imports) + ", " + repr(password, vars, imports) + ")\n"; } goCode += "\tresp, err := client.Do(req)\n"; goCode += IF_ERR; goCode += "\tdefer resp.Body.Close()\n"; goCode += "\tbodyText, err := io.ReadAll(resp.Body)\n"; goCode += IF_ERR; goCode += '\tfmt.Printf("%s\\n", bodyText)\n'; goCode += "}"; let preamble = "package main\n\n"; preamble += "import (\n"; for (const imp of Array.from(imports).sort()) { preamble += '\t"' + imp + '"\n'; } preamble += ")\n\n"; preamble += "func main() {\n"; // TODO: sorts wrong when >9 commands for (const [name, expr] of Array.from(Object.entries(vars)).sort()) { preamble += "\t" + name + ", err := " + expr + "\n"; preamble += IF_ERR; } if (Object.values(vars).length) { preamble += "\n"; } return preamble + goCode + "\n"; } export function toGoWarn( curlCommand: string | string[], warnings: Warnings = [], ): [string, Warnings] { const requests = parse(curlCommand, supportedArgs, warnings); const go = _toGo(requests, warnings); return [go, warnings]; } export function toGo(curlCommand: string | string[]): string { return toGoWarn(curlCommand)[0]; }