nu_command/filesystem/
open.rs

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