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