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, 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::GenericError{
230 error: format!("Error while parsing as {ext}"),
231 msg: format!("Could not parse '{}' with `from {}`", path.display(), ext),
232 span: Some(arg_span),
233 help: Some(format!("Check out `help from {}` or `help from` for more options or open raw data with `open --raw '{}'`", ext, path.display())),
234 inner: vec![inner],
235 }
236 })?);
237 }
238 None => {
239 let content_type = path
241 .extension()
242 .map(|ext| ext.to_string_lossy().to_string())
243 .and_then(|ref s| detect_content_type(s));
244
245 let stream_with_content_type =
246 stream.set_metadata(Some(PipelineMetadata {
247 data_source: DataSource::FilePath(path.to_path_buf()),
248 content_type,
249 ..Default::default()
250 }));
251 output.push(stream_with_content_type);
252 }
253 }
254 }
255 }
256 }
257
258 if output.is_empty() {
259 Ok(PipelineData::empty())
260 } else if output.len() == 1 {
261 Ok(output.remove(0))
262 } else {
263 Ok(output
264 .into_iter()
265 .flatten()
266 .into_pipeline_data(call_span, engine_state.signals().clone()))
267 }
268 }
269
270 fn examples(&self) -> Vec<nu_protocol::Example<'_>> {
271 vec![
272 Example {
273 description: "Open a file, with structure (based on file extension or SQLite database header)",
274 example: "open myfile.json",
275 result: None,
276 },
277 Example {
278 description: "Open a file, as raw bytes",
279 example: "open myfile.json --raw",
280 result: None,
281 },
282 Example {
283 description: "Open a file, using the input to get filename",
284 example: "'myfile.txt' | open",
285 result: None,
286 },
287 Example {
288 description: "Open a file, and decode it by the specified encoding",
289 example: "open myfile.txt --raw | decode utf-8",
290 result: None,
291 },
292 Example {
293 description: "Create a custom `from` parser to open newline-delimited JSON files with `open`",
294 example: r#"def "from ndjson" [] { from json -o }; open myfile.ndjson"#,
295 result: None,
296 },
297 Example {
298 description: "Show the extensions for which the `open` command will automatically parse",
299 example: r#"scope commands
300 | where name starts-with "from "
301 | insert extension { get name | str replace -r "^from " "" | $"*.($in)" }
302 | select extension name
303 | rename extension command
304"#,
305 result: None,
306 },
307 ]
308 }
309}
310
311fn permission_denied(dir: impl AsRef<Path>) -> bool {
312 match dir.as_ref().read_dir() {
313 Err(e) => matches!(e.kind(), std::io::ErrorKind::PermissionDenied),
314 Ok(_) => false,
315 }
316}
317
318fn extract_extensions(filename: &str) -> Vec<String> {
319 let parts: Vec<&str> = filename.split('.').collect();
320 let mut extensions: Vec<String> = Vec::new();
321 let mut current_extension = String::new();
322
323 for part in parts.iter().rev() {
324 if current_extension.is_empty() {
325 current_extension.push_str(part);
326 } else {
327 current_extension = format!("{part}.{current_extension}");
328 }
329 extensions.push(current_extension.clone());
330 }
331
332 extensions.pop();
333 extensions.reverse();
334
335 extensions
336}
337
338fn detect_content_type(extension: &str) -> Option<String> {
339 match extension {
342 "yaml" | "yml" => Some("application/yaml".to_string()),
344 "nu" => Some("application/x-nuscript".to_string()),
345 "json" | "jsonl" | "ndjson" => Some("application/json".to_string()),
346 "nuon" => Some("application/x-nuon".to_string()),
347 _ => mime_guess::from_ext(extension)
348 .first()
349 .map(|mime| mime.to_string()),
350 }
351}
352
353#[cfg(test)]
354mod test {
355
356 #[test]
357 fn test_content_type() {}
358}