diff --git a/Cargo.lock b/Cargo.lock index 98d31a1b78..41caf773ba 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2584,6 +2584,7 @@ dependencies = [ "typetag", "umask", "unicode-xid", + "url", "users", "uuid", "which", diff --git a/crates/nu-cli/Cargo.toml b/crates/nu-cli/Cargo.toml index 2a170842d2..55311159db 100644 --- a/crates/nu-cli/Cargo.toml +++ b/crates/nu-cli/Cargo.toml @@ -34,7 +34,7 @@ codespan-reporting = "0.9.5" csv = "1.1" ctrlc = {version = "3.1.4", optional = true} derive-new = "0.5.8" -directories = {version = "2.0.2", optional = true} +directories = {version = "2.0.2", optional = true} dirs = {version = "2.0.2", optional = true} dunce = "1.0.1" eml-parser = "0.1.0" @@ -94,6 +94,7 @@ rayon = "1.3.1" starship = {version = "0.43.0", optional = true} trash = {version = "1.0.1", optional = true} quick-xml = "0.18.1" +url = {version = "2.1.1"} [target.'cfg(unix)'.dependencies] users = "0.10.0" diff --git a/crates/nu-cli/src/cli.rs b/crates/nu-cli/src/cli.rs index 525c12a4f5..967d66c342 100644 --- a/crates/nu-cli/src/cli.rs +++ b/crates/nu-cli/src/cli.rs @@ -420,6 +420,12 @@ pub fn create_default_context( whole_stream_command(PathExpand), whole_stream_command(PathExists), whole_stream_command(PathType), + // Url + whole_stream_command(UrlCommand), + whole_stream_command(UrlScheme), + whole_stream_command(UrlPath), + whole_stream_command(UrlHost), + whole_stream_command(UrlQuery), ]); #[cfg(feature = "clipboard")] diff --git a/crates/nu-cli/src/commands.rs b/crates/nu-cli/src/commands.rs index d211fcd778..f46b044f7b 100644 --- a/crates/nu-cli/src/commands.rs +++ b/crates/nu-cli/src/commands.rs @@ -115,6 +115,7 @@ pub(crate) mod to_yaml; pub(crate) mod trim; pub(crate) mod uniq; pub(crate) mod update; +pub(crate) mod url_; pub(crate) mod version; pub(crate) mod what; pub(crate) mod where_; @@ -247,6 +248,7 @@ pub(crate) use to_yaml::ToYAML; pub(crate) use touch::Touch; pub(crate) use trim::Trim; pub(crate) use uniq::Uniq; +pub(crate) use url_::{UrlCommand, UrlHost, UrlPath, UrlQuery, UrlScheme}; pub(crate) use version::Version; pub(crate) use what::What; pub(crate) use where_::Where; diff --git a/crates/nu-cli/src/commands/url_/command.rs b/crates/nu-cli/src/commands/url_/command.rs new file mode 100644 index 0000000000..16e764503b --- /dev/null +++ b/crates/nu-cli/src/commands/url_/command.rs @@ -0,0 +1,46 @@ +use crate::commands::WholeStreamCommand; +use crate::prelude::*; +use nu_errors::ShellError; +use nu_protocol::{ReturnSuccess, Signature, UntaggedValue}; + +pub struct Url; + +#[async_trait] +impl WholeStreamCommand for Url { + fn name(&self) -> &str { + "url" + } + + fn signature(&self) -> Signature { + Signature::build("url") + } + + fn usage(&self) -> &str { + "Apply url function" + } + + async fn run( + &self, + _args: CommandArgs, + registry: &CommandRegistry, + ) -> Result { + let registry = registry.clone(); + + Ok(OutputStream::one(ReturnSuccess::value( + UntaggedValue::string(crate::commands::help::get_help(&Url, ®istry)) + .into_value(Tag::unknown()), + ))) + } +} + +#[cfg(test)] +mod tests { + use super::Url; + + #[test] + fn examples_work_as_expected() { + use crate::examples::test as test_examples; + + test_examples(Url {}) + } +} diff --git a/crates/nu-cli/src/commands/url_/host.rs b/crates/nu-cli/src/commands/url_/host.rs new file mode 100644 index 0000000000..4e3472b38b --- /dev/null +++ b/crates/nu-cli/src/commands/url_/host.rs @@ -0,0 +1,58 @@ +use url::Url; + +use super::{operate, DefaultArguments}; +use crate::commands::WholeStreamCommand; +use crate::prelude::*; +use nu_errors::ShellError; +use nu_protocol::{Signature, SyntaxShape, Value}; + +pub struct UrlHost; + +#[async_trait] +impl WholeStreamCommand for UrlHost { + fn name(&self) -> &str { + "url host" + } + + fn signature(&self) -> Signature { + Signature::build("url host") + .rest(SyntaxShape::ColumnPath, "optionally operate by column path") + } + + fn usage(&self) -> &str { + "gets the host of a url" + } + + async fn run( + &self, + args: CommandArgs, + registry: &CommandRegistry, + ) -> Result { + let (DefaultArguments { rest }, input) = args.process(®istry).await?; + operate(input, rest, &host).await + } + + fn examples(&self) -> Vec { + vec![Example { + description: "Get host of a url", + example: "echo 'http://www.example.com/foo/bar' | url host", + result: Some(vec![Value::from("www.example.com")]), + }] + } +} + +fn host(url: &Url) -> &str { + url.host_str().unwrap_or("") +} + +#[cfg(test)] +mod tests { + use super::UrlHost; + + #[test] + fn examples_work_as_expected() { + use crate::examples::test as test_examples; + + test_examples(UrlHost {}) + } +} diff --git a/crates/nu-cli/src/commands/url_/mod.rs b/crates/nu-cli/src/commands/url_/mod.rs new file mode 100644 index 0000000000..c2161cc16d --- /dev/null +++ b/crates/nu-cli/src/commands/url_/mod.rs @@ -0,0 +1,72 @@ +mod command; +mod host; +mod path; +mod query; +mod scheme; + +use crate::prelude::*; +use nu_errors::ShellError; +use nu_protocol::{ColumnPath, Primitive, ReturnSuccess, ShellTypeName, UntaggedValue, Value}; +use url::Url; + +pub use command::Url as UrlCommand; +pub use host::UrlHost; +pub use path::UrlPath; +pub use query::UrlQuery; +pub use scheme::UrlScheme; + +#[derive(Deserialize)] +struct DefaultArguments { + rest: Vec, +} + +fn handle_value(action: &F, v: &Value) -> Result +where + F: Fn(&Url) -> &str + Send + 'static, +{ + let a = |url| UntaggedValue::string(action(url)); + let v = match &v.value { + UntaggedValue::Primitive(Primitive::String(s)) + | UntaggedValue::Primitive(Primitive::Line(s)) => match Url::parse(s) { + Ok(url) => a(&url).into_value(v.tag()), + Err(_) => UntaggedValue::string("").into_value(v.tag()), + }, + other => { + let got = format!("got {}", other.type_name()); + return Err(ShellError::labeled_error( + "value is not a string", + got, + v.tag().span, + )); + } + }; + Ok(v) +} + +async fn operate( + input: crate::InputStream, + paths: Vec, + action: &'static F, +) -> Result +where + F: Fn(&Url) -> &str + Send + Sync + 'static, +{ + Ok(input + .map(move |v| { + if paths.is_empty() { + ReturnSuccess::value(handle_value(&action, &v)?) + } else { + let mut ret = v; + + for path in &paths { + ret = ret.swap_data_by_column_path( + path, + Box::new(move |old| handle_value(&action, &old)), + )?; + } + + ReturnSuccess::value(ret) + } + }) + .to_output_stream()) +} diff --git a/crates/nu-cli/src/commands/url_/path.rs b/crates/nu-cli/src/commands/url_/path.rs new file mode 100644 index 0000000000..58c8560aed --- /dev/null +++ b/crates/nu-cli/src/commands/url_/path.rs @@ -0,0 +1,61 @@ +use url::Url; + +use super::{operate, DefaultArguments}; +use crate::commands::WholeStreamCommand; +use crate::prelude::*; +use nu_errors::ShellError; +use nu_protocol::{Signature, SyntaxShape, Value}; + +pub struct UrlPath; + +#[async_trait] +impl WholeStreamCommand for UrlPath { + fn name(&self) -> &str { + "url path" + } + + fn signature(&self) -> Signature { + Signature::build("url path") + .rest(SyntaxShape::ColumnPath, "optionally operate by column path") + } + + fn usage(&self) -> &str { + "gets the path of a url" + } + + async fn run( + &self, + args: CommandArgs, + registry: &CommandRegistry, + ) -> Result { + let (DefaultArguments { rest }, input) = args.process(®istry).await?; + operate(input, rest, &Url::path).await + } + + fn examples(&self) -> Vec { + vec![ + Example { + description: "Get path of a url", + example: "echo 'http://www.example.com/foo/bar' | url path", + result: Some(vec![Value::from("/foo/bar")]), + }, + Example { + description: "A trailing slash will be reflected in the path", + example: "echo 'http://www.example.com' | url path", + result: Some(vec![Value::from("/")]), + }, + ] + } +} + +#[cfg(test)] +mod tests { + use super::UrlPath; + + #[test] + fn examples_work_as_expected() { + use crate::examples::test as test_examples; + + test_examples(UrlPath {}) + } +} diff --git a/crates/nu-cli/src/commands/url_/query.rs b/crates/nu-cli/src/commands/url_/query.rs new file mode 100644 index 0000000000..32a0d71528 --- /dev/null +++ b/crates/nu-cli/src/commands/url_/query.rs @@ -0,0 +1,65 @@ +use url::Url; + +use super::{operate, DefaultArguments}; +use crate::commands::WholeStreamCommand; +use crate::prelude::*; +use nu_errors::ShellError; +use nu_protocol::{Signature, SyntaxShape, Value}; + +pub struct UrlQuery; + +#[async_trait] +impl WholeStreamCommand for UrlQuery { + fn name(&self) -> &str { + "url query" + } + + fn signature(&self) -> Signature { + Signature::build("url query") + .rest(SyntaxShape::ColumnPath, "optionally operate by column path") + } + + fn usage(&self) -> &str { + "gets the query of a url" + } + + async fn run( + &self, + args: CommandArgs, + registry: &CommandRegistry, + ) -> Result { + let (DefaultArguments { rest }, input) = args.process(®istry).await?; + operate(input, rest, &query).await + } + + fn examples(&self) -> Vec { + vec![ + Example { + description: "Get query of a url", + example: "echo 'http://www.example.com/?foo=bar&baz=quux' | url query", + result: Some(vec![Value::from("foo=bar&baz=quux")]), + }, + Example { + description: "No query gives the empty string", + example: "echo 'http://www.example.com/' | url query", + result: Some(vec![Value::from("")]), + }, + ] + } +} + +fn query(url: &Url) -> &str { + url.query().unwrap_or("") +} + +#[cfg(test)] +mod tests { + use super::UrlQuery; + + #[test] + fn examples_work_as_expected() { + use crate::examples::test as test_examples; + + test_examples(UrlQuery {}) + } +} diff --git a/crates/nu-cli/src/commands/url_/scheme.rs b/crates/nu-cli/src/commands/url_/scheme.rs new file mode 100644 index 0000000000..33b446d860 --- /dev/null +++ b/crates/nu-cli/src/commands/url_/scheme.rs @@ -0,0 +1,60 @@ +use url::Url; + +use super::{operate, DefaultArguments}; +use crate::commands::WholeStreamCommand; +use crate::prelude::*; +use nu_errors::ShellError; +use nu_protocol::{Signature, SyntaxShape, Value}; + +pub struct UrlScheme; + +#[async_trait] +impl WholeStreamCommand for UrlScheme { + fn name(&self) -> &str { + "url scheme" + } + + fn signature(&self) -> Signature { + Signature::build("url scheme").rest(SyntaxShape::ColumnPath, "optionally operate by path") + } + + fn usage(&self) -> &str { + "gets the scheme (eg http, file) of a url" + } + + async fn run( + &self, + args: CommandArgs, + registry: &CommandRegistry, + ) -> Result { + let (DefaultArguments { rest }, input) = args.process(®istry).await?; + operate(input, rest, &Url::scheme).await + } + + fn examples(&self) -> Vec { + vec![ + Example { + description: "Get scheme of a url", + example: "echo 'http://www.example.com' | url scheme", + result: Some(vec![Value::from("http")]), + }, + Example { + description: "You get an empty string if there is no scheme", + example: "echo 'test' | url scheme", + result: Some(vec![Value::from("")]), + }, + ] + } +} + +#[cfg(test)] +mod tests { + use super::UrlScheme; + + #[test] + fn examples_work_as_expected() { + use crate::examples::test as test_examples; + + test_examples(UrlScheme {}) + } +}