Skip to content

Commit 0a898a5

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

File tree

4 files changed

+686
-138
lines changed

4 files changed

+686
-138
lines changed

extract_curl_args.py

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