at main 7.7 kB view raw
1use std::time::{SystemTime, UNIX_EPOCH}; 2 3use crate::{ 4 cmd::glob::{GlobOptions, expand_path}, 5 error::to_shell_err, 6 globals::{get_pwd, get_vfs}, 7}; 8use jacquard::chrono; 9use nu_engine::CallExt; 10use nu_protocol::{ 11 Category, ListStream, PipelineData, Record, ShellError, Signature, SyntaxShape, Type, Value, 12 engine::{Command, EngineState, Stack}, 13}; 14use std::sync::Arc; 15 16#[derive(Clone)] 17pub struct Ls; 18 19impl Command for Ls { 20 fn name(&self) -> &str { 21 "ls" 22 } 23 24 fn signature(&self) -> Signature { 25 Signature::build("ls") 26 .optional( 27 "path", 28 SyntaxShape::OneOf(vec![SyntaxShape::Filepath, SyntaxShape::GlobPattern]), 29 "the path to list", 30 ) 31 .switch( 32 "all", 33 "include hidden paths (that start with a dot)", 34 Some('a'), 35 ) 36 .switch( 37 "long", 38 "show detailed information about each file", 39 Some('l'), 40 ) 41 .switch("full-paths", "display paths as absolute paths", Some('f')) 42 .input_output_type(Type::Nothing, Type::table()) 43 .category(Category::FileSystem) 44 } 45 46 fn description(&self) -> &str { 47 "list the files in the virtual filesystem." 48 } 49 50 fn run( 51 &self, 52 engine_state: &EngineState, 53 stack: &mut Stack, 54 call: &nu_protocol::engine::Call, 55 _input: PipelineData, 56 ) -> Result<PipelineData, ShellError> { 57 let path_arg: Option<Value> = call.opt(engine_state, stack, 0)?; 58 let all = call.has_flag(engine_state, stack, "all")?; 59 let long = call.has_flag(engine_state, stack, "long")?; 60 let full_paths = call.has_flag(engine_state, stack, "full-paths")?; 61 62 let pwd = get_pwd(); 63 let span = call.head; 64 65 // If no path provided, list current directory 66 let (matches, base_path) = if let Some(path_val) = &path_arg { 67 let path_str = match path_val { 68 Value::String { val, .. } | Value::Glob { val, .. } => val, 69 _ => { 70 return Err(ShellError::GenericError { 71 error: "invalid path".into(), 72 msg: "path must be a string or glob pattern".into(), 73 span: Some(call.arguments_span()), 74 help: None, 75 inner: vec![], 76 }); 77 } 78 }; 79 80 let is_absolute = path_str.starts_with('/'); 81 let base_path: Arc<vfs::VfsPath> = if is_absolute { get_vfs() } else { pwd.clone() }; 82 83 // Check if it's a glob pattern 84 let is_glob = path_str.contains('*') 85 || path_str.contains('?') 86 || path_str.contains('[') 87 || path_str.contains("**"); 88 89 if is_glob { 90 // Glob pattern: expand and list matching paths 91 let options = GlobOptions { 92 max_depth: None, 93 no_dirs: false, 94 no_files: false, 95 }; 96 let matches = expand_path(path_str, base_path.clone(), options)?; 97 (matches, base_path) 98 } else { 99 // Non-glob path: check if it's a directory and list its contents 100 let normalized_path = path_str.trim_start_matches('/').trim_end_matches('/'); 101 let target_path = base_path 102 .join(normalized_path) 103 .map_err(to_shell_err(call.arguments_span()))?; 104 105 let metadata = target_path.metadata().map_err(to_shell_err(span))?; 106 match metadata.file_type { 107 vfs::VfsFileType::Directory => { 108 // List directory contents 109 let entries = target_path.read_dir().map_err(to_shell_err(span))?; 110 let matches: Vec<String> = entries 111 .map(|e| { 112 // Build relative path from base_path 113 let entry_name = e.filename(); 114 if normalized_path.is_empty() || normalized_path == "." { 115 entry_name 116 } else { 117 format!("{}/{}", normalized_path, entry_name) 118 } 119 }) 120 .collect(); 121 (matches, base_path) 122 } 123 vfs::VfsFileType::File => { 124 // Single file: return just this file (normalized, relative to base_path) 125 (vec![normalized_path.to_string()], base_path) 126 } 127 } 128 } 129 } else { 130 // No path: list current directory entries 131 let entries = pwd.read_dir().map_err(to_shell_err(span))?; 132 let matches: Vec<String> = entries.map(|e| e.filename()).collect(); 133 (matches, pwd.clone()) 134 }; 135 136 let make_record = move |rel_path: &str| { 137 let full_path = base_path.join(rel_path).map_err(to_shell_err(span))?; 138 let metadata = full_path.metadata().map_err(to_shell_err(span))?; 139 140 // Filter hidden files if --all is not set 141 let filename = rel_path.split('/').last().unwrap_or(rel_path); 142 if filename.starts_with('.') && !all { 143 return Ok(None); 144 } 145 146 let type_str = match metadata.file_type { 147 vfs::VfsFileType::Directory => "dir", 148 vfs::VfsFileType::File => "file", 149 }; 150 151 let mut record = Record::new(); 152 let display_name = if full_paths { 153 full_path.as_str().to_string() 154 } else { 155 rel_path.to_string() 156 }; 157 record.push("name", Value::string(display_name, span)); 158 record.push("type", Value::string(type_str, span)); 159 record.push("size", Value::filesize(metadata.len as i64, span)); 160 let mut add_timestamp = |field: &str, timestamp: Option<SystemTime>| { 161 if let Some(dur) = timestamp.and_then(|s| s.duration_since(UNIX_EPOCH).ok()) { 162 record.push( 163 field, 164 Value::date( 165 chrono::DateTime::from_timestamp_nanos(dur.as_nanos() as i64) 166 .fixed_offset(), 167 span, 168 ), 169 ); 170 } else { 171 record.push(field, Value::nothing(span)); 172 } 173 }; 174 add_timestamp("modified", metadata.modified); 175 if long { 176 add_timestamp("created", metadata.created); 177 add_timestamp("accessed", metadata.accessed); 178 } 179 Ok(Some(Value::record(record, span))) 180 }; 181 182 let entries = matches.into_iter().flat_map(move |rel_path| { 183 make_record(&rel_path) 184 .transpose() 185 .map(|res| res.unwrap_or_else(|err| Value::error(err, span))) 186 }); 187 188 let signals = engine_state.signals().clone(); 189 Ok(PipelineData::list_stream( 190 ListStream::new(entries, span, signals.clone()), 191 Some(nu_protocol::PipelineMetadata { 192 data_source: nu_protocol::DataSource::Ls, 193 content_type: None, 194 custom: Record::new(), 195 }), 196 )) 197 } 198}