1#[allow(deprecated)]
2use nu_engine::{command_prelude::*, current_dir, eval_call};
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 #[allow(deprecated)]
74 let cwd = current_dir(engine_state, stack)?;
75 let mut paths = call.rest::<Spanned<NuGlob>>(engine_state, stack, 0)?;
76
77 if paths.is_empty() && !call.has_positional_args(stack, 0) {
78 let (filename, span) = match input {
80 PipelineData::Value(val, ..) => {
81 let span = val.span();
82 (val.coerce_into_string()?, span)
83 }
84 _ => {
85 return Err(ShellError::MissingParameter {
86 param_name: "needs filename".to_string(),
87 span: call.head,
88 });
89 }
90 };
91
92 paths.push(Spanned {
93 item: NuGlob::Expand(filename),
94 span,
95 });
96 }
97
98 let mut output = vec![];
99
100 for mut path in paths {
101 path.item = path.item.strip_ansi_string_unlikely();
103
104 let arg_span = path.span;
105 for path in
108 nu_engine::glob_from(&path, &cwd, call_span, None, engine_state.signals().clone())
109 .map_err(|err| match err {
110 ShellError::Io(mut err) => {
111 err.kind = err.kind.not_found_as(NotFound::File);
112 err.span = arg_span;
113 err.into()
114 }
115 _ => err,
116 })?
117 .1
118 {
119 let path = path?;
120 let path = Path::new(&path);
121
122 if permission_denied(path) {
123 let err = IoError::new(
124 shell_error::io::ErrorKind::from_std(std::io::ErrorKind::PermissionDenied),
125 arg_span,
126 PathBuf::from(path),
127 );
128
129 #[cfg(unix)]
130 let err = {
131 let mut err = err;
132 err.additional_context = Some(
133 match path.metadata() {
134 Ok(md) => format!(
135 "The permissions of {:o} does not allow access for this user",
136 md.permissions().mode() & 0o0777
137 ),
138 Err(e) => e.to_string(),
139 }
140 .into(),
141 );
142 err
143 };
144
145 return Err(err.into());
146 } else {
147 #[cfg(feature = "sqlite")]
148 if !raw {
149 let res = SQLiteDatabase::try_from_path(
150 path,
151 arg_span,
152 engine_state.signals().clone(),
153 )
154 .map(|db| db.into_value(call.head).into_pipeline_data());
155
156 if res.is_ok() {
157 return res;
158 }
159 }
160
161 if path.is_dir() {
162 return Err(ShellError::Io(IoError::new(
165 #[allow(
166 deprecated,
167 reason = "we don't have a IsADirectory variant here, so we provide one"
168 )]
169 shell_error::io::ErrorKind::from_std(std::io::ErrorKind::IsADirectory),
170 arg_span,
171 PathBuf::from(path),
172 )));
173 }
174
175 let file = std::fs::File::open(path)
176 .map_err(|err| IoError::new(err, arg_span, PathBuf::from(path)))?;
177
178 let stream = PipelineData::ByteStream(
180 ByteStream::file(file, call_span, engine_state.signals().clone()),
181 Some(PipelineMetadata {
182 data_source: DataSource::FilePath(path.to_path_buf()),
183 content_type: None,
184 }),
185 );
186
187 let exts_opt: Option<Vec<String>> = if raw {
188 None
189 } else {
190 let path_str = path
191 .file_name()
192 .unwrap_or(std::ffi::OsStr::new(path))
193 .to_string_lossy()
194 .to_lowercase();
195 Some(extract_extensions(path_str.as_str()))
196 };
197
198 let converter = exts_opt.and_then(|exts| {
199 exts.iter().find_map(|ext| {
200 engine_state
201 .find_decl(format!("from {}", ext).as_bytes(), &[])
202 .map(|id| (id, ext.to_string()))
203 })
204 });
205
206 match converter {
207 Some((converter_id, ext)) => {
208 let open_call = ast::Call {
209 decl_id: converter_id,
210 head: call_span,
211 arguments: vec![],
212 parser_info: HashMap::new(),
213 };
214 let command_output = if engine_state.is_debugging() {
215 eval_call::<WithDebug>(engine_state, stack, &open_call, stream)
216 } else {
217 eval_call::<WithoutDebug>(engine_state, stack, &open_call, stream)
218 };
219 output.push(command_output.map_err(|inner| {
220 ShellError::GenericError{
221 error: format!("Error while parsing as {ext}"),
222 msg: format!("Could not parse '{}' with `from {}`", path.display(), ext),
223 span: Some(arg_span),
224 help: Some(format!("Check out `help from {}` or `help from` for more options or open raw data with `open --raw '{}'`", ext, path.display())),
225 inner: vec![inner],
226 }
227 })?);
228 }
229 None => {
230 let content_type = path
232 .extension()
233 .map(|ext| ext.to_string_lossy().to_string())
234 .and_then(|ref s| detect_content_type(s));
235
236 let stream_with_content_type =
237 stream.set_metadata(Some(PipelineMetadata {
238 data_source: DataSource::FilePath(path.to_path_buf()),
239 content_type,
240 }));
241 output.push(stream_with_content_type);
242 }
243 }
244 }
245 }
246 }
247
248 if output.is_empty() {
249 Ok(PipelineData::Empty)
250 } else if output.len() == 1 {
251 Ok(output.remove(0))
252 } else {
253 Ok(output
254 .into_iter()
255 .flatten()
256 .into_pipeline_data(call_span, engine_state.signals().clone()))
257 }
258 }
259
260 fn examples(&self) -> Vec<nu_protocol::Example> {
261 vec![
262 Example {
263 description: "Open a file, with structure (based on file extension or SQLite database header)",
264 example: "open myfile.json",
265 result: None,
266 },
267 Example {
268 description: "Open a file, as raw bytes",
269 example: "open myfile.json --raw",
270 result: None,
271 },
272 Example {
273 description: "Open a file, using the input to get filename",
274 example: "'myfile.txt' | open",
275 result: None,
276 },
277 Example {
278 description: "Open a file, and decode it by the specified encoding",
279 example: "open myfile.txt --raw | decode utf-8",
280 result: None,
281 },
282 Example {
283 description: "Create a custom `from` parser to open newline-delimited JSON files with `open`",
284 example: r#"def "from ndjson" [] { from json -o }; open myfile.ndjson"#,
285 result: None,
286 },
287 Example {
288 description: "Show the extensions for which the `open` command will automatically parse",
289 example: r#"scope commands
290 | where name starts-with "from "
291 | insert extension { get name | str replace -r "^from " "" | $"*.($in)" }
292 | select extension name
293 | rename extension command
294"#,
295 result: None,
296 },
297 ]
298 }
299}
300
301fn permission_denied(dir: impl AsRef<Path>) -> bool {
302 match dir.as_ref().read_dir() {
303 Err(e) => matches!(e.kind(), std::io::ErrorKind::PermissionDenied),
304 Ok(_) => false,
305 }
306}
307
308fn extract_extensions(filename: &str) -> Vec<String> {
309 let parts: Vec<&str> = filename.split('.').collect();
310 let mut extensions: Vec<String> = Vec::new();
311 let mut current_extension = String::new();
312
313 for part in parts.iter().rev() {
314 if current_extension.is_empty() {
315 current_extension.push_str(part);
316 } else {
317 current_extension = format!("{}.{}", part, current_extension);
318 }
319 extensions.push(current_extension.clone());
320 }
321
322 extensions.pop();
323 extensions.reverse();
324
325 extensions
326}
327
328fn detect_content_type(extension: &str) -> Option<String> {
329 match extension {
332 "yaml" | "yml" => Some("application/yaml".to_string()),
334 "nu" => Some("application/x-nuscript".to_string()),
335 "json" | "jsonl" | "ndjson" => Some("application/json".to_string()),
336 "nuon" => Some("application/x-nuon".to_string()),
337 _ => mime_guess::from_ext(extension)
338 .first()
339 .map(|mime| mime.to_string()),
340 }
341}
342
343#[cfg(test)]
344mod test {
345
346 #[test]
347 fn test_content_type() {}
348}