feat: 实现 WinShell命令行工具的基本功能
- 新增 mkdir、ls、mv、rm、touch、cat 等常用命令的实现 - 支持命令行参数解析和错误处理 - 使用模块化设计,便于后续扩展 - 添加 Cargo.toml 文件,定义项目依赖 - 创建 .gitignore 文件,忽略不必要的文件和目录
This commit is contained in:
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
/target
|
||||
.idea
|
||||
*.iml
|
||||
Cargo.lock
|
11
Cargo.toml
Normal file
11
Cargo.toml
Normal file
@@ -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"] }
|
50
src/commands/directory_ops/mkdir.rs
Normal file
50
src/commands/directory_ops/mkdir.rs
Normal file
@@ -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),
|
||||
}
|
||||
}
|
||||
}
|
1
src/commands/directory_ops/mod.rs
Normal file
1
src/commands/directory_ops/mod.rs
Normal file
@@ -0,0 +1 @@
|
||||
pub mod mkdir;
|
203
src/commands/file_ops/ls.rs
Normal file
203
src/commands/file_ops/ls.rs
Normal file
@@ -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"))
|
||||
}
|
4
src/commands/file_ops/mod.rs
Normal file
4
src/commands/file_ops/mod.rs
Normal file
@@ -0,0 +1,4 @@
|
||||
pub mod ls;
|
||||
pub mod mv;
|
||||
pub mod rm;
|
||||
pub mod touch;
|
90
src/commands/file_ops/mv.rs
Normal file
90
src/commands/file_ops/mv.rs
Normal file
@@ -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")
|
||||
}
|
76
src/commands/file_ops/rm.rs
Normal file
76
src/commands/file_ops/rm.rs
Normal file
@@ -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")
|
||||
}
|
35
src/commands/file_ops/touch.rs
Normal file
35
src/commands/file_ops/touch.rs
Normal file
@@ -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(())
|
||||
}
|
4
src/commands/mod.rs
Normal file
4
src/commands/mod.rs
Normal file
@@ -0,0 +1,4 @@
|
||||
|
||||
pub mod file_ops;
|
||||
pub mod directory_ops;
|
||||
pub mod text_ops;
|
33
src/commands/text_ops/cat.rs
Normal file
33
src/commands/text_ops/cat.rs
Normal file
@@ -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); // 输出读取错误
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
1
src/commands/text_ops/mod.rs
Normal file
1
src/commands/text_ops/mod.rs
Normal file
@@ -0,0 +1 @@
|
||||
pub mod cat;
|
25
src/main.rs
Normal file
25
src/main.rs
Normal file
@@ -0,0 +1,25 @@
|
||||
mod commands;
|
||||
|
||||
use std::env;
|
||||
|
||||
fn main() {
|
||||
let args: Vec<String> = 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),
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user