Skip to main content

nu_command/network/url/
parse.rs

1use nu_engine::command_prelude::*;
2use nu_protocol::Config;
3use url::Url;
4
5use super::query::query_string_to_table;
6
7#[derive(Clone)]
8pub struct UrlParse;
9
10impl Command for UrlParse {
11    fn name(&self) -> &str {
12        "url parse"
13    }
14
15    fn signature(&self) -> Signature {
16        Signature::build("url parse")
17            .input_output_types(vec![
18                (Type::String, Type::record()),
19                (Type::table(), Type::table()),
20                (Type::record(), Type::record()),
21            ])
22            .allow_variants_without_examples(true)
23            .named(
24                "base",
25                SyntaxShape::String,
26                "Base URL used to resolve relative URLs.",
27                Some('b'),
28            )
29            .rest(
30                "rest",
31                SyntaxShape::CellPath,
32                "Optionally operate by cell path.",
33            )
34            .category(Category::Network)
35    }
36
37    fn description(&self) -> &str {
38        "Parse a URL string into structured data."
39    }
40
41    fn search_terms(&self) -> Vec<&str> {
42        vec![
43            "scheme", "username", "password", "hostname", "port", "path", "query", "fragment",
44        ]
45    }
46
47    fn run(
48        &self,
49        engine_state: &EngineState,
50        stack: &mut Stack,
51        call: &Call,
52        input: PipelineData,
53    ) -> Result<PipelineData, ShellError> {
54        let base: Option<Spanned<String>> = call.get_flag(engine_state, stack, "base")?;
55
56        parse(
57            input.into_value(call.head)?,
58            call.head,
59            &stack.get_config(engine_state),
60            base.map(|b| b.item),
61        )
62    }
63
64    fn examples(&self) -> Vec<Example<'_>> {
65        vec![
66            Example {
67                description: "Parses a URL.",
68                example: "'http://user123:pass567@www.example.com:8081/foo/bar?param1=section&p2=&f[name]=vldc&f[no]=42#hello' | url parse",
69                result: Some(Value::test_record(record! {
70                        "scheme" =>   Value::test_string("http"),
71                        "username" => Value::test_string("user123"),
72                        "password" => Value::test_string("pass567"),
73                        "host" =>     Value::test_string("www.example.com"),
74                        "port" =>     Value::test_string("8081"),
75                        "path" =>     Value::test_string("/foo/bar"),
76                        "query" =>    Value::test_string("param1=section&p2=&f[name]=vldc&f[no]=42"),
77                        "fragment" => Value::test_string("hello"),
78                        "params" =>   Value::test_list(vec![
79                            Value::test_record(record! {"key" => Value::test_string("param1"), "value" => Value::test_string("section") }),
80                            Value::test_record(record! {"key" => Value::test_string("p2"), "value" => Value::test_string("") }),
81                            Value::test_record(record! {"key" => Value::test_string("f[name]"), "value" => Value::test_string("vldc") }),
82                            Value::test_record(record! {"key" => Value::test_string("f[no]"), "value" => Value::test_string("42") }),
83                        ]),
84                })),
85            },
86            Example {
87                description: "Resolves a relative URL against a base URL.",
88                example: "\"../images/logo.png\" | url parse --base \"https://example.com/products/item1\"",
89                result: Some(Value::test_record(record! {
90                    "scheme" =>   Value::test_string("https"),
91                    "username" => Value::test_string(""),
92                    "password" => Value::test_string(""),
93                    "host" =>     Value::test_string("example.com"),
94                    "port" =>     Value::test_string(""),
95                    "path" =>     Value::test_string("/images/logo.png"),
96                    "query" =>    Value::test_string(""),
97                    "fragment" => Value::test_string(""),
98                    "params" =>   Value::test_list(vec![]),
99                })),
100            },
101        ]
102    }
103}
104
105fn get_url_string(value: &Value, config: &Config) -> String {
106    value.to_expanded_string("", config)
107}
108
109fn parse(
110    value: Value,
111    head: Span,
112    config: &Config,
113    base_url: Option<String>,
114) -> Result<PipelineData, ShellError> {
115    let url_string = get_url_string(&value, config);
116
117    // This is the span of the original string, not the call head.
118    let span = value.span();
119
120    let url = match base_url {
121        Some(base_url) => {
122            let base = Url::parse(base_url.as_str()).map_err(|_| {
123                ShellError::UnsupportedInput {
124                    msg: "Incomplete or incorrect base URL. Expected a full URL, e.g., https://www.example.com".to_string(),
125                    input: "value originates from here".into(),
126                    msg_span: head,
127                    input_span: span,
128                }
129            })?;
130
131            base.join(url_string.as_str()).map_err(|_| ShellError::UnsupportedInput {
132                msg: "Incomplete or incorrect URL. Expected a valid URL, or a relative URL when using --base"
133                    .to_string(),
134                input: "value originates from here".into(),
135                msg_span: head,
136                input_span: span,
137            })?
138        }
139        None => Url::parse(url_string.as_str()).map_err(|_| ShellError::UnsupportedInput {
140            msg: "Incomplete or incorrect URL. Expected a full URL, e.g., https://www.example.com"
141                .to_string(),
142            input: "value originates from here".into(),
143            msg_span: head,
144            input_span: span,
145        })?,
146    };
147
148    let params = query_string_to_table(url.query().unwrap_or(""), head, span).map_err(|_| {
149        ShellError::UnsupportedInput {
150            msg: "String not compatible with url-encoding".to_string(),
151            input: "value originates from here".into(),
152            msg_span: head,
153            input_span: span,
154        }
155    })?;
156
157    let port = url.port().map(|p| p.to_string()).unwrap_or_default();
158
159    let record = record! {
160        "scheme" => Value::string(url.scheme(), head),
161        "username" => Value::string(url.username(), head),
162        "password" => Value::string(url.password().unwrap_or(""), head),
163        "host" => Value::string(url.host_str().unwrap_or(""), head),
164        "port" => Value::string(port, head),
165        "path" => Value::string(url.path(), head),
166        "query" => Value::string(url.query().unwrap_or(""), head),
167        "fragment" => Value::string(url.fragment().unwrap_or(""), head),
168        "params" => params,
169    };
170
171    Ok(PipelineData::value(Value::record(record, head), None))
172}
173
174#[cfg(test)]
175mod test {
176    use super::*;
177
178    #[test]
179    fn test_examples() -> nu_test_support::Result {
180        nu_test_support::test().examples(UrlParse)
181    }
182}