diff --git a/crates/nu-command/src/default_context.rs b/crates/nu-command/src/default_context.rs index 4d89d28ff1..af9927087f 100644 --- a/crates/nu-command/src/default_context.rs +++ b/crates/nu-command/src/default_context.rs @@ -357,6 +357,7 @@ pub fn add_shell_command_context(mut engine_state: EngineState) -> EngineState { HttpOptions, Url, UrlBuildQuery, + UrlDecode, UrlEncode, UrlJoin, UrlParse, diff --git a/crates/nu-command/src/network/url/decode.rs b/crates/nu-command/src/network/url/decode.rs new file mode 100644 index 0000000000..4684472620 --- /dev/null +++ b/crates/nu-command/src/network/url/decode.rs @@ -0,0 +1,122 @@ +use nu_cmd_base::input_handler::{operate, CellPathOnlyArgs}; +use nu_engine::CallExt; +use nu_protocol::ast::{Call, CellPath}; +use nu_protocol::engine::{Command, EngineState, Stack}; +use nu_protocol::Category; +use nu_protocol::{Example, PipelineData, ShellError, Signature, Span, SyntaxShape, Type, Value}; +use percent_encoding::percent_decode_str; + +#[derive(Clone)] +pub struct SubCommand; + +impl Command for SubCommand { + fn name(&self) -> &str { + "url decode" + } + + fn signature(&self) -> Signature { + Signature::build("url decode") + .input_output_types(vec![ + (Type::String, Type::String), + ( + Type::List(Box::new(Type::String)), + Type::List(Box::new(Type::String)), + ), + (Type::Table(vec![]), Type::Table(vec![])), + (Type::Record(vec![]), Type::Record(vec![])), + ]) + .allow_variants_without_examples(true) + .rest( + "rest", + SyntaxShape::CellPath, + "For a data structure input, url decode strings at the given cell paths", + ) + .category(Category::Strings) + } + + fn usage(&self) -> &str { + "Converts a percent-encoded web safe string to a string." + } + + fn search_terms(&self) -> Vec<&str> { + vec!["string", "text", "convert"] + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + let cell_paths: Vec = call.rest(engine_state, stack, 0)?; + let args = CellPathOnlyArgs::from(cell_paths); + operate(action, args, input, call.head, engine_state.ctrlc.clone()) + } + + fn examples(&self) -> Vec { + vec![ + Example { + description: "Decode a url with escape characters", + example: "'https://example.com/foo%20bar' | url decode", + result: Some(Value::test_string("https://example.com/foo bar")), + }, + Example { + description: "Decode multiple urls with escape characters in list", + example: "['https://example.com/foo%20bar' 'https://example.com/a%3Eb' '%E4%B8%AD%E6%96%87%E5%AD%97/eng/12%2034'] | url decode", + result: Some(Value::list( + vec![ + Value::test_string("https://example.com/foo bar"), + Value::test_string("https://example.com/a>b"), + Value::test_string("中文字/eng/12 34"), + ], + Span::test_data(), + )), + }, + ] + } +} + +fn action(input: &Value, _arg: &CellPathOnlyArgs, head: Span) -> Value { + let input_span = input.span(); + match input { + Value::String { val, .. } => { + let val = percent_decode_str(val).decode_utf8(); + match val { + Ok(val) => Value::string(val, head), + Err(e) => Value::error( + ShellError::GenericError( + "Failed to decode string".into(), + e.to_string(), + Some(input_span), + None, + Vec::new(), + ), + head, + ), + } + } + Value::Error { .. } => input.clone(), + _ => Value::error( + ShellError::OnlySupportsThisInputType { + exp_input_type: "string".into(), + wrong_type: input.get_type().to_string(), + dst_span: head, + src_span: input.span(), + }, + head, + ), + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(SubCommand {}) + } +} diff --git a/crates/nu-command/src/network/url/mod.rs b/crates/nu-command/src/network/url/mod.rs index 1b6cc7fd0b..7cdb5ad4e4 100644 --- a/crates/nu-command/src/network/url/mod.rs +++ b/crates/nu-command/src/network/url/mod.rs @@ -1,4 +1,5 @@ mod build_query; +mod decode; mod encode; mod join; mod parse; @@ -8,6 +9,7 @@ use url::{self}; pub use self::parse::SubCommand as UrlParse; pub use build_query::SubCommand as UrlBuildQuery; +pub use decode::SubCommand as UrlDecode; pub use encode::SubCommand as UrlEncode; pub use join::SubCommand as UrlJoin; pub use url_::Url; diff --git a/crates/nu-command/tests/commands/url/decode.rs b/crates/nu-command/tests/commands/url/decode.rs new file mode 100644 index 0000000000..34b826a1de --- /dev/null +++ b/crates/nu-command/tests/commands/url/decode.rs @@ -0,0 +1,19 @@ +use nu_test_support::nu; + +#[test] +fn url_decode_simple() { + let actual = nu!(r#"'a%20b' | url decode"#); + assert_eq!(actual.out, "a b"); +} + +#[test] +fn url_decode_special_characters() { + let actual = nu!(r#"'%21%40%23%24%25%C2%A8%26%2A%2D%2B%3B%2C%7B%7D%5B%5D%28%29' | url decode"#); + assert_eq!(actual.out, r#"!@#$%¨&*-+;,{}[]()"#); +} + +#[test] +fn url_decode_error_invalid_utf8() { + let actual = nu!(r#"'%99' | url decode"#); + assert!(actual.err.contains("invalid utf-8 sequence")); +} diff --git a/crates/nu-command/tests/commands/url/mod.rs b/crates/nu-command/tests/commands/url/mod.rs index 6c825d4708..10c5b3ad3a 100644 --- a/crates/nu-command/tests/commands/url/mod.rs +++ b/crates/nu-command/tests/commands/url/mod.rs @@ -1,2 +1,3 @@ +mod decode; mod join; mod parse;