nu_command/filesystem/
open.rs

1#[allow(deprecated)]
2use nu_engine::{command_prelude::*, current_dir, eval_call};
3use nu_protocol::{
4    ast,
5    debugger::{WithDebug, WithoutDebug},
6    shell_error::{self, io::IoError},
7    DataSource, NuGlob, PipelineMetadata,
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            for path in
109                nu_engine::glob_from(&path, &cwd, call_span, None, engine_state.signals().clone())
110                    .map_err(|err| match err {
111                        ShellError::Io(mut err) => {
112                            err.kind = err.kind.not_found_as(NotFound::File);
113                            err.span = arg_span;
114                            err.into()
115                        }
116                        _ => err,
117                    })?
118                    .1
119            {
120                let path = path?;
121                let path = Path::new(&path);
122
123                if permission_denied(path) {
124                    let err = IoError::new(
125                        std::io::ErrorKind::PermissionDenied,
126                        arg_span,
127                        PathBuf::from(path),
128                    );
129
130                    #[cfg(unix)]
131                    let err = {
132                        let mut err = err;
133                        err.additional_context = Some(
134                            match path.metadata() {
135                                Ok(md) => format!(
136                                    "The permissions of {:o} does not allow access for this user",
137                                    md.permissions().mode() & 0o0777
138                                ),
139                                Err(e) => e.to_string(),
140                            }
141                            .into(),
142                        );
143                        err
144                    };
145
146                    return Err(err.into());
147                } else {
148                    #[cfg(feature = "sqlite")]
149                    if !raw {
150                        let res = SQLiteDatabase::try_from_path(
151                            path,
152                            arg_span,
153                            engine_state.signals().clone(),
154                        )
155                        .map(|db| db.into_value(call.head).into_pipeline_data());
156
157                        if res.is_ok() {
158                            return res;
159                        }
160                    }
161
162                    if path.is_dir() {
163                        // At least under windows this check ensures that we don't get a
164                        // permission denied error on directories
165                        return Err(ShellError::Io(IoError::new(
166                            shell_error::io::ErrorKind::Std(std::io::ErrorKind::IsADirectory),
167                            arg_span,
168                            PathBuf::from(path),
169                        )));
170                    }
171
172                    let file = std::fs::File::open(path)
173                        .map_err(|err| IoError::new(err.kind(), arg_span, PathBuf::from(path)))?;
174
175                    // No content_type by default - Is added later if no converter is found
176                    let stream = PipelineData::ByteStream(
177                        ByteStream::file(file, call_span, engine_state.signals().clone()),
178                        Some(PipelineMetadata {
179                            data_source: DataSource::FilePath(path.to_path_buf()),
180                            content_type: None,
181                        }),
182                    );
183
184                    let exts_opt: Option<Vec<String>> = if raw {
185                        None
186                    } else {
187                        let path_str = path
188                            .file_name()
189                            .unwrap_or(std::ffi::OsStr::new(path))
190                            .to_string_lossy()
191                            .to_lowercase();
192                        Some(extract_extensions(path_str.as_str()))
193                    };
194
195                    let converter = exts_opt.and_then(|exts| {
196                        exts.iter().find_map(|ext| {
197                            engine_state
198                                .find_decl(format!("from {}", ext).as_bytes(), &[])
199                                .map(|id| (id, ext.to_string()))
200                        })
201                    });
202
203                    match converter {
204                        Some((converter_id, ext)) => {
205                            let open_call = ast::Call {
206                                decl_id: converter_id,
207                                head: call_span,
208                                arguments: vec![],
209                                parser_info: HashMap::new(),
210                            };
211                            let command_output = if engine_state.is_debugging() {
212                                eval_call::<WithDebug>(engine_state, stack, &open_call, stream)
213                            } else {
214                                eval_call::<WithoutDebug>(engine_state, stack, &open_call, stream)
215                            };
216                            output.push(command_output.map_err(|inner| {
217                                    ShellError::GenericError{
218                                        error: format!("Error while parsing as {ext}"),
219                                        msg: format!("Could not parse '{}' with `from {}`", path.display(), ext),
220                                        span: Some(arg_span),
221                                        help: Some(format!("Check out `help from {}` or `help from` for more options or open raw data with `open --raw '{}'`", ext, path.display())),
222                                        inner: vec![inner],
223                                }
224                                })?);
225                        }
226                        None => {
227                            // If no converter was found, add content-type metadata
228                            let content_type = path
229                                .extension()
230                                .map(|ext| ext.to_string_lossy().to_string())
231                                .and_then(|ref s| detect_content_type(s));
232
233                            let stream_with_content_type =
234                                stream.set_metadata(Some(PipelineMetadata {
235                                    data_source: DataSource::FilePath(path.to_path_buf()),
236                                    content_type,
237                                }));
238                            output.push(stream_with_content_type);
239                        }
240                    }
241                }
242            }
243        }
244
245        if output.is_empty() {
246            Ok(PipelineData::Empty)
247        } else if output.len() == 1 {
248            Ok(output.remove(0))
249        } else {
250            Ok(output
251                .into_iter()
252                .flatten()
253                .into_pipeline_data(call_span, engine_state.signals().clone()))
254        }
255    }
256
257    fn examples(&self) -> Vec<nu_protocol::Example> {
258        vec![
259            Example {
260                description: "Open a file, with structure (based on file extension or SQLite database header)",
261                example: "open myfile.json",
262                result: None,
263            },
264            Example {
265                description: "Open a file, as raw bytes",
266                example: "open myfile.json --raw",
267                result: None,
268            },
269            Example {
270                description: "Open a file, using the input to get filename",
271                example: "'myfile.txt' | open",
272                result: None,
273            },
274            Example {
275                description: "Open a file, and decode it by the specified encoding",
276                example: "open myfile.txt --raw | decode utf-8",
277                result: None,
278            },
279            Example {
280                description: "Create a custom `from` parser to open newline-delimited JSON files with `open`",
281                example: r#"def "from ndjson" [] { from json -o }; open myfile.ndjson"#,
282                result: None,
283            },
284        ]
285    }
286}
287
288fn permission_denied(dir: impl AsRef<Path>) -> bool {
289    match dir.as_ref().read_dir() {
290        Err(e) => matches!(e.kind(), std::io::ErrorKind::PermissionDenied),
291        Ok(_) => false,
292    }
293}
294
295fn extract_extensions(filename: &str) -> Vec<String> {
296    let parts: Vec<&str> = filename.split('.').collect();
297    let mut extensions: Vec<String> = Vec::new();
298    let mut current_extension = String::new();
299
300    for part in parts.iter().rev() {
301        if current_extension.is_empty() {
302            current_extension.push_str(part);
303        } else {
304            current_extension = format!("{}.{}", part, current_extension);
305        }
306        extensions.push(current_extension.clone());
307    }
308
309    extensions.pop();
310    extensions.reverse();
311
312    extensions
313}
314
315fn detect_content_type(extension: &str) -> Option<String> {
316    // This will allow the overriding of metadata to be consistent with
317    // the content type
318    match extension {
319        // Per RFC-9512, application/yaml should be used
320        "yaml" | "yml" => Some("application/yaml".to_string()),
321        "nu" => Some("application/x-nuscript".to_string()),
322        "json" | "jsonl" | "ndjson" => Some("application/json".to_string()),
323        "nuon" => Some("application/x-nuon".to_string()),
324        _ => mime_guess::from_ext(extension)
325            .first()
326            .map(|mime| mime.to_string()),
327    }
328}
329
330#[cfg(test)]
331mod test {
332
333    #[test]
334    fn test_content_type() {}
335}