1use std::ops::Not;
2
3use crate::{
4 cmd::glob::{GlobOptions, expand_path},
5 globals::{get_pwd, get_vfs},
6};
7use nu_command::{FromCsv, FromJson, FromOds, FromToml, FromTsv, FromXlsx, FromXml, FromYaml};
8use nu_engine::CallExt;
9use nu_protocol::{
10 ByteStream, Category, ListStream, PipelineData, ShellError, Signature, SyntaxShape, Type,
11 Value,
12 engine::{Command, EngineState, Stack},
13};
14use std::sync::Arc;
15
16#[derive(Clone)]
17pub struct Open;
18
19impl Command for Open {
20 fn name(&self) -> &str {
21 "open"
22 }
23
24 fn signature(&self) -> Signature {
25 Signature::build("open")
26 .required(
27 "path",
28 SyntaxShape::OneOf(vec![SyntaxShape::Filepath, SyntaxShape::GlobPattern]),
29 "path to the file",
30 )
31 .switch(
32 "raw",
33 "output content as raw string/binary without parsing",
34 Some('r'),
35 )
36 .input_output_type(Type::Nothing, Type::one_of([Type::String, Type::Binary]))
37 .category(Category::FileSystem)
38 }
39
40 fn description(&self) -> &str {
41 "open a file from the virtual filesystem."
42 }
43
44 fn run(
45 &self,
46 engine_state: &EngineState,
47 stack: &mut Stack,
48 call: &nu_protocol::engine::Call,
49 _input: PipelineData,
50 ) -> Result<PipelineData, ShellError> {
51 let path_value: Value = call.req(engine_state, stack, 0)?;
52 let raw_flag = call.has_flag(engine_state, stack, "raw")?;
53
54 let path_str = match path_value {
55 Value::String { val, .. } | Value::Glob { val, .. } => val,
56 _ => {
57 return Err(ShellError::GenericError {
58 error: "invalid path".into(),
59 msg: "path must be a string or glob pattern".into(),
60 span: Some(call.head),
61 help: None,
62 inner: vec![],
63 });
64 }
65 };
66
67 // Expand path (glob or single) into list of paths
68 let is_absolute = path_str.starts_with('/');
69 let base_path: Arc<vfs::VfsPath> = if is_absolute { get_vfs() } else { get_pwd() };
70
71 let options = GlobOptions {
72 max_depth: None,
73 no_dirs: true, // Only open files, not directories
74 no_files: false,
75 };
76
77 let matches = expand_path(&path_str, base_path.clone(), options)?;
78
79 let span = call.head;
80 let signals = engine_state.signals().clone();
81
82 // Open each matching file
83 let mut results = Vec::new();
84 for rel_path in matches {
85 let target_file = match base_path.join(&rel_path) {
86 Ok(p) => p,
87 Err(e) => {
88 results.push(Value::error(
89 ShellError::GenericError {
90 error: "path error".into(),
91 msg: e.to_string(),
92 span: Some(span),
93 help: None,
94 inner: vec![],
95 },
96 span,
97 ));
98 continue;
99 }
100 };
101
102 let parse_cmd = raw_flag
103 .not()
104 .then(|| {
105 target_file
106 .extension()
107 .and_then(|ext| get_cmd_for_ext(&ext))
108 })
109 .flatten();
110
111 match target_file.open_file() {
112 Ok(f) => {
113 let data = PipelineData::ByteStream(
114 ByteStream::read(
115 f,
116 span,
117 signals.clone(),
118 nu_protocol::ByteStreamType::String,
119 ),
120 None,
121 );
122
123 let value = if let Some(cmd) = parse_cmd {
124 match cmd.run(engine_state, stack, call, data) {
125 Ok(pipeline_data) => {
126 // Convert pipeline data to value
127 pipeline_data
128 .into_value(span)
129 .unwrap_or_else(|e| Value::error(e, span))
130 }
131 Err(e) => Value::error(e, span),
132 }
133 } else {
134 data.into_value(span)
135 .unwrap_or_else(|e| Value::error(e, span))
136 };
137 results.push(value);
138 }
139 Err(e) => {
140 results.push(Value::error(
141 ShellError::GenericError {
142 error: "io error".into(),
143 msg: format!("failed to open file {}: {}", rel_path, e),
144 span: Some(span),
145 help: None,
146 inner: vec![],
147 },
148 span,
149 ));
150 }
151 }
152 }
153
154 // If single file, return the single result directly (for backward compatibility)
155 if results.len() == 1
156 && !path_str.contains('*')
157 && !path_str.contains('?')
158 && !path_str.contains('[')
159 && !path_str.contains("**")
160 {
161 match results.into_iter().next().unwrap() {
162 Value::Error { error, .. } => Err(*error),
163 val => Ok(PipelineData::Value(val, None)),
164 }
165 } else {
166 Ok(PipelineData::list_stream(
167 ListStream::new(results.into_iter(), span, signals.clone()),
168 None,
169 ))
170 }
171 }
172}
173
174fn get_cmd_for_ext(ext: &str) -> Option<Box<dyn Command>> {
175 Some(match ext {
176 "json" => Box::new(FromJson),
177 "yaml" | "yml" => Box::new(FromYaml),
178 "toml" => Box::new(FromToml),
179 "csv" => Box::new(FromCsv),
180 "ods" => Box::new(FromOds),
181 "tsv" => Box::new(FromTsv),
182 "xml" => Box::new(FromXml),
183 "xlsx" => Box::new(FromXlsx),
184 _ => return None,
185 })
186}