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