Skip to content

Commit a27f242

Browse files
committed
Add all current flags and implement opt shortening
1 parent 57203b1 commit a27f242

File tree

4 files changed

+667
-138
lines changed

4 files changed

+667
-138
lines changed

extract_curl_args.py

Lines changed: 221 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,221 @@
1+
#!/usr/bin/env python3
2+
#
3+
# This script assumes ../curl/ is a git repo containing curl's source code
4+
# and extracts the list of arguments curl accepts and writes the result as
5+
# two JS objects (one for --long-options and one for -s (short) options)
6+
# to curl-to-go.js.
7+
8+
from pathlib import Path
9+
import sys
10+
import subprocess
11+
from collections import Counter
12+
13+
# Git repo of curl's source code to extract the args from
14+
# TODO: make CURL_REPO and OUTPUT_FILE command line args?
15+
CURL_REPO = Path(__file__).parent / "../curl"
16+
INPUT_FILE = CURL_REPO / "src" / "tool_getparam.c"
17+
18+
OUTPUT_FILE = Path(__file__).parent / "resources/js/curl-to-go.js"
19+
20+
21+
JS_PARAMS_START = "BEGIN GENERATED CURL OPTIONS"
22+
JS_PARAMS_END = "END GENERATED CURL OPTIONS"
23+
24+
OPTS_START = "struct LongShort aliases[]= {"
25+
OPTS_END = "};"
26+
27+
BOOL_TYPES = ["bool", "none"]
28+
STR_TYPES = ["string", "filename"]
29+
ALIAS_TYPES = BOOL_TYPES + STR_TYPES
30+
31+
DUPES = {
32+
"krb": "krb",
33+
"krb4": "krb",
34+
"ftp-ssl": "ssl",
35+
"ssl": "ssl",
36+
"ftp-ssl-reqd": "ssl-reqd",
37+
"ssl-reqd": "ssl-reqd",
38+
"proxy-service-name": "proxy-service-name",
39+
"socks5-gssapi-service": "proxy-service-name",
40+
}
41+
42+
if not OUTPUT_FILE.is_file():
43+
sys.exit(
44+
f"{OUTPUT_FILE} doesn't exist. You should run this script from curl-to-go/"
45+
)
46+
if not CURL_REPO.is_dir():
47+
sys.exit(
48+
f"{CURL_REPO} needs to be a git repo with curl's source code. "
49+
"You can clone it with\n\n"
50+
"git clone https://github.com/curl/curl ../curl"
51+
# or modify the CURL_REPO variable above
52+
)
53+
54+
55+
def on_git_master(git_dir):
56+
curl_branch = subprocess.run(
57+
["git", "rev-parse", "--abbrev-ref", "HEAD"],
58+
cwd=git_dir,
59+
check=True,
60+
capture_output=True,
61+
text=True,
62+
).stdout.strip()
63+
return curl_branch == "master"
64+
65+
66+
def parse_aliases(lines):
67+
aliases = {}
68+
for line in lines:
69+
if OPTS_START in line:
70+
break
71+
for line in lines:
72+
line = line.strip()
73+
if line.endswith(OPTS_END):
74+
break
75+
if not line.strip().startswith("{"):
76+
continue
77+
78+
# main.c has comments on the same line
79+
letter, lname, desc = line.split("/*")[0].strip().strip("{},").split(",")
80+
81+
letter = letter.strip().strip('"')
82+
lname = lname.strip().strip('"')
83+
desc = desc.strip()
84+
85+
if 1 > len(letter) > 2:
86+
raise ValueError(f"letter form of --{lname} must be 1 or 2 characters long")
87+
88+
# Simplify "desc"
89+
alias_type = desc.removeprefix("ARG_").lower()
90+
if alias_type == "filename":
91+
alias_type = "string"
92+
if alias_type not in ALIAS_TYPES:
93+
raise ValueError(f"unknown desc: {desc!r}")
94+
# TODO: some "string" arguments should be "list"s
95+
96+
# Rename "desc" to "type"
97+
alias = {"letter": letter, "lname": lname, "type": alias_type}
98+
99+
if lname in aliases and aliases[lname] != alias:
100+
print(
101+
f"{lname!r} repeated with different values: {aliases[lname]} vs. {alias} ",
102+
file=sys.stderr,
103+
)
104+
105+
aliases[lname] = alias
106+
107+
return list(aliases.values())
108+
109+
110+
def fill_out_aliases(aliases):
111+
# If both --option and --other-option have "oO" (for example) as their `letter`,
112+
# add a "name" property with the main option's `lname`
113+
letter_count = Counter(a["letter"] for a in aliases)
114+
115+
# "ARB_BOOL"-type OPTIONs have a --no-OPTION counterpart
116+
no_aliases = []
117+
118+
for idx, alias in enumerate(aliases):
119+
if alias["type"] in BOOL_TYPES:
120+
without_no = alias["lname"].removeprefix("no-").removeprefix("disable-")
121+
if alias["lname"] != without_no:
122+
print(f"Assuming --{alias['lname']} is {without_no!r}", file=sys.stderr)
123+
alias["name"] = without_no
124+
125+
if letter_count[alias["letter"]] > 1:
126+
# Can raise KeyError
127+
# todo lname vs name? might need some get()s technically?
128+
candidate = DUPES[alias["lname"]]
129+
if alias["lname"] != candidate:
130+
# name, not lname
131+
alias["name"] = candidate
132+
133+
if alias["type"] == "bool":
134+
no_alias = {**alias, "lname": "no-" + alias["lname"]}
135+
if "name" not in no_alias:
136+
no_alias["name"] = alias["lname"]
137+
# --no-OPTION options cannot be shortened
138+
no_alias["expand"] = False
139+
no_aliases.append((idx, no_alias))
140+
elif alias["type"] == "none":
141+
# The none/bool distinction becomes irrelevant after the step above
142+
alias["type"] = "bool"
143+
144+
for i, (insert_loc, no_alias) in enumerate(no_aliases):
145+
# +1 so that --no-OPTION appears after --OPTION
146+
aliases.insert(insert_loc + i + 1, no_alias)
147+
148+
return aliases
149+
150+
151+
def split(aliases):
152+
long_args = {}
153+
short_args = {}
154+
for alias in aliases:
155+
long_args[alias["lname"]] = {
156+
k: v for k, v in alias.items() if k not in ["letter", "lname"]
157+
}
158+
if len(alias["letter"]) == 1:
159+
alias_name = alias.get("name", alias["lname"])
160+
if alias["letter"] == "N": # -N is short for --no-buffer
161+
alias_name = "no-" + alias_name
162+
short_args[alias["letter"]] = alias_name
163+
return long_args, short_args
164+
165+
166+
def format_as_js(d, var_name):
167+
yield f"\tvar {var_name} = {{"
168+
for top_key, opt in d.items():
169+
170+
def quote(key):
171+
return key if key.isalpha() else repr(key)
172+
173+
def val_to_js(val):
174+
if isinstance(val, str):
175+
return repr(val)
176+
if isinstance(val, bool):
177+
return str(val).lower()
178+
raise TypeError(f"can't convert values of type {type(val)} to JS")
179+
180+
if isinstance(opt, dict):
181+
vals = [f"{quote(k)}: {val_to_js(v)}" for k, v in opt.items()]
182+
yield f"\t\t{top_key!r}: {{{', '.join(vals)}}},"
183+
elif isinstance(opt, str):
184+
yield f"\t\t{top_key!r}: {val_to_js(opt)},"
185+
186+
yield "\t};"
187+
188+
189+
if __name__ == "__main__":
190+
if not on_git_master(CURL_REPO):
191+
sys.exit("not on curl repo's git master")
192+
193+
with open(INPUT_FILE) as f:
194+
aliases = fill_out_aliases(parse_aliases(f))
195+
long_args, short_args = split(aliases)
196+
197+
js_params_lines = list(format_as_js(long_args, "longOptions"))
198+
js_params_lines += [""] # separate by a newline
199+
js_params_lines += list(format_as_js(short_args, "shortOptions"))
200+
201+
new_lines = []
202+
with open(OUTPUT_FILE) as f:
203+
for line in f:
204+
new_lines.append(line)
205+
if JS_PARAMS_START in line:
206+
break
207+
else:
208+
raise ValueError(f"{'// ' + JS_PARAMS_START!r} not in {OUTPUT_FILE}")
209+
210+
new_lines += [l + "\n" for l in js_params_lines]
211+
for line in f:
212+
if JS_PARAMS_END in line:
213+
new_lines.append(line)
214+
break
215+
else:
216+
raise ValueError(f"{'// ' + JS_PARAMS_END!r} not in {OUTPUT_FILE}")
217+
for line in f:
218+
new_lines.append(line)
219+
220+
with open(OUTPUT_FILE, "w", newline="\n") as f:
221+
f.write("".join(new_lines))

index.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ <h1>curl-to-Go</h1>
2828
<h2>Instantly convert <a href="http://curl.haxx.se/">curl</a> commands to <a href="https://golang.org/">Go</a> code</h2>
2929

3030
<p>
31-
This tool turns a curl command into Go code. (To do the reverse, check out <a href="https://github.com/moul/http2curl">moul/http2curl</a>.) Currently, it knows the following options: -d/--data, -H/--header, -I/--head, -u/--user, --url, and -X/--request. It also understands JSON content types (see <a href="https://mholt.github.io/json-to-go">JSON-to-Go</a>). If the content type is application/x-www-form-urlencoded then it will convert the data to <a href="https://pkg.go.dev/net/url#Values">Values</a> (same as <a href="https://pkg.go.dev/net/http#Client.PostForm">PostForm</a>). Feel free to <a href="https://github.com/mholt/curl-to-go">contribute on GitHub</a>!
31+
This tool turns a curl command into Go code. (To do the reverse, check out <a href="https://github.com/moul/http2curl">moul/http2curl</a>.) Currently, it knows the following options: <code>-d</code>/<code>--data</code>, <code>-H</code>/<code>--header</code>, <code>-I</code>/<code>--head</code>, <code>-u</code>/<code>--user</code>, <code>--url</code>, and <code>-X</code>/<code>--request</code>. It also understands JSON content types (see <a href="https://mholt.github.io/json-to-go">JSON-to-Go</a>). If the content type is <code>application/x-www-form-urlencoded</code> then it will convert the data to <a href="https://pkg.go.dev/net/url#Values"><code>Values</code></a> (same as <a href="https://pkg.go.dev/net/http#Client.PostForm"><code>PostForm</code></a>). Feel free to <a href="https://github.com/mholt/curl-to-go">contribute on GitHub</a>!
3232
</p>
3333

3434
<p class="examples">
@@ -50,7 +50,7 @@ <h2>Instantly convert <a href="http://curl.haxx.se/">curl</a> commands to <a hre
5050
</main>
5151

5252
<p>
53-
Note: http.DefaultClient will follow redirects by default, whereas curl does not without the <code>--location</code> flag. Since reusing the HTTP client is good Go practice, this tool does not attempt to configure the HTTP client for you.
53+
Note: <code>http.DefaultClient</code> will follow redirects by default, whereas curl does not without the <code>--location</code> flag. Since reusing the HTTP client is good Go practice, this tool does not attempt to configure the HTTP client for you.
5454
</p>
5555

5656

resources/js/common.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ $(function()
9292

9393
// Fill in examples
9494
$('#example1').click(function() {
95-
$('#input').val('curl canhazip.com').keyup();
95+
$('#input').val('curl icanhazip.com').keyup();
9696
});
9797
$('#example2').click(function() {
9898
$('#input').val('curl https://api.example.com/surprise \\\n -u banana:coconuts \\\n -d "sample data"').keyup();

0 commit comments

Comments
 (0)