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