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 (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 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 path.item = path.item.strip_ansi_string_unlikely();
104
105 let arg_span = path.span;
106 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 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 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 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 match extension {
344 "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}