feat: introduce curl --path-as-is option

This commit is contained in:
Tim Eggert 2023-06-20 22:19:22 +02:00 committed by Tim Eggert
parent 1c1b7dd8af
commit 34137c0e0d
No known key found for this signature in database
GPG Key ID: D8AE8A94EE30B48F
13 changed files with 113 additions and 0 deletions

View File

@ -171,6 +171,7 @@ will follow a redirection only for the second entry.
| <a href="#no-output" id="no-output"><code>--no-output</code></a> | Suppress output. By default, Hurl outputs the body of the last response.<br> |
| <a href="#noproxy" id="noproxy"><code>--noproxy &lt;HOST(S)&gt;</code></a> | Comma-separated list of hosts which do not use a proxy.<br>Override value from Environment variable no_proxy.<br> |
| <a href="#output" id="output"><code>-o, --output &lt;FILE&gt;</code></a> | Write output to FILE instead of stdout.<br> |
| <a href="#path-as-is" id="path-as-is"><code>--path-as-is</code></a> | Tell curl to not handle sequences of /../ or /./ in the given URL path. Normally curl will squash or merge them according to standards but with this option set you tell it not to do that.<br> |
| <a href="#proxy" id="proxy"><code>-x, --proxy &lt;[PROTOCOL://]HOST[:PORT]&gt;</code></a> | Use the specified proxy.<br> |
| <a href="#report-junit" id="report-junit"><code>--report-junit &lt;FILE&gt;</code></a> | Generate JUnit File.<br><br>If the FILE report already exists, it will be updated with the new test results.<br> |
| <a href="#report-html" id="report-html"><code>--report-html &lt;DIR&gt;</code></a> | Generate HTML report in DIR.<br><br>If the HTML report already exists, it will be updated with the new test results.<br> |
@ -224,3 +225,4 @@ Using an environment variable to set the proxy has the same effect as using the
curl(1) hurlfmt(1)

View File

@ -640,6 +640,7 @@ compressed: true # request a compressed response
insecure: true # allows insecure SSL connections and transfers
location: true # follow redirection for this request
max-redirs: 10 # maximum number of redirections
path-as-is: true # tell curl to not handle sequences of /../ or /./ in the given URL path
variable: country=Italy # define variable country
variable: planet=Earth # define variable planet
verbose: true # allow verbose output

View File

@ -239,6 +239,13 @@ pub fn output() -> clap::Arg {
.num_args(1)
}
pub fn path_as_is() -> clap::Arg {
clap::Arg::new("path_as_is")
.long("path_as_is")
.help("Tell curl to not handle sequences of /../ or /./ in the given URL path")
.action(ArgAction::SetTrue)
}
pub fn proxy() -> clap::Arg {
clap::Arg::new("proxy")
.short('x')

View File

@ -227,6 +227,10 @@ pub fn output_type(arg_matches: &ArgMatches) -> OutputType {
}
}
pub fn path_as_is(arg_matches: &ArgMatches) -> bool {
has_flag(arg_matches, "path_as_is")
}
pub fn progress_bar(arg_matches: &ArgMatches) -> bool {
let verbose = verbose(arg_matches) || very_verbose(arg_matches);
test(arg_matches)

View File

@ -60,6 +60,7 @@ pub struct Options {
pub no_proxy: Option<String>,
pub output: Option<String>,
pub output_type: OutputType,
pub path_as_is: bool,
pub progress_bar: bool,
pub proxy: Option<String>,
pub resolves: Vec<String>,
@ -148,6 +149,7 @@ pub fn parse() -> Result<Options, OptionsError> {
.arg(commands::no_output())
.arg(commands::noproxy())
.arg(commands::output())
.arg(commands::path_as_is())
.arg(commands::proxy())
.arg(commands::report_html())
.arg(commands::report_junit())
@ -206,6 +208,7 @@ fn parse_matches(arg_matches: &ArgMatches) -> Result<Options, OptionsError> {
let max_redirect = matches::max_redirect(arg_matches);
let no_proxy = matches::no_proxy(arg_matches);
let progress_bar = matches::progress_bar(arg_matches);
let path_as_is = matches::path_as_is(arg_matches);
let proxy = matches::proxy(arg_matches);
let output = matches::output(arg_matches);
let output_type = matches::output_type(arg_matches);
@ -244,6 +247,7 @@ fn parse_matches(arg_matches: &ArgMatches) -> Result<Options, OptionsError> {
junit_file,
max_redirect,
no_proxy,
path_as_is,
progress_bar,
proxy,
output,
@ -279,6 +283,7 @@ impl Options {
let follow_location = self.follow_location;
let insecure = self.insecure;
let max_redirect = self.max_redirect;
let path_as_is = self.path_as_is.clone();
let proxy = self.proxy.clone();
let no_proxy = self.no_proxy.clone();
let cookie_input_file = self.cookie_input_file.clone();
@ -332,6 +337,7 @@ impl Options {
.insecure(insecure)
.max_redirect(max_redirect)
.no_proxy(no_proxy)
.path_as_is(path_as_is)
.post_entry(post_entry)
.pre_entry(pre_entry)
.proxy(proxy)

View File

@ -149,6 +149,7 @@ impl Client {
self.handle.ssl_key(client_key_file).unwrap();
self.handle.ssl_cert_type("PEM").unwrap();
}
self.handle.path_as_is(options.path_as_is).unwrap();
if let Some(proxy) = options.proxy.clone() {
self.handle.proxy(proxy.as_str()).unwrap();
}

View File

@ -31,6 +31,7 @@ pub struct ClientOptions {
pub insecure: bool,
pub max_redirect: Option<usize>,
pub no_proxy: Option<String>,
pub path_as_is: bool,
pub proxy: Option<String>,
pub resolves: Vec<String>,
pub retry: Retry,
@ -61,6 +62,7 @@ impl Default for ClientOptions {
insecure: false,
max_redirect: Some(50),
no_proxy: None,
path_as_is: false,
proxy: None,
resolves: vec![],
retry: Retry::None,
@ -119,6 +121,9 @@ impl ClientOptions {
arguments.push("--max-redirs".to_string());
arguments.push(max_redirect.to_string());
}
if self.path_as_is {
arguments.push("--path-as-is".to_string());
}
if let Some(ref proxy) = self.proxy {
arguments.push("--proxy".to_string());
arguments.push(format!("'{proxy}'"));
@ -160,6 +165,7 @@ mod tests {
follow_location: true,
max_redirect: Some(10),
cookie_input_file: Some("cookie_file".to_string()),
path_as_is: true,
proxy: Some("localhost:3128".to_string()),
no_proxy: None,
verbosity: None,
@ -189,6 +195,7 @@ mod tests {
"--location".to_string(),
"--max-redirs".to_string(),
"10".to_string(),
"--path-as-is".to_string(),
"--proxy".to_string(),
"'localhost:3128'".to_string(),
"--resolve".to_string(),

View File

@ -227,6 +227,7 @@ impl ClientOptions {
follow_location: runner_options.follow_location,
max_redirect: runner_options.max_redirect,
cookie_input_file: runner_options.cookie_input_file.clone(),
path_as_is: runner_options.path_as_is,
proxy: runner_options.proxy.clone(),
no_proxy: runner_options.no_proxy.clone(),
verbosity: match verbosity {
@ -334,6 +335,10 @@ pub fn get_entry_options(
runner_options.max_redirect = Some(option.value);
logger.debug(format!("max-redirs: {}", option.value).as_str());
}
EntryOption::PathAsIs(option) => {
runner_options.path_as_is = option.value;
logger.debug(format!("path-as-is: {}", option.value).as_str());
}
EntryOption::Proxy(option) => {
runner_options.proxy = Some(option.value.clone());
logger.debug(format!("proxy: {}", option.value).as_str());

View File

@ -36,6 +36,7 @@ pub struct RunnerOptionsBuilder {
insecure: bool,
max_redirect: Option<usize>,
no_proxy: Option<String>,
path_as_is: bool,
post_entry: Option<fn() -> bool>,
pre_entry: Option<fn(Entry) -> bool>,
proxy: Option<String>,
@ -66,6 +67,7 @@ impl Default for RunnerOptionsBuilder {
insecure: false,
max_redirect: Some(50),
no_proxy: None,
path_as_is: false,
post_entry: None,
pre_entry: None,
proxy: None,
@ -181,6 +183,12 @@ impl RunnerOptionsBuilder {
self
}
/// Sets the path-as-is flag.
pub fn path_as_is(&mut self, path_as_is: bool) -> &mut Self {
self.path_as_is = path_as_is;
self
}
/// Sets list of hosts which do not use a proxy.
pub fn no_proxy(&mut self, no_proxy: Option<String>) -> &mut Self {
self.no_proxy = no_proxy;
@ -279,6 +287,7 @@ impl RunnerOptionsBuilder {
insecure: self.insecure,
max_redirect: self.max_redirect,
no_proxy: self.no_proxy.clone(),
path_as_is: self.path_as_is,
post_entry: self.post_entry,
pre_entry: self.pre_entry,
proxy: self.proxy.clone(),
@ -310,6 +319,7 @@ pub struct RunnerOptions {
pub(crate) insecure: bool,
pub(crate) max_redirect: Option<usize>,
pub(crate) no_proxy: Option<String>,
pub(crate) path_as_is: bool,
pub(crate) post_entry: Option<fn() -> bool>,
pub(crate) pre_entry: Option<fn(Entry) -> bool>,
pub(crate) proxy: Option<String>,

View File

@ -706,6 +706,7 @@ pub enum EntryOption {
Insecure(InsecureOption),
FollowLocation(FollowLocationOption),
MaxRedirect(MaxRedirectOption),
PathAsIs(PathAsIsOption),
Proxy(ProxyOption),
Retry(RetryOption),
RetryInterval(RetryIntervalOption),
@ -764,6 +765,16 @@ pub struct ClientKeyOption {
pub line_terminator0: LineTerminator,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct PathAsIsOption {
pub line_terminators: Vec<LineTerminator>,
pub space0: Whitespace,
pub space1: Whitespace,
pub space2: Whitespace,
pub value: bool,
pub line_terminator0: LineTerminator,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ProxyOption {
pub line_terminators: Vec<LineTerminator>,

View File

@ -209,6 +209,7 @@ impl HtmlFormatter {
EntryOption::Insecure(option) => self.fmt_insecure_option(option),
EntryOption::FollowLocation(option) => self.fmt_follow_location_option(option),
EntryOption::MaxRedirect(option) => self.fmt_max_redirect_option(option),
EntryOption::PathAsIs(option) => self.fmt_path_as_is_option(option),
EntryOption::Proxy(option) => self.fmt_proxy_option(option),
EntryOption::Retry(option) => self.fmt_retry_option(option),
EntryOption::RetryInterval(option) => self.fmt_retry_interval_option(option),
@ -309,6 +310,19 @@ impl HtmlFormatter {
self.fmt_lt(&option.line_terminator0);
}
fn fmt_path_as_is_option(&mut self, option: &PathAsIsOption) {
self.fmt_lts(&option.line_terminators);
self.fmt_span_open("line");
self.fmt_space(&option.space0);
self.fmt_string("path-as-is");
self.fmt_space(&option.space1);
self.buffer.push(':');
self.fmt_space(&option.space2);
self.fmt_bool(option.value);
self.fmt_span_close();
self.fmt_lt(&option.line_terminator0);
}
fn fmt_proxy_option(&mut self, option: &ProxyOption) {
self.fmt_lts(&option.line_terminators);
self.fmt_span_open("line");

View File

@ -364,6 +364,7 @@ fn option(reader: &mut Reader) -> ParseResult<'static, EntryOption> {
option_insecure,
option_follow_location,
option_max_redirect,
option_path_as_is,
option_proxy,
option_retry,
option_retry_interval,
@ -531,6 +532,28 @@ fn option_max_redirect(reader: &mut Reader) -> ParseResult<'static, EntryOption>
Ok(EntryOption::MaxRedirect(option))
}
fn option_path_as_is(reader: &mut Reader) -> ParseResult<'static, EntryOption> {
let line_terminators = optional_line_terminators(reader)?;
let space0 = zero_or_more_spaces(reader)?;
try_literal("path-as-is", reader)?;
let space1 = zero_or_more_spaces(reader)?;
try_literal(":", reader)?;
let space2 = zero_or_more_spaces(reader)?;
let value = nonrecover(boolean, reader)?;
let line_terminator0 = line_terminator(reader)?;
let option = PathAsIsOption {
line_terminators,
space0,
space1,
space2,
value,
line_terminator0,
};
Ok(EntryOption::PathAsIs(option))
}
fn option_proxy(reader: &mut Reader) -> ParseResult<'static, EntryOption> {
let line_terminators = optional_line_terminators(reader)?;
let space0 = zero_or_more_spaces(reader)?;

View File

@ -863,6 +863,7 @@ impl Tokenizable for EntryOption {
EntryOption::Insecure(option) => option.tokenize(),
EntryOption::FollowLocation(option) => option.tokenize(),
EntryOption::MaxRedirect(option) => option.tokenize(),
EntryOption::PathAsIs(option) => option.tokenize(),
EntryOption::Proxy(option) => option.tokenize(),
EntryOption::Retry(option) => option.tokenize(),
EntryOption::RetryInterval(option) => option.tokenize(),
@ -1020,6 +1021,27 @@ impl Tokenizable for MaxRedirectOption {
}
}
impl Tokenizable for PathAsIsOption {
fn tokenize(&self) -> Vec<Token> {
let mut tokens: Vec<Token> = vec![];
tokens.append(
&mut self
.line_terminators
.iter()
.flat_map(|e| e.tokenize())
.collect(),
);
tokens.append(&mut self.space0.tokenize());
tokens.push(Token::String("path-as-is".to_string()));
tokens.append(&mut self.space1.tokenize());
tokens.push(Token::Colon(String::from(":")));
tokens.append(&mut self.space2.tokenize());
tokens.push(Token::Boolean(self.value.to_string()));
tokens.append(&mut self.line_terminator0.tokenize());
tokens
}
}
impl Tokenizable for ProxyOption {
fn tokenize(&self) -> Vec<Token> {
let mut tokens: Vec<Token> = vec![];