commit cb832f614b132dc5d2706969a3e1c7a026633cc3 Author: 绝尘 <237809796@qq.com> Date: Mon Oct 21 17:46:32 2024 +0800 feat: 实现 WinShell命令行工具的基本功能 - 新增 mkdir、ls、mv、rm、touch、cat 等常用命令的实现 - 支持命令行参数解析和错误处理 - 使用模块化设计,便于后续扩展 - 添加 Cargo.toml 文件,定义项目依赖 - 创建 .gitignore 文件,忽略不必要的文件和目录 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..885b835 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +/target +.idea +*.iml +Cargo.lock \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..bb5829d --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "WinShell" +version = "0.1.0" +edition = "2021" + +# 依赖项 +[dependencies] +chrono = "0.4.38" +colored = "2.1.0" +filetime = "0.2.25" +tokio = { version = "1", features = ["full"] } diff --git a/src/commands/directory_ops/mkdir.rs b/src/commands/directory_ops/mkdir.rs new file mode 100644 index 0000000..7e2de68 --- /dev/null +++ b/src/commands/directory_ops/mkdir.rs @@ -0,0 +1,50 @@ +use std::fs; +use std::path::Path; + +pub fn execute(args: &[String]) { + if args.is_empty() { + eprintln!("用法: mkdir [-p] <目录名>..."); + return; + } + + let mut recursive = false; // 是否递归创建目录 + let mut targets = vec![]; // 要创建的目录列表 + + // 解析命令行参数 + for arg in args { + match arg.as_str() { + "-p" | "--parents" => recursive = true, // 启用递归创建 + _ => targets.push(arg), // 其他参数视为目录名 + } + } + + // 执行创建目录的操作 + for target in &targets { + let path = Path::new(target); + + // 检查目录是否存在 + if path.exists() { + if path.is_dir() { + if !recursive { + eprintln!("错误: 目录 '{}' 已存在", target); + } + } else { + eprintln!("错误: '{}' 已存在且不是目录", target); + } + continue; + } + + // 创建目录 + let result = if recursive { + fs::create_dir_all(path) // 递归创建目录 + } else { + fs::create_dir(path) // 创建单级目录 + }; + + // 检查创建结果 + match result { + Ok(_) => println!("创建目录: {}", target), + Err(e) => eprintln!("无法创建目录 '{}': {}", target, e), + } + } +} diff --git a/src/commands/directory_ops/mod.rs b/src/commands/directory_ops/mod.rs new file mode 100644 index 0000000..4f7d518 --- /dev/null +++ b/src/commands/directory_ops/mod.rs @@ -0,0 +1 @@ +pub mod mkdir; diff --git a/src/commands/file_ops/ls.rs b/src/commands/file_ops/ls.rs new file mode 100644 index 0000000..3b2792b --- /dev/null +++ b/src/commands/file_ops/ls.rs @@ -0,0 +1,203 @@ +// 引入 colored 库以支持颜色输出 +use chrono::NaiveDateTime; +use colored::*; +use std::fs::{self, DirEntry, Metadata}; +use std::time::{SystemTime, UNIX_EPOCH}; + +pub fn execute(args: &[String]) { + // 解析命令行参数 + let mut show_hidden = false; // 是否显示隐藏文件 + let mut long_format = false; // 是否使用长格式显示 + let mut target_path = "."; // 目标路径,默认为当前目录 + + // 处理参数 + for arg in args { + match arg.as_str() { + "-a" => show_hidden = true, // 显示隐藏文件 + "-l" => long_format = true, // 长格式显示 + // 支持 -al 参数 + "-al" | "-la" => { + show_hidden = true; + long_format = true; + } + path => target_path = path, // 指定的目录路径 + } + } + + // 读取指定目录的内容 + let entries = match fs::read_dir(target_path) { + Ok(entries) => entries, + Err(e) => { + eprintln!("无法读取目录 {}: {}", target_path, e); + return; + } + }; + + // 分别收集目录和文件 + let mut directories = vec![]; + let mut files = vec![]; + + for entry in entries { + let entry = match entry { + Ok(entry) => entry, + Err(e) => { + eprintln!("读取目录项时出错: {}", e); + continue; + } + }; + + let file_name = entry.file_name(); + let file_name_str = file_name.to_string_lossy(); + + // 过滤隐藏文件 + if !show_hidden && file_name_str.starts_with('.') { + continue; + } + + // 根据文件类型分别放入目录和文件的集合 + if entry.file_type().map(|ft| ft.is_dir()).unwrap_or(false) { + directories.push(entry); + } else { + files.push(entry); + } + } + + // 先输出目录,再输出文件 + for dir in &directories { + output_entry(dir, long_format); + } + for file in &files { + output_entry(file, long_format); + } +} +// 输出单个文件或目录的信息 +fn output_entry(entry: &fs::DirEntry, long_format: bool) { + let file_name = entry.file_name(); + let file_name_str = file_name.to_string_lossy(); + + if long_format { + // 获取文件的详细信息 + if let Ok(metadata) = entry.metadata() { + print_long_format(&metadata, &file_name_str); + } else { + eprintln!("无法获取 {} 的文件元数据", file_name_str); + } + } else { + // 简单输出文件名,目录高亮显示 + if entry.file_type().map(|ft| ft.is_dir()).unwrap_or(false) { + println!("{}", file_name_str.blue().bold()); // 目录名使用蓝色加粗 + } else { + // 使用统一的颜色输出 + print_file_color(entry, &file_name_str); + } + } +} + +// 输出文件的详细信息 +fn print_long_format(metadata: &Metadata, file_name: &str) { + // 获取文件类型(目录或普通文件) + let file_type = if metadata.is_dir() { "d" } else { "-" }; + let is_dir = metadata.is_dir(); + + // 获取文件大小 + let file_size = metadata.len(); // 这里是文件大小,使用 `len` 方法 + let modified_time = match metadata.modified() { + Ok(time) => format_system_time(time), + Err(_) => String::from("未知时间"), + }; + + // 判断文件名是否是隐藏文件 + let is_hidden = file_name.starts_with('.'); + // 判断文件是否是可执行文件 + let is_executable = metadata.permissions().readonly() == false && + metadata.file_type().is_file() && + file_name.ends_with(".exe"); + // 是否压缩文件 zip, tar, gz, bz2, rar, 7z, tgz, txz + let is_compressed = file_name.ends_with(".zip") || + file_name.ends_with(".tar") || + file_name.ends_with(".gz") || + file_name.ends_with(".bz2") || + file_name.ends_with(".rar") || + file_name.ends_with(".7z") || + file_name.ends_with(".tgz") || + file_name.ends_with(".txz"); + + // 美化输出格式:文件类型、大小、时间、文件名,不同类型使用不同颜色 + let formatted_output = format!( + "{} {:>10} {} {}", + file_type, + file_size, + modified_time, + file_name_color(file_name, is_dir, is_hidden, is_executable, is_compressed) + ); + + println!("{}", formatted_output); +} + +// 根据文件类型返回文件名的颜色 +fn file_name_color(file_name: &str, + is_dir: bool, + is_hidden: bool, + is_executable: bool, + is_compressed: bool) -> String { + if is_dir { + file_name.blue().bold().to_string() // 目录用蓝色加粗显示 + } else if is_hidden { + file_name.yellow().to_string() // 隐藏文件用黄色显示 + } else if is_executable { + file_name.green().to_string() // 可执行文件用绿色显示 + } else if is_compressed { + file_name.cyan().to_string() // 压缩文件用青色显示 + } else { + file_name.to_string() // 其他文件默认显示 + } +} + +// 输出文件颜色,简短形式 +fn print_file_color(entry: &DirEntry, file_name: &str) { + if let Ok(file_type) = entry.file_type() { // 处理 Result + if file_type.is_dir() { + println!("{}", file_name.blue().bold()); // 目录用蓝色加粗显示 + } else { + let is_dir = file_type.is_dir(); + let is_hidden = file_name.starts_with('.'); + let is_executable = entry.metadata().map_or(false, |m| + m.permissions().readonly() == false && + m.file_type().is_file() && + file_name.ends_with(".exe"), + ); + let is_compressed = file_name.ends_with(".zip") || + file_name.ends_with(".tar") || + file_name.ends_with(".gz") || + file_name.ends_with(".bz2") || + file_name.ends_with(".rar") || + file_name.ends_with(".7z") || + file_name.ends_with(".tgz") || + file_name.ends_with(".txz"); + + println!("{}", file_name_color(file_name, is_dir, is_hidden, is_executable, is_compressed)); + } + } else { + eprintln!("无法获取 {} 的文件类型", file_name); + } +} + +// 将文件名称做颜色格式化 +fn format_file_name(file_name: &str) -> String { + if file_name.starts_with('.') { + file_name.yellow().to_string() // 隐藏文件用黄色显示 + } else { + file_name.to_string() + } +} + +// 将系统时间格式化为可读的字符串 +fn format_system_time(system_time: SystemTime) -> String { + let datetime = system_time + .duration_since(UNIX_EPOCH) + .unwrap_or_default() + .as_secs(); + // 使用 NaiveDateTime::from_timestamp 将时间戳转换为日期时间 + let naive_datetime = NaiveDateTime::from_timestamp(datetime as i64, 0); + format!("{}", naive_datetime.format("%Y-%m-%d %H:%M:%S")) +} diff --git a/src/commands/file_ops/mod.rs b/src/commands/file_ops/mod.rs new file mode 100644 index 0000000..8c9089b --- /dev/null +++ b/src/commands/file_ops/mod.rs @@ -0,0 +1,4 @@ +pub mod ls; +pub mod mv; +pub mod rm; +pub mod touch; diff --git a/src/commands/file_ops/mv.rs b/src/commands/file_ops/mv.rs new file mode 100644 index 0000000..4ad7a9e --- /dev/null +++ b/src/commands/file_ops/mv.rs @@ -0,0 +1,90 @@ +use std::fs; +use std::io; +use std::path::{Path, PathBuf}; +use std::io::Write; + +pub fn execute(args: &[String]) { + if args.len() < 2 { + eprintln!("用法: mv [-i] [-f] <源文件或目录>... <目标>"); + return; + } + + let mut interactive = false; // 是否启用交互模式 + let mut force = false; // 是否启用强制模式 + let mut sources = vec![]; // 源文件或目录列表 + let mut target = String::new(); // 目标路径 + + // 解析命令行参数 + for arg in &args[..args.len() - 1] { + match arg.as_str() { + "-i" => interactive = true, // 交互模式 + "-f" => force = true, // 强制模式 + "-h" | "--help" => { + println!("用法: mv [-i] [-f] <源文件或目录>... <目标>"); + return; + }, + _ => sources.push(arg.clone()), // 视为源路径 + } + } + target = args[args.len() - 1].clone(); // 最后一个参数视为目标路径 + + // 如果只有一个源,则进行移动或重命名 + if sources.len() == 1 { + let source = Path::new(&sources[0]); + let destination = Path::new(&target); + move_single(source, destination, interactive, force); + } else { + // 多个源的情况,目标必须是目录 + let destination = Path::new(&target); + if !destination.is_dir() { + eprintln!("错误: 多个源文件时,目标必须是一个目录"); + return; + } + + for source in &sources { + let source_path = Path::new(source); + let mut dest_path = PathBuf::from(destination); + if let Some(file_name) = source_path.file_name() { + dest_path.push(file_name); + } + + move_single(source_path, &dest_path, interactive, force); + } + } +} + +// 移动或重命名单个文件或目录 +fn move_single(source: &Path, destination: &Path, interactive: bool, force: bool) { + if !source.exists() { + eprintln!("错误: 源文件或目录 '{}' 不存在", source.display()); + return; + } + + // 如果目标文件已存在,处理覆盖逻辑 + if destination.exists() { + if interactive && !confirm_overwrite(destination) { + println!("跳过: {}", destination.display()); + return; + } + if !force && !interactive { + eprintln!("错误: 目标 '{}' 已存在,使用 -f 强制覆盖或 -i 交互确认", destination.display()); + return; + } + } + + // 执行移动操作 + match fs::rename(source, destination) { + Ok(_) => println!("移动 '{}' 到 '{}'", source.display(), destination.display()), + Err(e) => eprintln!("无法移动 '{}' 到 '{}': {}", source.display(), destination.display(), e), + } +} + +// 确认是否覆盖目标文件 +fn confirm_overwrite(destination: &Path) -> bool { + print!("目标 '{}' 已存在,是否覆盖? (y/n): ", destination.display()); + io::stdout().flush().unwrap(); + + let mut input = String::new(); + io::stdin().read_line(&mut input).unwrap(); + matches!(input.trim(), "y" | "Y") +} diff --git a/src/commands/file_ops/rm.rs b/src/commands/file_ops/rm.rs new file mode 100644 index 0000000..50fe386 --- /dev/null +++ b/src/commands/file_ops/rm.rs @@ -0,0 +1,76 @@ +use std::fs; +use std::io; +use std::path::Path; +use std::io::Write; + +pub fn execute(args: &[String]) { + if args.is_empty() { + eprintln!("用法: rm [-r] [-i] [-f] <文件或目录名>"); + return; + } + + let mut recursive = false; // 是否递归删除 + let mut interactive = false; // 是否启用交互模式 + let mut force = false; // 是否强制删除 + let mut targets = vec![]; // 要删除的目标列表 + + // 解析命令行参数 + for arg in args { + match arg.as_str() { + "-r" | "--recursive" => recursive = true, + "-i" => interactive = true, + "-f" | "--force" => force = true, + _ => targets.push(arg), // 其他参数视为要删除的目标 + } + } + + // 执行删除操作 + for target in &targets { + let path = Path::new(target); + + // 如果路径不存在且不使用强制模式,打印错误 + if !path.exists() { + if !force { + eprintln!("错误: 文件或目录 '{}' 不存在", target); + } + continue; + } + + // 确认是否删除(如果启用了交互模式) + if interactive && !confirm_deletion(target) { + println!("跳过: {}", target); + continue; + } + + // 执行删除 + if path.is_dir() { + if recursive { + // 递归删除目录及其内容 + if let Err(e) = fs::remove_dir_all(path) { + eprintln!("无法删除目录 '{}': {}", target, e); + } else { + println!("删除目录: {}", target); + } + } else { + eprintln!("错误: '{}' 是一个目录,使用 -r 进行递归删除", target); + } + } else { + // 删除文件 + if let Err(e) = fs::remove_file(path) { + eprintln!("无法删除文件 '{}': {}", target, e); + } else { + println!("删除文件: {}", target); + } + } + } +} + +// 确认是否删除 +fn confirm_deletion(target: &str) -> bool { + print!("确认删除 '{}' 吗? (y/n): ", target); + io::stdout().flush().unwrap(); // 刷新输出缓冲区 + + let mut input = String::new(); + io::stdin().read_line(&mut input).unwrap(); + matches!(input.trim(), "y" | "Y") +} diff --git a/src/commands/file_ops/touch.rs b/src/commands/file_ops/touch.rs new file mode 100644 index 0000000..a632358 --- /dev/null +++ b/src/commands/file_ops/touch.rs @@ -0,0 +1,35 @@ +use filetime::FileTime; +use std::fs::File; +use std::path::Path; +use std::time::SystemTime; + +pub fn execute(args: &[String]) { + if args.is_empty() { + eprintln!("用法: touch <文件名>"); + return; + } + + for file_name in args { + let path = Path::new(file_name); + + if path.exists() { + // 如果文件已存在,更新其最后修改时间 + if let Err(e) = update_file_timestamp(path) { + eprintln!("无法更新 {} 的时间戳: {}", file_name, e); + } + } else { + // 如果文件不存在,则创建新文件 + match File::create(path) { + Ok(_) => println!("创建文件: {}", file_name), + Err(e) => eprintln!("无法创建文件 {}: {}", file_name, e), + } + } + } +} + +// 更新文件的最后修改时间 +fn update_file_timestamp(path: &Path) -> std::io::Result<()> { + let now = SystemTime::now(); + filetime::set_file_times(path, FileTime::from(now), FileTime::from(now))?; + Ok(()) +} diff --git a/src/commands/mod.rs b/src/commands/mod.rs new file mode 100644 index 0000000..16ef935 --- /dev/null +++ b/src/commands/mod.rs @@ -0,0 +1,4 @@ + +pub mod file_ops; +pub mod directory_ops; +pub mod text_ops; \ No newline at end of file diff --git a/src/commands/text_ops/cat.rs b/src/commands/text_ops/cat.rs new file mode 100644 index 0000000..533a36c --- /dev/null +++ b/src/commands/text_ops/cat.rs @@ -0,0 +1,33 @@ +use std::fs; +use std::path::Path; + +pub fn execute(args: &[String]) { + // 检查参数数量 + if args.is_empty() { + eprintln!("用法: cat <文件名>"); + return; + } + + // 遍历所有传入的文件名 + for arg in args { + let path = Path::new(arg); + + // 检查文件是否存在 + if !path.exists() { + eprintln!("错误: 文件 '{}' 不存在", arg); + continue; // 跳过不存在的文件 + } + + // 尝试读取文件内容 + match fs::read_to_string(path) { + Ok(contents) => { + // 打印文件内容 + println!("内容来自文件 '{}':", arg); + println!("{}", contents); + } + Err(e) => { + eprintln!("读取文件 '{}' 时出错: {}", arg, e); // 输出读取错误 + } + } + } +} diff --git a/src/commands/text_ops/mod.rs b/src/commands/text_ops/mod.rs new file mode 100644 index 0000000..463c62e --- /dev/null +++ b/src/commands/text_ops/mod.rs @@ -0,0 +1 @@ +pub mod cat; diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..302b1bf --- /dev/null +++ b/src/main.rs @@ -0,0 +1,25 @@ +mod commands; + +use std::env; + +fn main() { + let args: Vec = env::args().collect(); + + if args.len() < 2 { + eprintln!("使用方法: {} <命令> [参数...]", args[0]); + return; + } + + let command = &args[1]; + let command_args = &args[2..]; + + match command.as_str() { + "ls" => commands::file_ops::ls::execute(command_args), + "mv" => commands::file_ops::mv::execute(command_args), + "rm" => commands::file_ops::rm::execute(command_args), + "touch" => commands::file_ops::touch::execute(command_args), + "mkdir" => commands::directory_ops::mkdir::execute(command_args), + "cat" => commands::text_ops::cat::execute(command_args), + _ => eprintln!("未知命令: {}", command), + } +}