feat: 实现 WinShell命令行工具的基本功能

- 新增 mkdir、ls、mv、rm、touch、cat 等常用命令的实现
- 支持命令行参数解析和错误处理
- 使用模块化设计,便于后续扩展
- 添加 Cargo.toml 文件,定义项目依赖
- 创建 .gitignore 文件,忽略不必要的文件和目录
This commit is contained in:
2024-10-21 17:46:32 +08:00
commit cb832f614b
13 changed files with 537 additions and 0 deletions

4
.gitignore vendored Normal file
View File

@@ -0,0 +1,4 @@
/target
.idea
*.iml
Cargo.lock

11
Cargo.toml Normal file
View 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"] }

View 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),
}
}
}

View File

@@ -0,0 +1 @@
pub mod mkdir;

203
src/commands/file_ops/ls.rs Normal file
View 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"))
}

View File

@@ -0,0 +1,4 @@
pub mod ls;
pub mod mv;
pub mod rm;
pub mod touch;

View 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")
}

View 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")
}

View 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
View File

@@ -0,0 +1,4 @@
pub mod file_ops;
pub mod directory_ops;
pub mod text_ops;

View 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); // 输出读取错误
}
}
}
}

View File

@@ -0,0 +1 @@
pub mod cat;

25
src/main.rs Normal file
View 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),
}
}