at main 6.3 kB view raw
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}