nu_command/filesystem/
open.rs

1#[allow(deprecated)]
2use nu_engine::{command_prelude::*, current_dir, eval_call};
3use nu_path::is_windows_device_path;
4use nu_protocol::{
5    DataSource, NuGlob, PipelineMetadata, ast,
6    debugger::{WithDebug, WithoutDebug},
7    shell_error::{self, io::IoError},
8};
9use std::{
10    collections::HashMap,
11    path::{Path, PathBuf},
12};
13
14#[cfg(feature = "sqlite")]
15use crate::database::SQLiteDatabase;
16
17#[cfg(unix)]
18use std::os::unix::fs::PermissionsExt;
19
20#[derive(Clone)]
21pub struct Open;
22
23impl Command for Open {
24    fn name(&self) -> &str {
25        "open"
26    }
27
28    fn description(&self) -> &str {
29        "Load a file into a cell, converting to table if possible (avoid by appending '--raw')."
30    }
31
32    fn extra_description(&self) -> &str {
33        "Support to automatically parse files with an extension `.xyz` can be provided by a `from xyz` command in scope."
34    }
35
36    fn search_terms(&self) -> Vec<&str> {
37        vec![
38            "load",
39            "read",
40            "load_file",
41            "read_file",
42            "cat",
43            "get-content",
44        ]
45    }
46
47    fn signature(&self) -> nu_protocol::Signature {
48        Signature::build("open")
49            .input_output_types(vec![
50                (Type::Nothing, Type::Any),
51                (Type::String, Type::Any),
52                // FIXME Type::Any input added to disable pipeline input type checking, as run-time checks can raise undesirable type errors
53                // which aren't caught by the parser. see https://github.com/nushell/nushell/pull/14922 for more details
54                (Type::Any, Type::Any),
55            ])
56            .rest(
57                "files",
58                SyntaxShape::OneOf(vec![SyntaxShape::GlobPattern, SyntaxShape::String]),
59                "The file(s) to open.",
60            )
61            .switch("raw", "open file as raw binary", Some('r'))
62            .category(Category::FileSystem)
63    }
64
65    fn run(
66        &self,
67        engine_state: &EngineState,
68        stack: &mut Stack,
69        call: &Call,
70        input: PipelineData,
71    ) -> Result<PipelineData, ShellError> {
72        let raw = call.has_flag(engine_state, stack, "raw")?;
73        let call_span = call.head;
74        #[allow(deprecated)]
75        let cwd = current_dir(engine_state, stack)?;
76        let mut paths = call.rest::<Spanned<NuGlob>>(engine_state, stack, 0)?;
77
78        if paths.is_empty() && !call.has_positional_args(stack, 0) {
79            // try to use path from pipeline input if there were no positional or spread args
80            let (filename, span) = match input {
81                PipelineData::Value(val, ..) => {
82                    let span = val.span();
83                    (val.coerce_into_string()?, span)
84                }
85                _ => {
86                    return Err(ShellError::MissingParameter {
87                        param_name: "needs filename".to_string(),
88                        span: call.head,
89                    });
90                }
91            };
92
93            paths.push(Spanned {
94                item: NuGlob::Expand(filename),
95                span,
96            });
97        }
98
99        let mut output = vec![];
100
101        for mut path in paths {
102            //FIXME: `open` should not have to do this
103            path.item = path.item.strip_ansi_string_unlikely();
104
105            let arg_span = path.span;
106            // let path_no_whitespace = &path.item.trim_end_matches(|x| matches!(x, '\x09'..='\x0d'));
107
108            let matches: Box<dyn Iterator<Item = Result<PathBuf, ShellError>> + Send> =
109                if is_windows_device_path(Path::new(&path.item.to_string())) {
110                    Box::new(vec![Ok(PathBuf::from(path.item.to_string()))].into_iter())
111                } else {
112                    nu_engine::glob_from(
113                        &path,
114                        &cwd,
115                        call_span,
116                        None,
117                        engine_state.signals().clone(),
118                    )
119                    .map_err(|err| match err {
120                        ShellError::Io(mut err) => {
121                            err.kind = err.kind.not_found_as(NotFound::File);
122                            err.span = arg_span;
123                            err.into()
124                        }
125                        _ => err,
126                    })?
127                    .1
128                };
129            for path in matches {
130                let path = path?;
131                let path = Path::new(&path);
132
133                if permission_denied(path) {
134                    let err = IoError::new(
135                        shell_error::io::ErrorKind::from_std(std::io::ErrorKind::PermissionDenied),
136                        arg_span,
137                        PathBuf::from(path),
138                    );
139
140                    #[cfg(unix)]
141                    let err = {
142                        let mut err = err;
143                        err.additional_context = Some(
144                            match path.metadata() {
145                                Ok(md) => format!(
146                                    "The permissions of {:o} does not allow access for this user",
147                                    md.permissions().mode() & 0o0777
148                                ),
149                                Err(e) => e.to_string(),
150                            }
151                            .into(),
152                        );
153                        err
154                    };
155
156                    return Err(err.into());
157                } else {
158                    #[cfg(feature = "sqlite")]
159                    if !raw {
160                        let res = SQLiteDatabase::try_from_path(
161                            path,
162                            arg_span,
163                            engine_state.signals().clone(),
164                        )
165                        .map(|db| db.into_value(call.head).into_pipeline_data());
166
167                        if res.is_ok() {
168                            return res;
169                        }
170                    }
171
172                    if path.is_dir() {
173                        // At least under windows this check ensures that we don't get a
174                        // permission denied error on directories
175                        return Err(ShellError::Io(IoError::new(
176                            #[allow(
177                                deprecated,
178                                reason = "we don't have a IsADirectory variant here, so we provide one"
179                            )]
180                            shell_error::io::ErrorKind::from_std(std::io::ErrorKind::IsADirectory),
181                            arg_span,
182                            PathBuf::from(path),
183                        )));
184                    }
185
186                    let file = std::fs::File::open(path)
187                        .map_err(|err| IoError::new(err, arg_span, PathBuf::from(path)))?;
188
189                    // No content_type by default - Is added later if no converter is found
190                    let stream = PipelineData::byte_stream(
191                        ByteStream::file(file, call_span, engine_state.signals().clone()),
192                        Some(PipelineMetadata {
193                            data_source: DataSource::FilePath(path.to_path_buf()),
194                            ..Default::default()
195                        }),
196                    );
197
198                    let exts_opt: Option<Vec<String>> = if raw {
199                        None
200                    } else {
201                        let path_str = path
202                            .file_name()
203                            .unwrap_or(std::ffi::OsStr::new(path))
204                            .to_string_lossy()
205                            .to_lowercase();
206                        Some(extract_extensions(path_str.as_str()))
207                    };
208
209                    let converter = exts_opt.and_then(|exts| {
210                        exts.iter().find_map(|ext| {
211                            engine_state
212                                .find_decl(format!("from {ext}").as_bytes(), &[])
213                                .map(|id| (id, ext.to_string()))
214                        })
215                    });
216
217                    match converter {
218                        Some((converter_id, ext)) => {
219                            let open_call = ast::Call {
220                                decl_id: converter_id,
221                                head: call_span,
222                                arguments: vec![],
223                                parser_info: HashMap::new(),
224                            };
225                            let command_output = if engine_state.is_debugging() {
226                                eval_call::<WithDebug>(engine_state, stack, &open_call, stream)
227                            } else {
228                                eval_call::<WithoutDebug>(engine_state, stack, &open_call, stream)
229                            };
230                            output.push(command_output.map_err(|inner| {
231                                    ShellError::GenericError{
232                                        error: format!("Error while parsing as {ext}"),
233                                        msg: format!("Could not parse '{}' with `from {}`", path.display(), ext),
234                                        span: Some(arg_span),
235                                        help: Some(format!("Check out `help from {}` or `help from` for more options or open raw data with `open --raw '{}'`", ext, path.display())),
236                                        inner: vec![inner],
237                                }
238                                })?);
239                        }
240                        None => {
241                            // If no converter was found, add content-type metadata
242                            let content_type = path
243                                .extension()
244                                .map(|ext| ext.to_string_lossy().to_string())
245                                .and_then(|ref s| detect_content_type(s));
246
247                            let stream_with_content_type =
248                                stream.set_metadata(Some(PipelineMetadata {
249                                    data_source: DataSource::FilePath(path.to_path_buf()),
250                                    content_type,
251                                    ..Default::default()
252                                }));
253                            output.push(stream_with_content_type);
254                        }
255                    }
256                }
257            }
258        }
259
260        if output.is_empty() {
261            Ok(PipelineData::empty())
262        } else if output.len() == 1 {
263            Ok(output.remove(0))
264        } else {
265            Ok(output
266                .into_iter()
267                .flatten()
268                .into_pipeline_data(call_span, engine_state.signals().clone()))
269        }
270    }
271
272    fn examples(&self) -> Vec<nu_protocol::Example<'_>> {
273        vec![
274            Example {
275                description: "Open a file, with structure (based on file extension or SQLite database header)",
276                example: "open myfile.json",
277                result: None,
278            },
279            Example {
280                description: "Open a file, as raw bytes",
281                example: "open myfile.json --raw",
282                result: None,
283            },
284            Example {
285                description: "Open a file, using the input to get filename",
286                example: "'myfile.txt' | open",
287                result: None,
288            },
289            Example {
290                description: "Open a file, and decode it by the specified encoding",
291                example: "open myfile.txt --raw | decode utf-8",
292                result: None,
293            },
294            Example {
295                description: "Create a custom `from` parser to open newline-delimited JSON files with `open`",
296                example: r#"def "from ndjson" [] { from json -o }; open myfile.ndjson"#,
297                result: None,
298            },
299            Example {
300                description: "Show the extensions for which the `open` command will automatically parse",
301                example: r#"scope commands
302    | where name starts-with "from "
303    | insert extension { get name | str replace -r "^from " "" | $"*.($in)" }
304    | select extension name
305    | rename extension command
306"#,
307                result: None,
308            },
309        ]
310    }
311}
312
313fn permission_denied(dir: impl AsRef<Path>) -> bool {
314    match dir.as_ref().read_dir() {
315        Err(e) => matches!(e.kind(), std::io::ErrorKind::PermissionDenied),
316        Ok(_) => false,
317    }
318}
319
320fn extract_extensions(filename: &str) -> Vec<String> {
321    let parts: Vec<&str> = filename.split('.').collect();
322    let mut extensions: Vec<String> = Vec::new();
323    let mut current_extension = String::new();
324
325    for part in parts.iter().rev() {
326        if current_extension.is_empty() {
327            current_extension.push_str(part);
328        } else {
329            current_extension = format!("{part}.{current_extension}");
330        }
331        extensions.push(current_extension.clone());
332    }
333
334    extensions.pop();
335    extensions.reverse();
336
337    extensions
338}
339
340fn detect_content_type(extension: &str) -> Option<String> {
341    // This will allow the overriding of metadata to be consistent with
342    // the content type
343    match extension {
344        // Per RFC-9512, application/yaml should be used
345        "yaml" | "yml" => Some("application/yaml".to_string()),
346        "nu" => Some("application/x-nuscript".to_string()),
347        "json" | "jsonl" | "ndjson" => Some("application/json".to_string()),
348        "nuon" => Some("application/x-nuon".to_string()),
349        _ => mime_guess::from_ext(extension)
350            .first()
351            .map(|mime| mime.to_string()),
352    }
353}
354
355#[cfg(test)]
356mod test {
357
358    #[test]
359    fn test_content_type() {}
360}