diff --git a/Cargo.lock b/Cargo.lock index fce28ee3b6..751eb7d57a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2636,6 +2636,7 @@ dependencies = [ "once_cell", "open", "pathdiff", + "percent-encoding", "polars", "powierza-coefficient", "proptest", diff --git a/crates/nu-command/Cargo.toml b/crates/nu-command/Cargo.toml index 05992db689..3fa3a8c0a6 100644 --- a/crates/nu-command/Cargo.toml +++ b/crates/nu-command/Cargo.toml @@ -88,6 +88,7 @@ titlecase = "2.0.0" toml = "0.5.8" unicode-segmentation = "1.8.0" url = "2.2.1" +percent-encoding = "2.2.0" uuid = { version = "1.1.2", features = ["v4"] } which = { version = "4.3.0", optional = true } reedline = { version = "0.14.0", features = ["bashisms", "sqlite"]} diff --git a/crates/nu-command/src/default_context.rs b/crates/nu-command/src/default_context.rs index 2a044a6e95..5eb73c7118 100644 --- a/crates/nu-command/src/default_context.rs +++ b/crates/nu-command/src/default_context.rs @@ -436,6 +436,7 @@ pub fn create_default_context() -> EngineState { Fetch, Post, Url, + UrlEncode, UrlParse, Port, } diff --git a/crates/nu-command/src/network/url/encode.rs b/crates/nu-command/src/network/url/encode.rs new file mode 100644 index 0000000000..d64d9d596f --- /dev/null +++ b/crates/nu-command/src/network/url/encode.rs @@ -0,0 +1,144 @@ +use crate::input_handler::{operate, CellPathOnlyArgs}; +use nu_engine::CallExt; +use nu_protocol::ast::Call; +use nu_protocol::ast::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::{utf8_percent_encode, AsciiSet, NON_ALPHANUMERIC}; + +#[derive(Clone)] +pub struct SubCommand; + +impl Command for SubCommand { + fn name(&self) -> &str { + "url encode" + } + + fn signature(&self) -> Signature { + Signature::build("url encode") + .input_output_types(vec![(Type::String, Type::String)]) + .vectorizes_over_list(true) + .switch( + "all", + "encode all non-alphanumeric chars including `/`, `.`, `:`", + Some('a')) + .rest( + "rest", + SyntaxShape::CellPath, + "For a data structure input, check strings at the given cell paths, and replace with result", + ) + .category(Category::Strings) + } + + fn usage(&self) -> &str { + "Converts a string to a percent encoded web safe 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); + if call.has_flag("all") { + operate( + action_all, + args, + input, + call.head, + engine_state.ctrlc.clone(), + ) + } else { + operate(action, args, input, call.head, engine_state.ctrlc.clone()) + } + } + + fn examples(&self) -> Vec { + vec![ + Example { + description: "Encode a url with escape characters", + example: "'https://example.com/foo bar' | url encode", + result: Some(Value::test_string("https://example.com/foo%20bar")), + }, + Example { + description: "Encode multiple urls with escape characters in list", + example: "['https://example.com/foo bar' 'https://example.com/a>b' '中文字/eng/12 34'] | url encode", + result: Some(Value::List { + vals: vec![ + Value::test_string("https://example.com/foo%20bar"), + Value::test_string("https://example.com/a%3Eb"), + Value::test_string("%E4%B8%AD%E6%96%87%E5%AD%97/eng/12%2034"), + ], + span: Span::test_data(), + }), + }, + Example { + description: "Encode all non anphanumeric chars with all flag", + example: "'https://example.com/foo bar' | url encode --all", + result: Some(Value::test_string("https%3A%2F%2Fexample%2Ecom%2Ffoo%20bar")), + }, + ] + } +} + +fn action_all(input: &Value, _arg: &CellPathOnlyArgs, head: Span) -> Value { + match input { + Value::String { val, .. } => { + const FRAGMENT: &AsciiSet = NON_ALPHANUMERIC; + Value::String { + val: utf8_percent_encode(val, FRAGMENT).to_string(), + span: head, + } + } + Value::Error { .. } => input.clone(), + _ => Value::Error { + error: ShellError::OnlySupportsThisInputType( + "string".into(), + input.get_type().to_string(), + head, + input.expect_span(), + ), + }, + } +} + +fn action(input: &Value, _arg: &CellPathOnlyArgs, head: Span) -> Value { + match input { + Value::String { val, .. } => { + const FRAGMENT: &AsciiSet = &NON_ALPHANUMERIC.remove(b'/').remove(b':').remove(b'.'); + Value::String { + val: utf8_percent_encode(val, FRAGMENT).to_string(), + span: head, + } + } + Value::Error { .. } => input.clone(), + _ => Value::Error { + error: ShellError::OnlySupportsThisInputType( + "string".into(), + input.get_type().to_string(), + head, + input.expect_span(), + ), + }, + } +} + +#[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 930be1ad22..d2845bc7e1 100644 --- a/crates/nu-command/src/network/url/mod.rs +++ b/crates/nu-command/src/network/url/mod.rs @@ -1,7 +1,9 @@ +mod encode; mod parse; mod url_; use url::{self}; pub use self::parse::SubCommand as UrlParse; +pub use encode::SubCommand as UrlEncode; pub use url_::Url;