diff --git a/integration/hurl/tests_ok/stdout.err.pattern b/integration/hurl/tests_ok/stdout.err.pattern new file mode 100644 index 00000000000..2d56bcc3b53 --- /dev/null +++ b/integration/hurl/tests_ok/stdout.err.pattern @@ -0,0 +1,30 @@ +* ------------------------------------------------------------------------------ +* Executing entry 1 +* +* Entry options: +* output: - +* +* Cookie store: +* +* Request: +* GET http://localhost:8000/stdout/text +* +* Request can be run with the following curl command: +* curl --output - 'http://localhost:8000/stdout/text' +* +> GET /stdout/text HTTP/1.1 +> Host: localhost:8000 +> Accept: */* +> User-Agent: hurl/~~~ +> +* Response: (received 5 bytes in ~~~ ms) +* +< HTTP/1.1 200 OK +< Server: Werkzeug/~~~ +< Date: ~~~ +< Content-Type: text/html; charset=utf-8 +< Content-Length: 5 +< Server: Flask Server +< Connection: close +< +* diff --git a/integration/hurl/tests_ok/stdout.hurl b/integration/hurl/tests_ok/stdout.hurl new file mode 100755 index 00000000000..f656aa811a9 --- /dev/null +++ b/integration/hurl/tests_ok/stdout.hurl @@ -0,0 +1,8 @@ +GET http://localhost:8000/stdout/text +[Options] +output: - +HTTP 200 +`Hello` + + + diff --git a/integration/hurl/tests_ok/stdout.out b/integration/hurl/tests_ok/stdout.out new file mode 100644 index 00000000000..b3a7c9b4e11 --- /dev/null +++ b/integration/hurl/tests_ok/stdout.out @@ -0,0 +1 @@ +HelloHello \ No newline at end of file diff --git a/integration/hurl/tests_ok/stdout.ps1 b/integration/hurl/tests_ok/stdout.ps1 new file mode 100755 index 00000000000..a4db8b7d768 --- /dev/null +++ b/integration/hurl/tests_ok/stdout.ps1 @@ -0,0 +1,5 @@ +Set-StrictMode -Version latest +$ErrorActionPreference = 'Stop' +hurl --verbose --output - tests_ok/stdout.hurl + + diff --git a/integration/hurl/tests_ok/stdout.py b/integration/hurl/tests_ok/stdout.py new file mode 100644 index 00000000000..6ae0af6452b --- /dev/null +++ b/integration/hurl/tests_ok/stdout.py @@ -0,0 +1,7 @@ +# coding=utf-8 +from app import app + + +@app.route("/stdout/text") +def stdout_text(): + return "Hello" diff --git a/integration/hurl/tests_ok/stdout.sh b/integration/hurl/tests_ok/stdout.sh new file mode 100755 index 00000000000..ce3a1b0f0a4 --- /dev/null +++ b/integration/hurl/tests_ok/stdout.sh @@ -0,0 +1,5 @@ +#!/bin/bash +set -Eeuo pipefail +hurl --verbose tests_ok/stdout.hurl + + diff --git a/packages/hurl/src/cli/options/matches.rs b/packages/hurl/src/cli/options/matches.rs index db95e06d7b8..b61667b2c3a 100644 --- a/packages/hurl/src/cli/options/matches.rs +++ b/packages/hurl/src/cli/options/matches.rs @@ -28,7 +28,7 @@ use hurl_core::ast::Retry; use super::variables::{parse as parse_variable, parse_value}; use super::OptionsError; -use crate::cli::options::{ErrorFormat, HttpVersion, IpResolve}; +use crate::cli::options::{ErrorFormat, HttpVersion, IpResolve, Output}; use crate::cli::OutputType; pub fn cacert_file(arg_matches: &ArgMatches) -> Result, OptionsError> { @@ -254,8 +254,14 @@ pub fn no_proxy(arg_matches: &ArgMatches) -> Option { get::(arg_matches, "noproxy") } -pub fn output(arg_matches: &ArgMatches) -> Option { - get::(arg_matches, "output") +pub fn output(arg_matches: &ArgMatches) -> Option { + get::(arg_matches, "output").map(|filename| { + if filename == "-" { + Output::StdOut + } else { + Output::File(filename) + } + }) } pub fn output_type(arg_matches: &ArgMatches) -> OutputType { diff --git a/packages/hurl/src/cli/options/mod.rs b/packages/hurl/src/cli/options/mod.rs index 611dbc9713e..cf7882e8723 100644 --- a/packages/hurl/src/cli/options/mod.rs +++ b/packages/hurl/src/cli/options/mod.rs @@ -27,6 +27,7 @@ use std::time::Duration; use clap::ArgMatches; use hurl::http; use hurl::http::RequestedHttpVersion; +use hurl::runner::Output; use hurl::util::logger::{LoggerOptions, LoggerOptionsBuilder, Verbosity}; use hurl::util::path::ContextDir; use hurl_core::ast::{Entry, Retry}; @@ -63,7 +64,7 @@ pub struct Options { pub junit_file: Option, pub max_redirect: Option, pub no_proxy: Option, - pub output: Option, + pub output: Option, pub output_type: OutputType, pub path_as_is: bool, pub progress_bar: bool, diff --git a/packages/hurl/src/http/client.rs b/packages/hurl/src/http/client.rs index 15d17487dd9..3d28b70cd94 100644 --- a/packages/hurl/src/http/client.rs +++ b/packages/hurl/src/http/client.rs @@ -35,6 +35,7 @@ use crate::http::request_spec::*; use crate::http::response::*; use crate::http::timings::Timings; use crate::http::{easy_ext, Call, Header, HttpError, Verbosity}; +use crate::runner::Output; use crate::util::logger::Logger; use crate::util::path::ContextDir; @@ -737,7 +738,7 @@ impl Client { &mut self, request_spec: &RequestSpec, context_dir: &ContextDir, - output: Option<&str>, + output: Option<&Output>, options: &ClientOptions, ) -> String { let mut arguments = vec!["curl".to_string()]; @@ -973,7 +974,7 @@ mod tests { let data = b"GET /hello HTTP/1.1\r\nHost: localhost:8000\r\n\r\n"; let lines = split_lines(data); assert_eq!(lines.len(), 3); - assert_eq!(lines.get(0).unwrap().as_str(), "GET /hello HTTP/1.1"); + assert_eq!(lines.first().unwrap().as_str(), "GET /hello HTTP/1.1"); assert_eq!(lines.get(1).unwrap().as_str(), "Host: localhost:8000"); assert_eq!(lines.get(2).unwrap().as_str(), ""); } @@ -1118,7 +1119,8 @@ mod tests { ..Default::default() }; let context_dir = ContextDir::default(); - let output = Some("/tmp/foo.bin"); + let file = Output::File("/tmp/foo.bin".to_string()); + let output = Some(&file); let options = ClientOptions { aws_sigv4: Some("aws:amz:sts".to_string()), cacert_file: Some("/etc/cert.pem".to_string()), diff --git a/packages/hurl/src/main.rs b/packages/hurl/src/main.rs index ee6614fbc0f..c5679106be0 100644 --- a/packages/hurl/src/main.rs +++ b/packages/hurl/src/main.rs @@ -113,6 +113,7 @@ fn main() { let success = hurl_result.success; // We can output the result, either the raw body or a structured JSON representation. + let output_body = success && !opts.interactive && matches!(opts.output_type, cli::OutputType::ResponseBody); diff --git a/packages/hurl/src/output/json.rs b/packages/hurl/src/output/json.rs index 85172a8923e..9e71293a1fc 100644 --- a/packages/hurl/src/output/json.rs +++ b/packages/hurl/src/output/json.rs @@ -27,15 +27,15 @@ pub fn write_json( hurl_result: &HurlResult, content: &str, filename_in: &str, - filename_out: &Option, + filename_out: &Option, ) -> Result<(), Error> { let json_result = hurl_result.to_json(content, filename_in); let serialized = serde_json::to_string(&json_result).unwrap(); let s = format!("{serialized}\n"); let bytes = s.into_bytes(); match filename_out { - Some(file) => Output::File(file.to_string()).write(&bytes)?, - None => Output::StdOut.write(&bytes)?, + Some(Output::File(file)) => Output::File(file.to_string()).write(&bytes)?, + _ => Output::StdOut.write(&bytes)?, } Ok(()) } diff --git a/packages/hurl/src/output/raw.rs b/packages/hurl/src/output/raw.rs index fcb72651e0a..ca10873a2a9 100644 --- a/packages/hurl/src/output/raw.rs +++ b/packages/hurl/src/output/raw.rs @@ -31,7 +31,7 @@ pub fn write_body( filename_in: &str, include_headers: bool, color: bool, - filename_out: &Option, + filename_out: &Option, logger: &Logger, ) -> Result<(), Error> { // By default, we output the body response bytes of the last entry @@ -65,8 +65,8 @@ pub fn write_body( output.extend(bytes); } match filename_out { - Some(file) => Output::File(file.to_string()).write(&output)?, - None => Output::StdOut.write(&output)?, + Some(Output::File(file)) => Output::File(file.to_string()).write(&output)?, + _ => runner::Output::StdOut.write(&output)?, } } else { logger.info("No response has been received"); diff --git a/packages/hurl/src/runner/entry.rs b/packages/hurl/src/runner/entry.rs index 22a38af1119..e02c5ff5534 100644 --- a/packages/hurl/src/runner/entry.rs +++ b/packages/hurl/src/runner/entry.rs @@ -84,13 +84,9 @@ pub fn run( log_request_spec(&http_request, logger); logger.debug("Request can be run with the following curl command:"); - let output = &runner_options.output; - let curl_command = http_client.curl_command_line( - &http_request, - context_dir, - output.as_deref(), - &client_options, - ); + let output = runner_options.output.clone(); + let curl_command = + http_client.curl_command_line(&http_request, context_dir, output.as_ref(), &client_options); logger.debug(curl_command.as_str()); logger.debug(""); diff --git a/packages/hurl/src/runner/hurl_file.rs b/packages/hurl/src/runner/hurl_file.rs index 2c0c9fd7d85..27760c696c5 100644 --- a/packages/hurl/src/runner/hurl_file.rs +++ b/packages/hurl/src/runner/hurl_file.rs @@ -28,7 +28,7 @@ use hurl_core::parser; use crate::http::Call; use crate::runner::runner_options::RunnerOptions; -use crate::runner::{entry, options, EntryResult, HurlResult, RunnerError, Value}; +use crate::runner::{entry, options, EntryResult, HurlResult, Output, RunnerError, Value}; use crate::util::logger::{ErrorFormat, Logger, LoggerOptions, LoggerOptionsBuilder}; use crate::{http, runner}; @@ -220,14 +220,25 @@ pub fn run( // an error. If we want to treat it as an error, we've to add it to the current // `entry_result` errors, and optionally deals with retry if we can't write to the // specified path. - if !runner_options.context_dir.is_access_allowed(&output) { - let inner = RunnerError::UnauthorizedFileAccess { - path: PathBuf::from(output.clone()), - }; - let error = runner::Error::new(entry.request.source_info, inner, false); - logger.warning(&error.fixme()); - } else if let Err(error) = entry_result.write_response(output) { - logger.warning(&error.fixme()); + + let authorized = if let Output::File(filename) = output.clone() { + if !runner_options.context_dir.is_access_allowed(&filename) { + let inner = RunnerError::UnauthorizedFileAccess { + path: PathBuf::from(filename.clone()), + }; + let error = runner::Error::new(entry.request.source_info, inner, false); + logger.warning(&error.fixme()); + false + } else { + true + } + } else { + true + }; + if authorized { + if let Err(error) = entry_result.write_response(&output) { + logger.warning(&error.fixme()); + } } } } @@ -508,7 +519,7 @@ mod test { let non_default_options = get_non_default_options(&options); assert_eq!(non_default_options.len(), 1); - let first_non_default = non_default_options.get(0).unwrap(); + let first_non_default = non_default_options.first().unwrap(); assert_eq!(first_non_default.0, "delay"); assert_eq!(first_non_default.1, "500ms"); diff --git a/packages/hurl/src/runner/options.rs b/packages/hurl/src/runner/options.rs index 135237696a2..0d1163cfa22 100644 --- a/packages/hurl/src/runner/options.rs +++ b/packages/hurl/src/runner/options.rs @@ -25,7 +25,7 @@ use hurl_core::ast::{ use crate::http::{IpResolve, RequestedHttpVersion}; use crate::runner::template::{eval_expression, eval_template}; -use crate::runner::{Error, Number, RunnerError, RunnerOptions, Value}; +use crate::runner::{Error, Number, Output, RunnerError, RunnerOptions, Value}; use crate::util::logger::{Logger, Verbosity}; /// Returns a new [`RunnerOptions`] based on the `entry` optional Options section @@ -166,9 +166,14 @@ pub fn get_entry_options( let value = eval_natural_option(value, variables)?; runner_options.max_redirect = Some(value as usize) } - OptionKind::Output(filename) => { - let value = eval_template(filename, variables)?; - runner_options.output = Some(value) + OptionKind::Output(output) => { + let filename = eval_template(output, variables)?; + let output = if filename == "-" { + Output::StdOut + } else { + Output::File(filename) + }; + runner_options.output = Some(output) } OptionKind::PathAsIs(value) => { let value = eval_boolean_option(value, variables)?; diff --git a/packages/hurl/src/runner/output.rs b/packages/hurl/src/runner/output.rs index c019ad6dba5..356953e4661 100644 --- a/packages/hurl/src/runner/output.rs +++ b/packages/hurl/src/runner/output.rs @@ -16,20 +16,31 @@ * */ use std::fs::File; -use std::io; #[cfg(target_family = "windows")] use std::io::IsTerminal; use std::io::Write; +use std::{fmt, io}; use crate::runner::{Error, RunnerError}; use hurl_core::ast::{Pos, SourceInfo}; /// Represents the output of write operation: can be either a file or stdout. +#[derive(Clone, Debug, PartialEq, Eq)] pub enum Output { StdOut, File(String), } +impl fmt::Display for Output { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let output = match self { + Output::StdOut => "-".to_string(), + Output::File(file) => file.to_string(), + }; + write!(f, "{output}") + } +} + impl Output { /// Writes these `bytes` to the output. pub fn write(&self, bytes: &[u8]) -> Result<(), Error> { diff --git a/packages/hurl/src/runner/result.rs b/packages/hurl/src/runner/result.rs index 8a317712154..c87aedd04dd 100644 --- a/packages/hurl/src/runner/result.rs +++ b/packages/hurl/src/runner/result.rs @@ -108,7 +108,7 @@ pub type PredicateResult = Result<(), Error>; impl EntryResult { /// Writes the last HTTP response of this entry result to the file `filename`. /// The HTTP response can be decompressed if the entry's `compressed` option has been set. - pub fn write_response(&self, filename: String) -> Result<(), Error> { + pub fn write_response(&self, output: &Output) -> Result<(), Error> { match self.calls.last() { Some(call) => { let response = &call.response; @@ -124,9 +124,9 @@ impl EntryResult { return Err(Error::new(source_info, e.into(), false)); } }; - Output::File(filename).write(&bytes) + output.write(&bytes) } else { - Output::File(filename).write(&response.body) + output.write(&response.body) } } None => Ok(()), diff --git a/packages/hurl/src/runner/runner_options.rs b/packages/hurl/src/runner/runner_options.rs index 239b4826c33..f76ab7bc2bc 100644 --- a/packages/hurl/src/runner/runner_options.rs +++ b/packages/hurl/src/runner/runner_options.rs @@ -20,6 +20,7 @@ use std::time::Duration; use hurl_core::ast::{Entry, Retry}; use crate::http::{IpResolve, RequestedHttpVersion}; +use crate::runner::Output; use crate::util::path::ContextDir; pub struct RunnerOptionsBuilder { @@ -42,7 +43,7 @@ pub struct RunnerOptionsBuilder { ip_resolve: IpResolve, max_redirect: Option, no_proxy: Option, - output: Option, + output: Option, path_as_is: bool, post_entry: Option bool>, pre_entry: Option bool>, @@ -256,7 +257,7 @@ impl RunnerOptionsBuilder { } /// Specifies the file to output the HTTP response instead of stdout. - pub fn output(&mut self, output: Option) -> &mut Self { + pub fn output(&mut self, output: Option) -> &mut Self { self.output = output; self } @@ -404,7 +405,7 @@ pub struct RunnerOptions { pub(crate) insecure: bool, pub(crate) max_redirect: Option, pub(crate) no_proxy: Option, - pub(crate) output: Option, + pub(crate) output: Option, pub(crate) path_as_is: bool, pub(crate) post_entry: Option bool>, pub(crate) pre_entry: Option bool>, diff --git a/packages/hurlfmt/src/format/token.rs b/packages/hurlfmt/src/format/token.rs index a8eb598b095..033c653c4a0 100644 --- a/packages/hurlfmt/src/format/token.rs +++ b/packages/hurlfmt/src/format/token.rs @@ -960,6 +960,16 @@ impl Tokenizable for VariableValue { } } +// +// impl Tokenizable for Output { +// fn tokenize(&self) -> Vec { +// match self { +// Output::StdOut => vec![Token::String("-".to_string())], +// Output::File(file) => file.tokenize(), +// } +// } +// } + impl Tokenizable for Filter { fn tokenize(&self) -> Vec { match self.value.clone() {