Initial commit: Add cutPic, dexcel, and zip tools
This commit is contained in:
+27
@@ -0,0 +1,27 @@
|
||||
# Rust
|
||||
target/
|
||||
**/target/
|
||||
debug/
|
||||
release/
|
||||
*.pdb
|
||||
|
||||
# Cargo
|
||||
Cargo.lock
|
||||
|
||||
# IDE
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Skill files (optional, uncomment if you don't want to track them)
|
||||
# *-skill.md
|
||||
|
||||
# Test files
|
||||
test.xlsx
|
||||
test.png
|
||||
@@ -0,0 +1,305 @@
|
||||
# Tools 工具箱 🧰
|
||||
|
||||
这是一个实用的命令行工具集合,包含三个高效的 Rust 开发的工具,用于日常文件处理和数据处理任务。
|
||||
|
||||
## 📦 工具列表
|
||||
|
||||
### 1. [cutpic](./cutPic/README.md) - 图片裁切工具 🖼️
|
||||
|
||||
**功能**: 将图片按指定的行列数均匀分割成多个小图片
|
||||
|
||||
**适用场景**:
|
||||
- 制作拼图素材
|
||||
- 提取 Sprite Sheet 中的动画帧
|
||||
- 分割图标集
|
||||
- 创建 Instagram 九宫格
|
||||
- 处理大幅面图片
|
||||
|
||||
**快速开始**:
|
||||
```bash
|
||||
# 直接使用全局命令
|
||||
cutpic -i image.png -w 3 -h 2
|
||||
```
|
||||
|
||||
[查看详细文档 →](./cutPic/README.md)
|
||||
|
||||
---
|
||||
|
||||
### 2. [zip](./zip/README.md) - ZIP 打包工具 📦
|
||||
|
||||
**功能**: 将文件或文件夹快速打包成 zip 压缩文件
|
||||
|
||||
**适用场景**:
|
||||
- 项目备份
|
||||
- 文件分享
|
||||
- 代码归档
|
||||
- 日志收集
|
||||
- 资源打包
|
||||
|
||||
**特色功能**:
|
||||
- ✅ 支持单个文件、多个文件或整个文件夹
|
||||
- ✅ 智能命名(自动生成有意义的文件名)
|
||||
- ✅ 剪贴板集成(Windows 上可复制文件到剪贴板)
|
||||
- ✅ 详细的打包统计信息
|
||||
|
||||
**快速开始**:
|
||||
```bash
|
||||
# 直接使用全局命令
|
||||
zip myfolder -n backup.zip -v
|
||||
```
|
||||
|
||||
[查看详细文档 →](./zip/README.md)
|
||||
|
||||
---
|
||||
|
||||
### 3. [dexcel](./dexcel/README.md) - Excel 读写工具 📊
|
||||
|
||||
**功能**: 命令行 Excel 文件读取和写入工具
|
||||
|
||||
**适用场景**:
|
||||
- 数据分析
|
||||
- 数据转换
|
||||
- 报告生成
|
||||
- 数据验证
|
||||
- 批量数据录入
|
||||
|
||||
**特色功能**:
|
||||
- ✅ 灵活的范围读取(行、列选择)
|
||||
- ✅ 负数列索引(从右往左倒数)
|
||||
- ✅ 智能数据保留(写入时不覆盖现有数据)
|
||||
- ✅ TSV 格式输出(便于管道处理)
|
||||
|
||||
**快速开始**:
|
||||
```bash
|
||||
# 直接使用全局命令
|
||||
|
||||
# 读取 Excel
|
||||
dexcel read -f data.xlsx
|
||||
|
||||
# 写入 Excel
|
||||
dexcel write -f data.xlsx -x 1 -y 1 -v "Hello"
|
||||
```
|
||||
|
||||
[查看详细文档 →](./dexcel/README.md)
|
||||
|
||||
---
|
||||
|
||||
## 🚀 安装方式
|
||||
|
||||
### 方式一:Cargo Install(推荐)
|
||||
|
||||
在每个工具目录下执行:
|
||||
|
||||
```bash
|
||||
# 安装 cutpic
|
||||
cd cutPic
|
||||
cargo install --path .
|
||||
|
||||
# 安装 dexcel
|
||||
cd ../dexcel
|
||||
cargo install --path .
|
||||
|
||||
# 安装 zip
|
||||
cd ../zip
|
||||
cargo install --path .
|
||||
```
|
||||
|
||||
安装后,可以直接在命令行中使用 `cutpic`、`dexcel`、`zip` 命令。
|
||||
|
||||
### 方式二:本地编译
|
||||
|
||||
```bash
|
||||
# Windows PowerShell
|
||||
Get-ChildItem -Directory | Where-Object { $_.Name -ne 'target' } | ForEach-Object {
|
||||
Write-Host "Building $($_.Name)..."
|
||||
Push-Location $_.FullName
|
||||
cargo build --release
|
||||
Pop-Location
|
||||
}
|
||||
|
||||
# Linux/macOS
|
||||
for dir in */; do
|
||||
if [ -f "$dir/Cargo.toml" ]; then
|
||||
echo "Building $dir..."
|
||||
cd "$dir" && cargo build --release && cd ..
|
||||
fi
|
||||
done
|
||||
```
|
||||
|
||||
编译后的可执行文件位于各工具的 `target/release/` 目录下。
|
||||
|
||||
---
|
||||
|
||||
## 💡 综合使用示例
|
||||
|
||||
### 示例 1: 项目管理自动化
|
||||
|
||||
```bash
|
||||
# 1. 打包项目源代码
|
||||
zip src/ README.md LICENSE -n project-source.zip
|
||||
|
||||
# 2. 创建版本信息 Excel
|
||||
dexcel write -f version-info.xlsx -x 1 -y 1 -v "版本"
|
||||
dexcel write -f version-info.xlsx -x 1 -y 2 -v "日期"
|
||||
dexcel write -f version-info.xlsx -x 1 -y 3 -v "说明"
|
||||
dexcel write -f version-info.xlsx -x 2 -y 1 -v "1.0.0"
|
||||
dexcel write -f version-info.xlsx -x 2 -y 2 -v "2024-01-01"
|
||||
dexcel write -f version-info.xlsx -x 2 -y 3 -v "初始版本"
|
||||
|
||||
# 3. 打包发布文件
|
||||
zip project-source.zip version-info.xlsx -n release-v1.0.0.zip -v
|
||||
```
|
||||
|
||||
### 示例 2: 图片资源处理工作流
|
||||
|
||||
```bash
|
||||
# 1. 分割游戏精灵图
|
||||
cutpic -i spritesheet.png -w 8 -h 4 --out ./frames
|
||||
|
||||
# 2. 创建资源清单 Excel
|
||||
dexcel write -f assets.xlsx -x 1 -y 1 -v "文件名"
|
||||
dexcel write -f assets.xlsx -x 1 -y 2 -v "类型"
|
||||
dexcel write -f assets.xlsx -x 1 -y 3 -v "尺寸"
|
||||
|
||||
# 添加资源信息
|
||||
dexcel write -f assets.xlsx -x 2 -y 1 -v "frame_1.png"
|
||||
dexcel write -f assets.xlsx -x 2 -y 2 -v "动画帧"
|
||||
dexcel write -f assets.xlsx -x 2 -y 3 -v "64x64"
|
||||
|
||||
# 3. 打包所有资源
|
||||
zip frames/ assets.xlsx -n game-assets.zip
|
||||
```
|
||||
|
||||
### 示例 3: 数据备份和处理
|
||||
|
||||
```bash
|
||||
# 1. 备份重要文档
|
||||
zip documents/ reports/ -n docs-backup.zip
|
||||
|
||||
# 2. 创建备份清单
|
||||
dexcel write -f backup-log.xlsx -x 1 -y 1 -v "文件名"
|
||||
dexcel write -f backup-log.xlsx -x 1 -y 2 -v "大小"
|
||||
dexcel write -f backup-log.xlsx -x 1 -y 3 -v "日期"
|
||||
dexcel write -f backup-log.xlsx -x 2 -y 1 -v "docs-backup.zip"
|
||||
dexcel write -f backup-log.xlsx -x 2 -y 2 -v "15MB"
|
||||
dexcel write -f backup-log.xlsx -x 2 -y 3 -v "2024-01-01"
|
||||
|
||||
# 3. 打包备份和日志
|
||||
zip docs-backup.zip backup-log.xlsx -n complete-backup.zip -v
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎯 工具对比
|
||||
|
||||
| 特性 | cutpic | zip | dexcel |
|
||||
|------|--------|-----|--------|
|
||||
| **主要用途** | 图片分割 | 文件压缩 | Excel 读写 |
|
||||
| **输入** | 单张图片 | 多文件/文件夹 | Excel 文件 |
|
||||
| **输出** | 多张图片 | ZIP 压缩包 | 控制台/Excel |
|
||||
| **平台支持** | 全平台 | 全平台 | 全平台 |
|
||||
| **依赖数量** | 少 | 中 | 中 |
|
||||
| **学习曲线** | 简单 | 简单 | 中等 |
|
||||
| **适用人群** | 设计师、开发者 | 所有人 | 数据分析师、开发者 |
|
||||
|
||||
---
|
||||
|
||||
## 🔧 技术栈
|
||||
|
||||
所有工具都使用 **Rust** 开发,具有以下优势:
|
||||
|
||||
- ⚡ **高性能**: 编译型语言,运行速度快
|
||||
- 🔒 **内存安全**: 无垃圾回收,无内存泄漏
|
||||
- 📦 **单文件分发**: 无需运行时环境
|
||||
- 🌍 **跨平台**: Windows、macOS、Linux 原生支持
|
||||
- 🛠️ **现代工具链**: Cargo 包管理,优秀的开发体验
|
||||
|
||||
---
|
||||
|
||||
## 📊 项目结构
|
||||
|
||||
```
|
||||
tools/
|
||||
├── cutPic/ # 图片裁切工具
|
||||
│ ├── src/
|
||||
│ │ └── main.rs
|
||||
│ ├── Cargo.toml
|
||||
│ ├── README.md
|
||||
│ └── target/
|
||||
│ └── release/
|
||||
│ └── cutpic.exe
|
||||
│
|
||||
├── zip/ # ZIP 打包工具
|
||||
│ ├── src/
|
||||
│ │ └── main.rs
|
||||
│ ├── Cargo.toml
|
||||
│ ├── README.md
|
||||
│ └── target/
|
||||
│ └── release/
|
||||
│ └── zip.exe
|
||||
│
|
||||
├── dexcel/ # Excel 读写工具
|
||||
│ ├── src/
|
||||
│ │ └── main.rs
|
||||
│ ├── Cargo.toml
|
||||
│ ├── README.md
|
||||
│ ├── example.bat
|
||||
│ └── target/
|
||||
│ └── release/
|
||||
│ └── dexcel.exe
|
||||
│
|
||||
└── README.md # 本文件
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🤝 贡献指南
|
||||
|
||||
欢迎贡献代码、报告问题或提出建议!
|
||||
|
||||
### 提交 Issue
|
||||
- 描述清楚问题和复现步骤
|
||||
- 提供环境信息(操作系统、Rust 版本等)
|
||||
- 如果可能,提供截图或日志
|
||||
|
||||
### 提交 Pull Request
|
||||
1. Fork 本项目
|
||||
2. 创建特性分支 (`git checkout -b feature/AmazingFeature`)
|
||||
3. 提交更改 (`git commit -m 'Add some AmazingFeature'`)
|
||||
4. 推送到分支 (`git push origin feature/AmazingFeature`)
|
||||
5. 开启 Pull Request
|
||||
|
||||
### 开发规范
|
||||
- 遵循 Rust 官方代码风格
|
||||
- 添加必要的注释和文档
|
||||
- 确保代码通过 `cargo clippy` 检查
|
||||
- 更新相关的 README 文档
|
||||
|
||||
---
|
||||
|
||||
## 📄 许可证
|
||||
|
||||
本项目采用 MIT 许可证 - 详见各子项目的 LICENSE 文件。
|
||||
|
||||
---
|
||||
|
||||
## 🙏 致谢
|
||||
|
||||
感谢以下开源项目和社区:
|
||||
- [Rust](https://www.rust-lang.org/) - 编程语言
|
||||
- [Cargo](https://doc.rust-lang.org/cargo/) - 包管理器
|
||||
- 所有依赖库的作者和维护者
|
||||
- 所有贡献者和用户
|
||||
|
||||
---
|
||||
|
||||
## 📞 联系方式
|
||||
|
||||
- 项目地址: `c:\Users\macro\Documents\workspace\self\tools`
|
||||
- 问题反馈: 提交 GitHub Issue
|
||||
|
||||
---
|
||||
|
||||
**Made with ❤️ using Rust**
|
||||
|
||||
*最后更新: 2024年*
|
||||
@@ -0,0 +1 @@
|
||||
/target
|
||||
@@ -0,0 +1,8 @@
|
||||
[package]
|
||||
name = "cutpic"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
image = "0.25"
|
||||
clap = { version = "4.5", features = ["derive"] }
|
||||
@@ -0,0 +1,283 @@
|
||||
# cutpic - 图片裁切工具 🖼️
|
||||
|
||||
一个简单易用的命令行图片裁切工具,可以将图片按指定的行列数均匀分割成多个小图片。
|
||||
|
||||
## ✨ 功能特性
|
||||
|
||||
- 📐 **灵活分割**: 支持自定义水平和垂直分割数
|
||||
- 🎯 **均匀裁切**: 自动计算每块尺寸,均匀分割图片
|
||||
- 📁 **智能输出**: 默认在输入文件同级目录创建 dist 文件夹
|
||||
- 🖼️ **多格式支持**: 支持 PNG, JPG, JPEG, BMP, GIF, WebP 等常见图片格式
|
||||
- 🔢 **智能命名**: 自动生成带序号的文件名,方便识别和管理
|
||||
- ⚡ **高性能**: 基于 Rust 开发,处理速度快
|
||||
|
||||
## 📦 安装
|
||||
|
||||
### 从源码编译
|
||||
|
||||
```bash
|
||||
cd cutPic
|
||||
cargo build --release
|
||||
```
|
||||
|
||||
编译后的可执行文件位于 `target/release/cutpic.exe`
|
||||
|
||||
### 添加到系统路径(可选)
|
||||
|
||||
将 `target/release/cutpic.exe` 复制到系统 PATH 目录,或将其所在目录添加到 PATH 环境变量中。
|
||||
|
||||
## 🚀 快速开始
|
||||
|
||||
### 基本用法
|
||||
|
||||
```bash
|
||||
# 将图片分割成 3x2 的网格(共6块)
|
||||
cutpic -i image.png -w 3 -h 2
|
||||
|
||||
# 将图片分割成 4x4 的网格(共16块)
|
||||
cutpic -i photo.jpg -w 4 -h 4
|
||||
```
|
||||
|
||||
### 指定输出目录
|
||||
|
||||
```bash
|
||||
# 指定输出到 custom_output 目录
|
||||
cutpic -i image.png -w 2 -h 2 --out ./custom_output
|
||||
```
|
||||
|
||||
## 📖 命令参数
|
||||
|
||||
```bash
|
||||
cutpic [OPTIONS] -i <INPUT>
|
||||
|
||||
参数:
|
||||
-i, --input <FILE> 📁 输入图片路径(必需)
|
||||
-w, --width <NUM> 📐 水平方向分割数(默认: 1)
|
||||
-h, --height <NUM> 📏 垂直方向分割数(默认: 1)
|
||||
--out <DIR> 📂 输出目录(默认: 输入文件同级的 dist 文件夹)
|
||||
-h, --help 显示帮助信息
|
||||
-V, --version 显示版本信息
|
||||
```
|
||||
|
||||
## 💡 使用示例
|
||||
|
||||
### 示例 1: 制作拼图素材
|
||||
|
||||
将一张大图分割成 5x5 的拼图块:
|
||||
|
||||
```bash
|
||||
cutpic -i puzzle.png -w 5 -h 5
|
||||
```
|
||||
|
||||
**输出:**
|
||||
- 生成 25 张图片
|
||||
- 文件命名: `puzzle_1_25.png`, `puzzle_2_25.png`, ..., `puzzle_25_25.png`
|
||||
- 输出位置: `./dist/` 目录
|
||||
|
||||
### 示例 2: 提取图标集中的单个图标
|
||||
|
||||
从 4x4 排列的图标集中提取每个图标:
|
||||
|
||||
```bash
|
||||
cutpic -i icons.png -w 4 -h 4 --out ./individual_icons
|
||||
```
|
||||
|
||||
**输出:**
|
||||
- 生成 16 张单独的图标
|
||||
- 输出到 `./individual_icons/` 目录
|
||||
|
||||
### 示例 3: 分割游戏精灵图(Sprite Sheet)
|
||||
|
||||
横向分割角色动画帧(8 帧动画):
|
||||
|
||||
```bash
|
||||
cutpic -i character.png -w 8 -h 1 --out ./animation_frames
|
||||
```
|
||||
|
||||
**输出:**
|
||||
- 生成 8 张动画帧图片
|
||||
- 适合用于游戏开发中的帧动画
|
||||
|
||||
### 示例 4: 只进行水平分割
|
||||
|
||||
将横幅广告图分割成 4 部分:
|
||||
|
||||
```bash
|
||||
cutpic -i banner.png -w 4 -h 1
|
||||
```
|
||||
|
||||
### 示例 5: 只进行垂直分割
|
||||
|
||||
将长图分割成 3 段:
|
||||
|
||||
```bash
|
||||
cutpic -i long-image.jpg -w 1 -h 3
|
||||
```
|
||||
|
||||
## 📊 输出说明
|
||||
|
||||
### 文件命名规则
|
||||
|
||||
生成的文件遵循以下命名格式:
|
||||
|
||||
```
|
||||
{原文件名}_{序号}_{总数}.{扩展名}
|
||||
```
|
||||
|
||||
**示例:**
|
||||
- 原文件: `image.png`
|
||||
- 分割: 3x2 (共6块)
|
||||
- 输出文件:
|
||||
- `image_1_6.png` - 第1块
|
||||
- `image_2_6.png` - 第2块
|
||||
- ...
|
||||
- `image_6_6.png` - 第6块
|
||||
|
||||
### 输出目录结构
|
||||
|
||||
```
|
||||
项目目录/
|
||||
├── image.png # 原始图片
|
||||
└── dist/ # 默认输出目录
|
||||
├── image_1_6.png
|
||||
├── image_2_6.png
|
||||
├── image_3_6.png
|
||||
├── image_4_6.png
|
||||
├── image_5_6.png
|
||||
└── image_6_6.png
|
||||
```
|
||||
|
||||
如果指定了 `--out` 参数:
|
||||
|
||||
```
|
||||
项目目录/
|
||||
├── image.png # 原始图片
|
||||
└── my_output/ # 自定义输出目录
|
||||
├── image_1_6.png
|
||||
├── image_2_6.png
|
||||
└── ...
|
||||
```
|
||||
|
||||
## ⚙️ 工作原理
|
||||
|
||||
1. **加载图片**: 读取输入图片并获取尺寸信息
|
||||
2. **计算尺寸**:
|
||||
- 每块宽度 = 总宽度 ÷ 水平分割数
|
||||
- 每块高度 = 总高度 ÷ 垂直分割数
|
||||
3. **逐块裁切**: 按行列顺序遍历,裁切每一块
|
||||
4. **处理边界**: 最后一行/列可能包含剩余像素,自动调整尺寸
|
||||
5. **保存文件**: 按命名规则保存到输出目录
|
||||
|
||||
### 边界处理示例
|
||||
|
||||
假设图片尺寸为 100x100 像素,分割为 3x3:
|
||||
|
||||
- 理论每块: 33.33 x 33.33 像素
|
||||
- 实际处理:
|
||||
- 第1、2块: 33 x 33 像素
|
||||
- 第3块: 34 x 34 像素(包含剩余像素)
|
||||
|
||||
这样确保所有像素都被保留,不会丢失任何内容。
|
||||
|
||||
## 🎯 应用场景
|
||||
|
||||
### 1. 游戏开发
|
||||
- 分割 Sprite Sheet 为单独的动画帧
|
||||
- 提取瓦片地图(Tilemap)的单个瓦片
|
||||
- 处理角色、道具等资源图片
|
||||
|
||||
### 2. 网页设计
|
||||
- 将大图分割成小块用于懒加载
|
||||
- 制作图片拼图效果
|
||||
- 优化大图的加载性能
|
||||
|
||||
### 3. 社交媒体
|
||||
- 制作 Instagram 九宫格图片
|
||||
- 分割长图为适合移动端查看的多张图片
|
||||
- 创建有趣的图片分割效果
|
||||
|
||||
### 4. 数据处理
|
||||
- 批量处理扫描文档
|
||||
- 分割大幅面图纸
|
||||
- 处理卫星图像或地图切片
|
||||
|
||||
### 5. 教育用途
|
||||
- 制作教学用拼图
|
||||
- 分解复杂图表
|
||||
- 创建互动学习材料
|
||||
|
||||
## ⚠️ 注意事项
|
||||
|
||||
1. **图片格式**: 输出格式与输入格式相同
|
||||
2. **最小分割**: 分割数为 1x1 时,会生成原图的副本
|
||||
3. **内存使用**: 处理超大图片时会占用较多内存
|
||||
4. **文件数量**: 分割数越多,生成的文件越多,注意磁盘空间
|
||||
5. **索引顺序**: 文件按先行后列的顺序编号(从左到右,从上到下)
|
||||
|
||||
## 🔧 技术细节
|
||||
|
||||
### 依赖库
|
||||
- **clap**: 命令行参数解析
|
||||
- **image**: 图片处理和裁切
|
||||
|
||||
### 性能特点
|
||||
- 使用 Rust 编写,性能优异
|
||||
- 支持多线程(取决于 image 库的实现)
|
||||
- 内存效率高,逐块处理
|
||||
|
||||
### 平台支持
|
||||
- ✅ Windows
|
||||
- ✅ macOS
|
||||
- ✅ Linux
|
||||
|
||||
## 📝 完整示例工作流
|
||||
|
||||
### 工作流 1: 制作 Instagram 九宫格
|
||||
|
||||
```bash
|
||||
# 1. 准备一张正方形图片(建议 1080x1080 或更高)
|
||||
# 2. 分割成 3x3 的九宫格
|
||||
cutpic -i instagram-post.png -w 3 -h 3 --out ./instagram-grid
|
||||
|
||||
# 3. 上传生成的 9 张图片到 Instagram(按逆序上传以获得正确显示顺序)
|
||||
```
|
||||
|
||||
### 工作流 2: 提取游戏资源
|
||||
|
||||
```bash
|
||||
# 1. 从游戏中提取 sprite sheet
|
||||
# 2. 分析 sprite sheet 的布局(例如 8x4 排列)
|
||||
cutpic -i spritesheet.png -w 8 -h 4 --out ./game-assets
|
||||
|
||||
# 3. 重命名生成的文件为有意义的名称
|
||||
# 4. 在游戏引擎中使用这些单独的资源
|
||||
```
|
||||
|
||||
### 工作流 3: 创建拼图游戏
|
||||
|
||||
```bash
|
||||
# 1. 选择一张有趣的图片
|
||||
# 2. 根据难度选择分割数(简单: 3x3, 中等: 5x5, 困难: 8x8)
|
||||
cutpic -i puzzle-image.jpg -w 5 -h 5 --out ./puzzle-pieces
|
||||
|
||||
# 3. 打乱文件顺序
|
||||
# 4. 开发拼图游戏逻辑
|
||||
```
|
||||
|
||||
## 🤝 贡献
|
||||
|
||||
欢迎提交 Issue 和 Pull Request!
|
||||
|
||||
## 📄 许可证
|
||||
|
||||
本项目采用 MIT 许可证。
|
||||
|
||||
## 🙏 致谢
|
||||
|
||||
感谢以下开源项目:
|
||||
- [clap](https://github.com/clap-rs/clap) - 优秀的命令行参数解析库
|
||||
- [image](https://github.com/image-rs/image) - 强大的图片处理库
|
||||
|
||||
---
|
||||
|
||||
**Made with ❤️ using Rust**
|
||||
@@ -0,0 +1,108 @@
|
||||
use clap::Parser;
|
||||
use image::GenericImageView;
|
||||
use std::path::PathBuf;
|
||||
|
||||
/// 图片裁切工具 - 将图片按行列分割成多份
|
||||
#[derive(Parser, Debug)]
|
||||
#[command(name = "cutpic")]
|
||||
#[command(author, version, about, long_about = None)]
|
||||
#[command(
|
||||
about = "🖼️ 图片裁切工具 - 将图片按指定的行列数分割成多个小图片",
|
||||
long_about = "🖼️ 图片裁切工具\n\n将一张图片按照指定的水平和垂直分割数,均匀裁切成多个小图片。\n适用于制作拼图、图标集、 sprite sheet 等场景。\n\n示例:\n cutpic -i image.png -w 3 -h 2\n cutpic -i photo.jpg --width 4 --height 4 --out ./output"
|
||||
)]
|
||||
struct Args {
|
||||
/// 📁 输入图片路径(支持 PNG, JPG, JPEG, BMP, GIF, WebP 等格式)
|
||||
#[arg(short = 'i', long = "input", value_name = "FILE")]
|
||||
input: PathBuf,
|
||||
|
||||
/// 📐 水平方向分割数(将宽度分成几份,默认 1)
|
||||
#[arg(short = 'w', long = "width", default_value_t = 1, value_name = "NUM")]
|
||||
width: u32,
|
||||
|
||||
/// 📏 垂直方向分割数(将高度分成几份,默认 1)
|
||||
#[arg(short = 'h', long = "height", default_value_t = 1, value_name = "NUM")]
|
||||
height: u32,
|
||||
|
||||
/// 📂 输出目录(默认为输入文件同级目录下的 dist 文件夹)
|
||||
#[arg(long = "out", value_name = "DIR", default_value = ".")]
|
||||
output: PathBuf,
|
||||
}
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let args = Args::parse();
|
||||
|
||||
// 加载图片
|
||||
println!("\n🔄 正在加载图片: {:?}", args.input);
|
||||
let img = image::open(&args.input)?;
|
||||
let (width, height) = img.dimensions();
|
||||
|
||||
println!("📊 图片尺寸: {}x{} 像素", width, height);
|
||||
println!("✂️ 分割方式: {}x{} (共 {} 份)", args.width, args.height, args.width * args.height);
|
||||
|
||||
// 计算每个子图的尺寸
|
||||
let tile_width = width / args.width;
|
||||
let tile_height = height / args.height;
|
||||
|
||||
println!("📏 每块尺寸: {}x{} 像素", tile_width, tile_height);
|
||||
|
||||
// 确定输出目录:如果用户指定了输出目录则使用,否则在输入文件旁边创建 dist 目录
|
||||
let output_dir = if args.output != PathBuf::from(".") {
|
||||
args.output.clone()
|
||||
} else {
|
||||
let mut default_output = args.input.parent().unwrap_or(std::path::Path::new(".")).to_path_buf();
|
||||
default_output.push("dist");
|
||||
default_output
|
||||
};
|
||||
|
||||
println!("📁 输出目录: {:?}\n", output_dir);
|
||||
|
||||
// 创建输出目录(如果不存在)
|
||||
std::fs::create_dir_all(&output_dir)?;
|
||||
|
||||
// 获取文件名(不含扩展名)和扩展名
|
||||
let file_stem = args
|
||||
.input
|
||||
.file_stem()
|
||||
.and_then(|s| s.to_str())
|
||||
.unwrap_or("image");
|
||||
let extension = args
|
||||
.input
|
||||
.extension()
|
||||
.and_then(|s| s.to_str())
|
||||
.unwrap_or("png");
|
||||
|
||||
// 裁切并保存每一块
|
||||
for row in 0..args.height {
|
||||
for col in 0..args.width {
|
||||
let x = col * tile_width;
|
||||
let y = row * tile_height;
|
||||
|
||||
// 最后一块可能需要调整尺寸以适应剩余部分
|
||||
let actual_width = if col == args.width - 1 {
|
||||
width - x
|
||||
} else {
|
||||
tile_width
|
||||
};
|
||||
let actual_height = if row == args.height - 1 {
|
||||
height - y
|
||||
} else {
|
||||
tile_height
|
||||
};
|
||||
|
||||
// 裁切图片
|
||||
let tile = img.crop_imm(x, y, actual_width, actual_height);
|
||||
|
||||
// 生成输出文件名
|
||||
let index = row * args.width + col + 1;
|
||||
let output_filename = format!("{}_{}_{}.{}", file_stem, index, args.width * args.height, extension);
|
||||
let output_path = output_dir.join(output_filename);
|
||||
|
||||
// 保存图片
|
||||
tile.save(&output_path)?;
|
||||
println!("已保存: {:?}", output_path);
|
||||
}
|
||||
}
|
||||
|
||||
println!("\n✅ 完成! 共生成 {} 张图片", args.width * args.height);
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
/target
|
||||
@@ -0,0 +1,11 @@
|
||||
[package]
|
||||
name = "dexcel"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
calamine = { version = "0.26", features = ["dates"] }
|
||||
clap = { version = "4.5", features = ["derive"] }
|
||||
rust_xlsxwriter = "0.80"
|
||||
tabled = "0.16"
|
||||
chrono = "0.4"
|
||||
@@ -0,0 +1,514 @@
|
||||
# dexcel - Excel 读写工具 📊
|
||||
|
||||
一个简单易用的命令行 Excel 处理工具,支持查询、统计、插入、更新和删除操作。
|
||||
|
||||
## ✨ 功能特性
|
||||
|
||||
### 核心操作
|
||||
- 🔍 **query**: 灵活查询 Excel 数据,支持行范围选择和自定义分隔符
|
||||
- 📊 **count**: 快速统计行数,适合脚本使用
|
||||
- ➕ **insert**: 插入新行,支持指定位置或追加到末尾
|
||||
- ✏️ **update**: 更新现有数据,支持部分列更新
|
||||
- ❌ **delete**: 删除行或单元格,带安全确认机制
|
||||
|
||||
### 通用特性
|
||||
- 📑 **多 Sheet 支持**: 所有操作都支持 `--sheet` 参数指定工作表
|
||||
- 🔢 **负数索引**: 支持负数行号(-1 表示最后一行)
|
||||
- 🆕 **自动创建**: insert 操作在文件不存在时自动创建
|
||||
- ⚠️ **安全确认**: delete 操作需要用户确认
|
||||
- 📤 **灵活输出**: query 支持自定义分隔符(默认 |)
|
||||
|
||||
## 📦 安装
|
||||
|
||||
### 从源码编译
|
||||
|
||||
```bash
|
||||
cd dexcel
|
||||
cargo build --release
|
||||
```
|
||||
|
||||
编译后的可执行文件位于 `target/release/dexcel.exe` (Windows)
|
||||
|
||||
### 全局安装
|
||||
|
||||
```bash
|
||||
cargo install --path . --force
|
||||
```
|
||||
|
||||
安装后可直接使用 `dexcel` 命令。
|
||||
|
||||
## 🚀 使用方法
|
||||
|
||||
**基本格式:**
|
||||
```bash
|
||||
dexcel <文件路径> <操作> [参数]
|
||||
```
|
||||
|
||||
**注意:文件路径是第一个位置参数,不需要 `--file` 标志!**
|
||||
|
||||
### 1. 查询数据 (query)
|
||||
|
||||
```bash
|
||||
# 查看整个文件(默认显示最后20行)
|
||||
dexcel data.xlsx query
|
||||
|
||||
# 查看前5行
|
||||
dexcel data.xlsx query 5
|
||||
|
||||
# 查看最后3行
|
||||
dexcel data.xlsx query -3
|
||||
|
||||
# 查看最后一行
|
||||
dexcel data.xlsx query --last
|
||||
|
||||
# 从第10行开始查看
|
||||
dexcel data.xlsx query --start 10
|
||||
|
||||
# 指定 Sheet 名称
|
||||
dexcel data.xlsx query --sheet "Sales"
|
||||
```
|
||||
|
||||
#### query 参数说明
|
||||
- `[COUNT]`: 获取行数(正数从前,负数从后),默认0显示最后20行
|
||||
- `--start <START>`: 开始行号(支持负数)
|
||||
- `--last`: 获取最后一行
|
||||
- `--sheet <SHEET>`: Sheet 名称(可选,默认第一个 Sheet)
|
||||
|
||||
### 2. 统计行数 (count)
|
||||
|
||||
```bash
|
||||
# 统计总行数
|
||||
dexcel data.xlsx count
|
||||
|
||||
# 统计指定 Sheet 的行数
|
||||
dexcel data.xlsx count --sheet "Sales"
|
||||
```
|
||||
|
||||
#### count 参数说明
|
||||
- `--sheet <SHEET>`: Sheet 名称(可选,默认第一个 Sheet)
|
||||
|
||||
**输出**: 只输出一个数字,表示总行数,适合脚本中使用。
|
||||
|
||||
### 3. 插入数据 (insert)
|
||||
|
||||
```bash
|
||||
# 创建新文件并插入表头
|
||||
dexcel new-file.xlsx insert "姓名|年龄|城市"
|
||||
|
||||
# 追加数据到末尾
|
||||
dexcel data.xlsx insert "张三|18|北京"
|
||||
|
||||
# 在指定位置插入(第2行)
|
||||
dexcel data.xlsx insert "李四|20|上海" --row 2
|
||||
|
||||
# 使用不同分隔符
|
||||
dexcel data.xlsx insert "王五,25,广州" --split ","
|
||||
|
||||
# 指定 Sheet
|
||||
dexcel data.xlsx insert "数据" --sheet "Sheet2"
|
||||
```
|
||||
|
||||
#### insert 参数说明
|
||||
- `<VALUE>`: 要插入的值(用 split 分隔列)**必需**
|
||||
- `--row <ROW>`: 插入位置(不指定则追加到末尾)
|
||||
- `--sheet <SHEET>`: Sheet 名称(可选)
|
||||
- `--split <SPLIT>`: 分隔符(默认 |)
|
||||
|
||||
### 4. 更新数据 (update)
|
||||
|
||||
```bash
|
||||
# 更新整行(从第1列开始)
|
||||
dexcel data.xlsx update "赵六|22|深圳" --row 2
|
||||
|
||||
# 更新部分列(从第2列开始)
|
||||
dexcel data.xlsx update "新年龄" --row 5 --cell 2
|
||||
|
||||
# 更新多列(从第3列开始)
|
||||
dexcel data.xlsx update "新城市|新备注" --row 4 --cell 3
|
||||
|
||||
# 指定 Sheet
|
||||
dexcel data.xlsx update "新值" --row 1 --sheet "Sheet2"
|
||||
```
|
||||
|
||||
#### update 参数说明
|
||||
- `<VALUE>`: 要更新的值(用 split 分隔列)**必需**
|
||||
- `--row <ROW>`: 行号(必需,支持负数)
|
||||
- `--cell <CELL>`: 起始列号(从1开始,默认1)
|
||||
- `--sheet <SHEET>`: Sheet 名称(可选)
|
||||
- `--split <SPLIT>`: 分隔符(默认 |)
|
||||
|
||||
### 5. 删除数据 (delete)
|
||||
|
||||
```bash
|
||||
# 删除整行(带确认)
|
||||
dexcel data.xlsx delete --row 5
|
||||
|
||||
# 删除最后一行
|
||||
dexcel data.xlsx delete --row -1
|
||||
|
||||
# 删除单元格(第3行第2列)
|
||||
dexcel data.xlsx delete --row 3 --cell 2
|
||||
|
||||
# 指定 Sheet
|
||||
dexcel data.xlsx delete --row 1 --sheet "Sheet2"
|
||||
```
|
||||
|
||||
#### delete 参数说明
|
||||
- `[COUNT]`: 删除数量(默认1)
|
||||
- `--row <ROW>`: 行号(必需,支持负数)
|
||||
- `--cell <CELL>`: 列号(可选,不指定则删除整行)
|
||||
- `--sheet <SHEET>`: Sheet 名称(可选)
|
||||
|
||||
**注意**: delete 操作需要用户确认(输入 y 继续)
|
||||
|
||||
## 💡 使用示例
|
||||
|
||||
### 示例 1: 创建学生信息表
|
||||
|
||||
```bash
|
||||
# 创建表头
|
||||
dexcel students.xlsx insert "姓名|年龄|班级"
|
||||
|
||||
# 添加学生数据
|
||||
dexcel students.xlsx insert "张三|18|一班"
|
||||
dexcel students.xlsx insert "李四|19|二班"
|
||||
dexcel students.xlsx insert "王五|20|三班"
|
||||
|
||||
# 查看所有数据
|
||||
dexcel students.xlsx query
|
||||
|
||||
# 统计总行数
|
||||
dexcel students.xlsx count
|
||||
```
|
||||
|
||||
### 示例 2: 数据查询和分析
|
||||
|
||||
```bash
|
||||
# 查看前5行
|
||||
dexcel data.xlsx query 5
|
||||
|
||||
# 查看最后10行
|
||||
dexcel data.xlsx query -10
|
||||
|
||||
# 从第50行开始查看
|
||||
dexcel data.xlsx query --start 50
|
||||
|
||||
# 导出到文件
|
||||
dexcel data.xlsx query > output.txt
|
||||
|
||||
# 使用逗号分隔
|
||||
dexcel data.xlsx query --split ","
|
||||
```
|
||||
|
||||
### 示例 3: 数据更新
|
||||
|
||||
```bash
|
||||
# 更新第2行的所有数据
|
||||
dexcel data.xlsx update "新姓名|新年龄|新城市" --row 2
|
||||
|
||||
# 只更新第2行的第2列
|
||||
dexcel data.xlsx update "新年龄" --row 2 --cell 2
|
||||
|
||||
# 批量更新(脚本)
|
||||
for i in 2 3 4 5; do
|
||||
dexcel data.xlsx update "已审核" --row $i --cell 4
|
||||
done
|
||||
```
|
||||
|
||||
### 示例 4: 数据清理
|
||||
|
||||
```bash
|
||||
# 删除错误数据行
|
||||
dexcel data.xlsx delete --row 10
|
||||
|
||||
# 删除最后一行
|
||||
dexcel data.xlsx delete --row -1
|
||||
|
||||
# 删除特定单元格
|
||||
dexcel data.xlsx delete --row 5 --cell 3
|
||||
```
|
||||
|
||||
### 示例 5: 多 Sheet 操作
|
||||
|
||||
```bash
|
||||
# 在不同 Sheet 中操作
|
||||
dexcel data.xlsx insert "数据1" --sheet "Sheet1"
|
||||
dexcel data.xlsx insert "数据2" --sheet "Sheet2"
|
||||
|
||||
# 查询指定 Sheet
|
||||
dexcel data.xlsx query --sheet "Sales"
|
||||
|
||||
# 统计指定 Sheet 的行数
|
||||
dexcel data.xlsx count --sheet "Sales"
|
||||
```
|
||||
|
||||
## 📊 输出格式
|
||||
|
||||
### query 输出(表格格式)
|
||||
```
|
||||
┌──────┬──────┬──────┐
|
||||
│ 姓名 │ 年龄 │ 城市 │
|
||||
├──────┼──────┼──────┤
|
||||
│ 张三 │ 25 │ 北京 │
|
||||
├──────┼──────┼──────┤
|
||||
│ 李四 │ 30 │ 上海 │
|
||||
└──────┴──────┴──────┘
|
||||
|
||||
共输出 2 行数据,3 列
|
||||
```
|
||||
|
||||
**特点**:
|
||||
- ✅ 使用美观的表格格式(基于 tabled 库)
|
||||
- ✅ 自动对齐列宽
|
||||
- ✅ 清晰的分隔线和边框
|
||||
- ✅ 显示行数和列数统计
|
||||
- ✅ 易于阅读和查看
|
||||
|
||||
### count 输出
|
||||
```
|
||||
100
|
||||
```
|
||||
|
||||
**特点**:
|
||||
- 只输出一个数字
|
||||
- 适合脚本中使用
|
||||
- 可与其他命令组合
|
||||
|
||||
### 操作反馈
|
||||
|
||||
**insert 成功**:
|
||||
```
|
||||
✓ 操作成功:已创建新文件并插入一行数据
|
||||
文件: data.xlsx, Sheet: Sheet1
|
||||
```
|
||||
|
||||
**update 成功**:
|
||||
```
|
||||
✓ 操作成功:已更新第 5 行,从第 1 列开始
|
||||
文件: data.xlsx, Sheet: Sheet1
|
||||
```
|
||||
|
||||
**delete 成功**:
|
||||
```
|
||||
✓ 操作成功:已删除第 5 行
|
||||
剩余行数: 9
|
||||
文件: data.xlsx, Sheet: Sheet1
|
||||
```
|
||||
|
||||
## ⚙️ 技术细节
|
||||
|
||||
### 依赖库
|
||||
|
||||
- **calamine**: Excel 文件读取库,支持 .xlsx 格式
|
||||
- **rust_xlsxwriter**: Excel 文件写入库,功能强大
|
||||
- **clap**: 命令行参数解析库
|
||||
- **chrono**: 日期时间处理库
|
||||
|
||||
### 工作原理
|
||||
|
||||
#### 查询流程
|
||||
1. 打开 Excel 文件
|
||||
2. 选择指定的 Sheet(默认第一个)
|
||||
3. 根据参数计算读取范围
|
||||
4. 遍历单元格并提取数据
|
||||
5. 以指定分隔符格式输出
|
||||
|
||||
#### 写入流程
|
||||
1. 检查文件是否存在(insert 可自动创建)
|
||||
2. 读取现有数据到新工作簿
|
||||
3. 执行插入/更新/删除操作
|
||||
4. 保存文件
|
||||
|
||||
### 数据类型支持
|
||||
|
||||
**读取时支持:**
|
||||
- 字符串 (String)
|
||||
- 整数 (Int)
|
||||
- 浮点数 (Float)
|
||||
- 布尔值 (Bool)
|
||||
- 日期时间 (DateTime) - 自动识别并格式化
|
||||
- 空值 (Empty)
|
||||
- 错误值 (Error)
|
||||
|
||||
**写入时:**
|
||||
- 所有数据都作为字符串写入
|
||||
|
||||
## ⚠️ 注意事项
|
||||
|
||||
1. **索引从 1 开始**: 行和列的索引都是从 1 开始,不是 0
|
||||
2. **负数索引**: `-1` = 最后一行/列,`-2` = 倒数第二
|
||||
3. **文件自动创建**: insert 操作在文件不存在时自动创建
|
||||
4. **分隔符默认**: 默认使用 `|` 分隔列
|
||||
5. **文件格式**: 仅支持 `.xlsx`,不支持 `.xls`
|
||||
6. **删除需确认**: delete 操作必须用户确认才能执行
|
||||
7. **日期自动识别**: 自动识别并格式化日期单元格
|
||||
8. **不可撤销**: delete 操作不可撤销,请谨慎操作!
|
||||
|
||||
### 限制
|
||||
|
||||
**读取限制:**
|
||||
- 公式: 不计算公式,只读取原始值
|
||||
- 样式: 不读取单元格样式(颜色、字体等)
|
||||
- 图表: 不支持读取图表
|
||||
- 宏: 不支持 VBA 宏
|
||||
- 大文件: > 10MB 的文件可能较慢
|
||||
|
||||
**写入限制:**
|
||||
- 无样式: 不能设置颜色、字体等样式
|
||||
- 无公式: 不能写入公式
|
||||
- 覆盖警告: update 会覆盖目标单元格的原有内容
|
||||
- insert 下移: insert 会将原有数据下移
|
||||
|
||||
## 🔧 常见问题
|
||||
|
||||
### Q1: 如何查看文件有多少行?
|
||||
|
||||
```bash
|
||||
dexcel data.xlsx count
|
||||
```
|
||||
|
||||
### Q2: 如何追加多行数据?
|
||||
|
||||
多次调用 insert 命令:
|
||||
```bash
|
||||
dexcel data.xlsx insert "数据1|数据2"
|
||||
dexcel data.xlsx insert "数据3|数据4"
|
||||
```
|
||||
|
||||
### Q3: 负数参数怎么用?
|
||||
|
||||
```bash
|
||||
dexcel data.xlsx query -5 # 最后5行
|
||||
dexcel data.xlsx delete --row -1 # 删除最后一行
|
||||
```
|
||||
|
||||
### Q4: 如何处理中文内容?
|
||||
|
||||
直接传入,完全支持 UTF-8:
|
||||
```bash
|
||||
dexcel data.xlsx insert "张三|18|北京"
|
||||
```
|
||||
|
||||
### Q5: 如何在脚本中使用 count?
|
||||
|
||||
```bash
|
||||
# 获取行数并存储到变量
|
||||
$lines = dexcel data.xlsx count
|
||||
|
||||
# 条件判断
|
||||
if ((dexcel data.xlsx count) -gt 100) {
|
||||
Write-Host "文件超过100行"
|
||||
}
|
||||
```
|
||||
|
||||
### Q6: 删除操作能撤销吗?
|
||||
|
||||
不能撤销,请谨慎操作!建议先备份文件。
|
||||
|
||||
## 🎯 应用场景
|
||||
|
||||
### 1. 数据预览
|
||||
快速查看大型 Excel 文件的部分内容:
|
||||
```bash
|
||||
dexcel large-file.xlsx query 5
|
||||
```
|
||||
|
||||
### 2. 数据提取
|
||||
提取特定数据并导出:
|
||||
```bash
|
||||
dexcel data.xlsx query -10 > last-rows.txt
|
||||
```
|
||||
|
||||
### 3. 报告生成
|
||||
创建报告模板:
|
||||
```bash
|
||||
dexcel report.xlsx insert "月度报告||||"
|
||||
dexcel report.xlsx insert "日期|销售额|利润|备注"
|
||||
```
|
||||
|
||||
### 4. 数据更新
|
||||
批量更新某列数据:
|
||||
```bash
|
||||
dexcel data.xlsx update "新值" --row 5 --cell 2
|
||||
dexcel data.xlsx update "新值" --row 6 --cell 2
|
||||
```
|
||||
|
||||
### 5. 数据清理
|
||||
删除错误数据:
|
||||
```bash
|
||||
dexcel data.xlsx delete --row 10
|
||||
```
|
||||
|
||||
### 6. 自动化脚本
|
||||
PowerShell 批量导入:
|
||||
```powershell
|
||||
$students = @(
|
||||
@("张三", "18", "一班"),
|
||||
@("李四", "19", "二班")
|
||||
)
|
||||
|
||||
foreach ($student in $students) {
|
||||
dexcel students.xlsx insert "$($student[0])|$($student[1])|$($student[2])"
|
||||
}
|
||||
```
|
||||
|
||||
## 📝 批处理脚本示例
|
||||
|
||||
### Windows 批量导入 (import.bat)
|
||||
|
||||
```batch
|
||||
@echo off
|
||||
echo 正在导入数据...
|
||||
|
||||
dexcel data.xlsx insert "ID|名称|数量"
|
||||
dexcel data.xlsx insert "001|产品A|100"
|
||||
dexcel data.xlsx insert "002|产品B|200"
|
||||
dexcel data.xlsx insert "003|产品C|300"
|
||||
|
||||
echo 导入完成!
|
||||
dexcel data.xlsx query
|
||||
pause
|
||||
```
|
||||
|
||||
### PowerShell 数据处理 (process.ps1)
|
||||
|
||||
```powershell
|
||||
# 读取并处理数据
|
||||
$output = dexcel data.xlsx query 100
|
||||
|
||||
# 过滤包含关键词的行
|
||||
$filtered = $output | Select-String "关键词"
|
||||
|
||||
# 保存到文件
|
||||
$filtered | Out-File -FilePath filtered.txt -Encoding UTF8
|
||||
|
||||
Write-Host "处理完成!" -ForegroundColor Green
|
||||
```
|
||||
|
||||
## 🤝 贡献
|
||||
|
||||
欢迎提交 Issue 和 Pull Request!
|
||||
|
||||
可能的改进方向:
|
||||
- 支持更多数据类型(公式等)
|
||||
- 支持单元格样式(颜色、字体等)
|
||||
- 支持批量写入优化
|
||||
- 支持复制 Sheet
|
||||
- 添加数据验证功能
|
||||
- 支持更多输出格式(CSV, JSON 等)
|
||||
|
||||
## 📄 许可证
|
||||
|
||||
本项目采用 MIT 许可证。
|
||||
|
||||
## 🙏 致谢
|
||||
|
||||
感谢以下开源项目:
|
||||
- [calamine](https://github.com/tafia/calamine) - 优秀的 Excel 读取库
|
||||
- [rust_xlsxwriter](https://github.com/jmcnamara/rust_xlsxwriter) - 强大的 Excel 写入库
|
||||
- [clap](https://github.com/clap-rs/clap) - 优秀的命令行参数解析库
|
||||
- [chrono](https://github.com/chronotope/chrono) - 日期时间处理库
|
||||
|
||||
---
|
||||
|
||||
**Made with ❤️ using Rust**
|
||||
@@ -0,0 +1,48 @@
|
||||
@echo off
|
||||
set DEXCEL=%~dp0target\release\dexcel.exe
|
||||
echo ====================================
|
||||
echo dexcel 工具使用示例
|
||||
echo ====================================
|
||||
echo.
|
||||
|
||||
echo 1. 创建测试 Excel 文件
|
||||
echo -------------------
|
||||
%DEXCEL% write -f example.xlsx -x 1 -y 1 -v "姓名"
|
||||
%DEXCEL% write -f example.xlsx -x 1 -y 2 -v "年龄"
|
||||
%DEXCEL% write -f example.xlsx -x 1 -y 3 -v "城市"
|
||||
%DEXCEL% write -f example.xlsx -x 2 -y 1 -v "张三"
|
||||
%DEXCEL% write -f example.xlsx -x 2 -y 2 -v "25"
|
||||
%DEXCEL% write -f example.xlsx -x 2 -y 3 -v "北京"
|
||||
%DEXCEL% write -f example.xlsx -x 3 -y 1 -v "李四"
|
||||
%DEXCEL% write -f example.xlsx -x 3 -y 2 -v "30"
|
||||
%DEXCEL% write -f example.xlsx -x 3 -y 3 -v "上海"
|
||||
echo.
|
||||
|
||||
echo 2. 读取整个文件
|
||||
echo -------------------
|
||||
%DEXCEL% read -f example.xlsx
|
||||
echo.
|
||||
|
||||
echo 3. 读取第2行开始的数据(2行)
|
||||
echo -------------------
|
||||
%DEXCEL% read -f example.xlsx -r 2 -n 2
|
||||
echo.
|
||||
|
||||
echo 4. 只读取姓名列(第1列)
|
||||
echo -------------------
|
||||
%DEXCEL% read -f example.xlsx -c 1 -m 1
|
||||
echo.
|
||||
|
||||
echo 5. 读取最后一列(城市列)
|
||||
echo -------------------
|
||||
%DEXCEL% read -f example.xlsx -c -1 -m 1
|
||||
echo.
|
||||
|
||||
echo 6. 读取特定单元格(第2行,第2列)
|
||||
echo -------------------
|
||||
%DEXCEL% read -f example.xlsx -r 2 -n 1 -c 2 -m 1
|
||||
echo.
|
||||
|
||||
echo ====================================
|
||||
echo 示例完成!
|
||||
echo ====================================
|
||||
@@ -0,0 +1,706 @@
|
||||
use calamine::{open_workbook, Data, DataType, Reader, Xlsx};
|
||||
use chrono::{Datelike, Timelike};
|
||||
use clap::{Parser, Subcommand};
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(name = "dexcel")]
|
||||
#[command(about = "Excel 读写工具", long_about = None)]
|
||||
struct Cli {
|
||||
/// Excel 文件路径
|
||||
file: PathBuf,
|
||||
|
||||
#[command(subcommand)]
|
||||
command: Commands,
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
enum Commands {
|
||||
/// 查询数据
|
||||
Query {
|
||||
/// 获取行数(正数从前获取,负数从后获取)
|
||||
#[arg(default_value_t = 0, allow_hyphen_values = true)]
|
||||
count: i32,
|
||||
|
||||
/// 开始行号(从1开始,负数表示倒数)
|
||||
#[arg(long, allow_hyphen_values = true)]
|
||||
start: Option<i32>,
|
||||
|
||||
/// 获取最后一行
|
||||
#[arg(long)]
|
||||
last: bool,
|
||||
|
||||
/// Sheet 名称(可选,默认为第一个sheet)
|
||||
#[arg(long)]
|
||||
sheet: Option<String>,
|
||||
},
|
||||
/// 统计行数
|
||||
Count {
|
||||
/// Sheet 名称(可选,默认为第一个sheet)
|
||||
#[arg(long)]
|
||||
sheet: Option<String>,
|
||||
},
|
||||
/// 插入行
|
||||
Insert {
|
||||
/// 要插入的值(使用 split 分隔列)
|
||||
value: String,
|
||||
|
||||
/// 插入位置(行号,不指定则追加到末尾)
|
||||
#[arg(long, allow_hyphen_values = true)]
|
||||
row: Option<i32>,
|
||||
|
||||
/// Sheet 名称(可选,默认为第一个sheet)
|
||||
#[arg(long)]
|
||||
sheet: Option<String>,
|
||||
|
||||
/// 分割符号(默认 |)
|
||||
#[arg(long, default_value = "|")]
|
||||
split: String,
|
||||
},
|
||||
/// 更新数据
|
||||
Update {
|
||||
/// 要更新的值(使用 split 分隔列)
|
||||
value: String,
|
||||
|
||||
/// 行号(必需)
|
||||
#[arg(long, allow_hyphen_values = true)]
|
||||
row: i32,
|
||||
|
||||
/// 起始列号(从1开始,不指定则从第1列开始)
|
||||
#[arg(long)]
|
||||
cell: Option<u32>,
|
||||
|
||||
/// Sheet 名称(可选,默认为第一个sheet)
|
||||
#[arg(long)]
|
||||
sheet: Option<String>,
|
||||
|
||||
/// 分割符号(默认 |)
|
||||
#[arg(long, default_value = "|")]
|
||||
split: String,
|
||||
},
|
||||
/// 删除数据
|
||||
Delete {
|
||||
/// 删除数量(单元格数或行数)
|
||||
#[arg(default_value_t = 1)]
|
||||
count: u32,
|
||||
|
||||
/// 行号(必需)
|
||||
#[arg(long, allow_hyphen_values = true)]
|
||||
row: i32,
|
||||
|
||||
/// 列号(可选,不指定则删除整行)
|
||||
#[arg(long)]
|
||||
cell: Option<u32>,
|
||||
|
||||
/// Sheet 名称(可选,默认为第一个sheet)
|
||||
#[arg(long)]
|
||||
sheet: Option<String>,
|
||||
},
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let cli = Cli::parse();
|
||||
|
||||
match cli.command {
|
||||
Commands::Query {
|
||||
count,
|
||||
start,
|
||||
last,
|
||||
sheet,
|
||||
} => {
|
||||
if let Err(e) = query_excel(&cli.file, count, start, last, &sheet) {
|
||||
eprintln!("错误: {}", e);
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
Commands::Count { sheet } => {
|
||||
if let Err(e) = count_excel(&cli.file, &sheet) {
|
||||
eprintln!("错误: {}", e);
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
Commands::Insert {
|
||||
value,
|
||||
row,
|
||||
sheet,
|
||||
split,
|
||||
} => {
|
||||
if let Err(e) = insert_excel(&cli.file, &value, row, &sheet, &split) {
|
||||
eprintln!("错误: {}", e);
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
Commands::Update {
|
||||
value,
|
||||
row,
|
||||
cell,
|
||||
sheet,
|
||||
split,
|
||||
} => {
|
||||
if let Err(e) = update_excel(&cli.file, &value, row, cell, &sheet, &split) {
|
||||
eprintln!("错误: {}", e);
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
Commands::Delete {
|
||||
count,
|
||||
row,
|
||||
cell,
|
||||
sheet,
|
||||
} => {
|
||||
if let Err(e) = delete_excel(&cli.file, count, row, cell, &sheet) {
|
||||
eprintln!("错误: {}", e);
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 格式化单元格值为字符串
|
||||
fn format_cell(cell: &Data) -> String {
|
||||
match cell {
|
||||
Data::Empty => String::new(),
|
||||
Data::Int(v) => v.to_string(),
|
||||
Data::Float(v) => {
|
||||
if *v > 1.0 && *v < 2958466.0 && (*v - v.floor()).abs() < 0.0001 {
|
||||
let temp_data = Data::Float(*v);
|
||||
if let Some(dt) = temp_data.as_datetime() {
|
||||
let year = dt.year();
|
||||
if year >= 1900 && year <= 2100 {
|
||||
return dt.format("%Y-%m-%d").to_string();
|
||||
}
|
||||
}
|
||||
}
|
||||
v.to_string()
|
||||
}
|
||||
Data::String(s) => s.clone(),
|
||||
Data::Bool(b) => b.to_string(),
|
||||
Data::DateTime(d) => {
|
||||
if let Some(dt) = d.as_datetime() {
|
||||
if dt.hour() == 0 && dt.minute() == 0 && dt.second() == 0 {
|
||||
dt.format("%Y-%m-%d").to_string()
|
||||
} else {
|
||||
dt.format("%Y-%m-%d %H:%M:%S").to_string()
|
||||
}
|
||||
} else {
|
||||
d.to_string()
|
||||
}
|
||||
}
|
||||
Data::DateTimeIso(s) => s.clone(),
|
||||
Data::DurationIso(s) => s.clone(),
|
||||
Data::Error(e) => format!("#{}", e),
|
||||
_ => String::new(),
|
||||
}
|
||||
}
|
||||
|
||||
// 统计行数(count 命令)
|
||||
fn count_excel(
|
||||
file: &PathBuf,
|
||||
sheet_name: &Option<String>,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let mut workbook: Xlsx<_> = open_workbook(file)?;
|
||||
|
||||
let sheet = match sheet_name {
|
||||
Some(name) => name.clone(),
|
||||
None => {
|
||||
let sheets = workbook.sheet_names();
|
||||
if sheets.is_empty() {
|
||||
return Err("Excel 文件中没有 sheet".into());
|
||||
}
|
||||
sheets[0].clone()
|
||||
}
|
||||
};
|
||||
|
||||
let range = workbook
|
||||
.worksheet_range(&sheet)
|
||||
.map_err(|e| format!("找不到 sheet: {}, 错误: {}", sheet, e))?;
|
||||
|
||||
let total_rows = range.height();
|
||||
|
||||
println!("{}", total_rows);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// 查询数据(query 命令)
|
||||
fn query_excel(
|
||||
file: &PathBuf,
|
||||
count: i32,
|
||||
start: Option<i32>,
|
||||
last: bool,
|
||||
sheet_name: &Option<String>,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let mut workbook: Xlsx<_> = open_workbook(file)?;
|
||||
|
||||
let sheet = match sheet_name {
|
||||
Some(name) => name.clone(),
|
||||
None => {
|
||||
let sheets = workbook.sheet_names();
|
||||
if sheets.is_empty() {
|
||||
return Err("Excel 文件中没有 sheet".into());
|
||||
}
|
||||
sheets[0].clone()
|
||||
}
|
||||
};
|
||||
|
||||
let range = workbook
|
||||
.worksheet_range(&sheet)
|
||||
.map_err(|e| format!("找不到 sheet: {}, 错误: {}", sheet, e))?;
|
||||
|
||||
let total_rows = range.height();
|
||||
let _total_cols = range.width();
|
||||
|
||||
if total_rows == 0 {
|
||||
println!("表格为空");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// 计算要读取的行范围
|
||||
let (start_row, end_row) = if last {
|
||||
(total_rows - 1, total_rows)
|
||||
} else if let Some(s) = start {
|
||||
// 使用 start 参数
|
||||
let actual_start = if s < 0 {
|
||||
let pos = total_rows as i32 + s;
|
||||
if pos < 0 { 0 } else { pos as usize }
|
||||
} else {
|
||||
(s as usize).saturating_sub(1)
|
||||
};
|
||||
|
||||
if count > 0 {
|
||||
(actual_start, (actual_start + count as usize).min(total_rows))
|
||||
} else if count < 0 {
|
||||
let abs_count = (-count) as usize;
|
||||
let end = (actual_start + abs_count).min(total_rows);
|
||||
(actual_start, end)
|
||||
} else {
|
||||
// count 为 0,显示从 start 到末尾(最多20行)
|
||||
let max_display = 20;
|
||||
let end = (actual_start + max_display).min(total_rows);
|
||||
if total_rows - actual_start > max_display {
|
||||
eprintln!("提示: 从第 {} 行开始共有 {} 行数据,仅显示 {} 行",
|
||||
actual_start + 1, total_rows - actual_start, max_display);
|
||||
}
|
||||
(actual_start, end)
|
||||
}
|
||||
} else if count > 0 {
|
||||
(0, count as usize)
|
||||
} else if count < 0 {
|
||||
let abs_count = (-count) as usize;
|
||||
let start = if abs_count >= total_rows { 0 } else { total_rows - abs_count };
|
||||
(start, total_rows)
|
||||
} else {
|
||||
// 默认显示最后20行
|
||||
let max_display = 20;
|
||||
if total_rows > max_display {
|
||||
eprintln!("提示: 共有 {} 行数据,仅显示最后 {} 行", total_rows, max_display);
|
||||
eprintln!("如需查看更多数据,请使用 dexcel query <行数> 或 --last\n");
|
||||
(total_rows - max_display, total_rows)
|
||||
} else {
|
||||
(0, total_rows)
|
||||
}
|
||||
};
|
||||
|
||||
let end_row = end_row.min(total_rows);
|
||||
|
||||
// 收集所有数据
|
||||
let mut data: Vec<Vec<String>> = Vec::new();
|
||||
for r in start_row..end_row {
|
||||
let mut row_values = Vec::new();
|
||||
for c in 0..range.width() {
|
||||
let cell = range.get((r, c));
|
||||
if let Some(cell_data) = cell {
|
||||
row_values.push(format_cell(cell_data));
|
||||
} else {
|
||||
row_values.push(String::new());
|
||||
}
|
||||
}
|
||||
data.push(row_values);
|
||||
}
|
||||
|
||||
if data.is_empty() {
|
||||
println!("没有数据");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// 使用 tabled 输出表格
|
||||
let output_count = data.len();
|
||||
let col_count = data[0].len();
|
||||
|
||||
// 使用 Builder API 创建动态表格
|
||||
use tabled::builder::Builder;
|
||||
let mut builder = Builder::default();
|
||||
|
||||
// 添加所有行
|
||||
for row in &data {
|
||||
builder.push_record(row.iter().map(|s| s.as_str()));
|
||||
}
|
||||
|
||||
let mut table = builder.build();
|
||||
|
||||
// 设置样式
|
||||
table.with(tabled::settings::Style::modern());
|
||||
|
||||
println!("{}", table);
|
||||
eprintln!("\n共输出 {} 行数据,{} 列", output_count, col_count);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// 插入行(insert 命令)
|
||||
fn insert_excel(
|
||||
file: &PathBuf,
|
||||
value: &str,
|
||||
row: Option<i32>,
|
||||
sheet_name: &Option<String>,
|
||||
split: &str,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
use rust_xlsxwriter::*;
|
||||
|
||||
let sheet = match sheet_name {
|
||||
Some(name) => name.clone(),
|
||||
None => "Sheet1".to_string(),
|
||||
};
|
||||
|
||||
// 如果文件不存在,创建新文件
|
||||
if !file.exists() {
|
||||
let mut workbook = Workbook::new();
|
||||
let worksheet = workbook.add_worksheet();
|
||||
worksheet.set_name(&sheet)?;
|
||||
|
||||
let values: Vec<&str> = value.split(split).collect();
|
||||
for (col_idx, val) in values.iter().enumerate() {
|
||||
let trimmed = val.trim();
|
||||
if !trimmed.is_empty() {
|
||||
worksheet.write_string(0, col_idx as u16, trimmed)?;
|
||||
}
|
||||
}
|
||||
|
||||
workbook.save(file.as_path())?;
|
||||
println!("✓ 操作成功:已创建新文件并插入一行数据");
|
||||
eprintln!(" 文件: {}, Sheet: {}", file.display(), sheet);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let mut workbook: Xlsx<_> = open_workbook(file.as_path())?;
|
||||
|
||||
let range = workbook
|
||||
.worksheet_range(&sheet)
|
||||
.map_err(|e| format!("找不到 sheet: {}, 错误: {}", sheet, e))?;
|
||||
|
||||
let total_rows = range.height();
|
||||
let total_cols = range.width();
|
||||
|
||||
// 确定插入位置
|
||||
let insert_row = match row {
|
||||
Some(r) => {
|
||||
if r < 0 {
|
||||
let pos = total_rows as i32 + r + 1;
|
||||
if pos < 0 || pos > total_rows as i32 {
|
||||
return Err(format!("行号无效: {}", r).into());
|
||||
}
|
||||
pos as usize
|
||||
} else {
|
||||
let pos = r as usize - 1;
|
||||
if pos > total_rows {
|
||||
return Err(format!("行号超出范围: {} (总行数: {})", r, total_rows).into());
|
||||
}
|
||||
pos
|
||||
}
|
||||
}
|
||||
None => total_rows, // 默认追加到末尾
|
||||
};
|
||||
|
||||
// 创建新工作簿
|
||||
let mut new_workbook = Workbook::new();
|
||||
let new_worksheet = new_workbook.add_worksheet();
|
||||
new_worksheet.set_name(&sheet)?;
|
||||
|
||||
let values: Vec<&str> = value.split(split).collect();
|
||||
let mut new_row = 0u32;
|
||||
|
||||
// 复制插入行之前的数据
|
||||
for r in 0..insert_row {
|
||||
for c in 0..range.width() {
|
||||
if let Some(cell_data) = range.get((r, c)) {
|
||||
write_cell(new_worksheet, new_row, c as u16, cell_data)?;
|
||||
}
|
||||
}
|
||||
new_row += 1;
|
||||
}
|
||||
|
||||
// 写入新行
|
||||
for (col_idx, val) in values.iter().enumerate() {
|
||||
let trimmed = val.trim();
|
||||
if !trimmed.is_empty() {
|
||||
new_worksheet.write_string(new_row, col_idx as u16, trimmed)?;
|
||||
}
|
||||
}
|
||||
new_row += 1;
|
||||
|
||||
// 复制插入行之后的数据
|
||||
for r in insert_row..total_rows {
|
||||
for c in 0..range.width() {
|
||||
if let Some(cell_data) = range.get((r, c)) {
|
||||
write_cell(new_worksheet, new_row, c as u16, cell_data)?;
|
||||
}
|
||||
}
|
||||
new_row += 1;
|
||||
}
|
||||
|
||||
// 保存文件
|
||||
new_workbook.save(file.as_path())?;
|
||||
|
||||
println!("✓ 操作成功:已在第 {} 行插入一行数据", insert_row + 1);
|
||||
eprintln!(" 文件: {}, Sheet: {}, 总行数: {}", file.display(), sheet, total_rows + 1);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// 更新数据(update 命令)
|
||||
fn update_excel(
|
||||
file: &PathBuf,
|
||||
value: &str,
|
||||
row: i32,
|
||||
cell: Option<u32>,
|
||||
sheet_name: &Option<String>,
|
||||
split: &str,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
use rust_xlsxwriter::*;
|
||||
|
||||
if !file.exists() {
|
||||
return Err(format!("文件不存在: {}", file.display()).into());
|
||||
}
|
||||
|
||||
let mut workbook: Xlsx<_> = open_workbook(file.as_path())?;
|
||||
|
||||
let sheet = match sheet_name {
|
||||
Some(name) => name.clone(),
|
||||
None => {
|
||||
let sheets = workbook.sheet_names();
|
||||
if sheets.is_empty() {
|
||||
return Err("Excel 文件中没有 sheet".into());
|
||||
}
|
||||
sheets[0].clone()
|
||||
}
|
||||
};
|
||||
|
||||
let range = workbook
|
||||
.worksheet_range(&sheet)
|
||||
.map_err(|e| format!("找不到 sheet: {}, 错误: {}", sheet, e))?;
|
||||
|
||||
let total_rows = range.height();
|
||||
let total_cols = range.width();
|
||||
|
||||
// 确定行号
|
||||
let target_row = if row < 0 {
|
||||
let pos = total_rows as i32 + row;
|
||||
if pos < 0 || pos >= total_rows as i32 {
|
||||
return Err(format!("行号无效: {}", row).into());
|
||||
}
|
||||
pos as usize
|
||||
} else {
|
||||
let pos = row as usize - 1;
|
||||
if pos >= total_rows {
|
||||
return Err(format!("行号超出范围: {} (总行数: {})", row, total_rows).into());
|
||||
}
|
||||
pos
|
||||
};
|
||||
|
||||
// 确定起始列
|
||||
let start_col = cell.map(|c| (c - 1) as usize).unwrap_or(0);
|
||||
|
||||
// 创建新工作簿并复制所有数据
|
||||
let mut new_workbook = Workbook::new();
|
||||
let new_worksheet = new_workbook.add_worksheet();
|
||||
new_worksheet.set_name(&sheet)?;
|
||||
|
||||
let values: Vec<&str> = value.split(split).collect();
|
||||
|
||||
for r in 0..total_rows {
|
||||
for c in 0..range.width() {
|
||||
if r == target_row && c >= start_col {
|
||||
// 更新这一行的指定列
|
||||
let col_offset = c - start_col;
|
||||
if col_offset < values.len() {
|
||||
let trimmed = values[col_offset].trim();
|
||||
if !trimmed.is_empty() {
|
||||
new_worksheet.write_string(r as u32, c as u16, trimmed)?;
|
||||
}
|
||||
} else {
|
||||
// 超出提供的值,保持原样
|
||||
if let Some(cell_data) = range.get((r, c)) {
|
||||
write_cell(new_worksheet, r as u32, c as u16, cell_data)?;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 复制原有数据
|
||||
if let Some(cell_data) = range.get((r, c)) {
|
||||
write_cell(new_worksheet, r as u32, c as u16, cell_data)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 保存文件
|
||||
new_workbook.save(file.as_path())?;
|
||||
|
||||
println!("✓ 操作成功:已更新第 {} 行,从第 {} 列开始", target_row + 1, start_col + 1);
|
||||
eprintln!(" 文件: {}, Sheet: {}", file.display(), sheet);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// 删除数据(delete 命令)
|
||||
fn delete_excel(
|
||||
file: &PathBuf,
|
||||
count: u32,
|
||||
row: i32,
|
||||
cell: Option<u32>,
|
||||
sheet_name: &Option<String>,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
use rust_xlsxwriter::*;
|
||||
|
||||
if !file.exists() {
|
||||
return Err(format!("文件不存在: {}", file.display()).into());
|
||||
}
|
||||
|
||||
let mut workbook: Xlsx<_> = open_workbook(file.as_path())?;
|
||||
|
||||
let sheet = match sheet_name {
|
||||
Some(name) => name.clone(),
|
||||
None => {
|
||||
let sheets = workbook.sheet_names();
|
||||
if sheets.is_empty() {
|
||||
return Err("Excel 文件中没有 sheet".into());
|
||||
}
|
||||
sheets[0].clone()
|
||||
}
|
||||
};
|
||||
|
||||
let range = workbook
|
||||
.worksheet_range(&sheet)
|
||||
.map_err(|e| format!("找不到 sheet: {}, 错误: {}", sheet, e))?;
|
||||
|
||||
let total_rows = range.height();
|
||||
let total_cols = range.width();
|
||||
|
||||
// 确定行号
|
||||
let target_row = if row < 0 {
|
||||
let pos = total_rows as i32 + row;
|
||||
if pos < 0 || pos >= total_rows as i32 {
|
||||
return Err(format!("行号无效: {}", row).into());
|
||||
}
|
||||
pos as usize
|
||||
} else {
|
||||
let pos = row as usize - 1;
|
||||
if pos >= total_rows {
|
||||
return Err(format!("行号超出范围: {} (总行数: {})", row, total_rows).into());
|
||||
}
|
||||
pos
|
||||
};
|
||||
|
||||
// 显示确认信息
|
||||
if let Some(c) = cell {
|
||||
eprintln!("⚠ 警告:即将删除单元格");
|
||||
eprintln!(" 文件: {}", file.display());
|
||||
eprintln!(" Sheet: {}", sheet);
|
||||
eprintln!(" 位置: 第 {} 行, 第 {} 列", target_row + 1, c);
|
||||
} else {
|
||||
eprintln!("⚠ 警告:即将删除以下数据:");
|
||||
eprintln!(" 文件: {}", file.display());
|
||||
eprintln!(" Sheet: {}", sheet);
|
||||
eprintln!(" 删除行: 第 {} 行(共 {} 行)", target_row + 1, total_rows);
|
||||
|
||||
// 显示要删除的行的内容
|
||||
eprint!(" 内容: ");
|
||||
for c in 0..range.width() {
|
||||
if let Some(cell_data) = range.get((target_row, c)) {
|
||||
eprint!("{}", format_cell(cell_data));
|
||||
if c < range.width() - 1 {
|
||||
eprint!(" | ");
|
||||
}
|
||||
}
|
||||
}
|
||||
eprintln!();
|
||||
}
|
||||
eprintln!();
|
||||
|
||||
// 请求用户确认
|
||||
eprint!("是否继续?(y/N): ");
|
||||
use std::io::{self, Write};
|
||||
io::stdout().flush().unwrap();
|
||||
|
||||
let mut input = String::new();
|
||||
io::stdin().read_line(&mut input)?;
|
||||
let input = input.trim().to_lowercase();
|
||||
|
||||
if input != "y" && input != "yes" {
|
||||
println!("✗ 操作已取消");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// 创建新工作簿
|
||||
let mut new_workbook = Workbook::new();
|
||||
let new_worksheet = new_workbook.add_worksheet();
|
||||
new_worksheet.set_name(&sheet)?;
|
||||
|
||||
if let Some(c) = cell {
|
||||
// 删除单元格:将该单元格及其右侧的单元格左移
|
||||
let target_col = (c - 1) as usize;
|
||||
let mut new_row = 0u32;
|
||||
|
||||
for r in 0..total_rows {
|
||||
let mut new_col = 0u16;
|
||||
for col in 0..total_cols {
|
||||
if r == target_row && col == target_col {
|
||||
continue; // 跳过要删除的单元格
|
||||
}
|
||||
if let Some(cell_data) = range.get((r, col)) {
|
||||
write_cell(new_worksheet, new_row, new_col, cell_data)?;
|
||||
new_col += 1;
|
||||
}
|
||||
}
|
||||
new_row += 1;
|
||||
}
|
||||
|
||||
println!("✓ 操作成功:已删除第 {} 行第 {} 列的单元格", target_row + 1, c);
|
||||
} else {
|
||||
// 删除整行
|
||||
let mut new_row = 0u32;
|
||||
for r in 0..total_rows {
|
||||
if r == target_row {
|
||||
continue; // 跳过要删除的行
|
||||
}
|
||||
for c in 0..range.width() {
|
||||
if let Some(cell_data) = range.get((r, c)) {
|
||||
write_cell(new_worksheet, new_row, c as u16, cell_data)?;
|
||||
}
|
||||
}
|
||||
new_row += 1;
|
||||
}
|
||||
|
||||
println!("✓ 操作成功:已删除第 {} 行", target_row + 1);
|
||||
eprintln!(" 剩余行数: {}", total_rows - 1);
|
||||
}
|
||||
|
||||
// 保存文件
|
||||
new_workbook.save(file.as_path())?;
|
||||
eprintln!(" 文件: {}, Sheet: {}", file.display(), sheet);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// 辅助函数:写入单元格
|
||||
fn write_cell(
|
||||
worksheet: &mut rust_xlsxwriter::Worksheet,
|
||||
row: u32,
|
||||
col: u16,
|
||||
data: &Data,
|
||||
) -> Result<(), rust_xlsxwriter::XlsxError> {
|
||||
match data {
|
||||
Data::String(s) => worksheet.write_string(row, col, s.as_str()).map(|_| ()),
|
||||
Data::Int(v) => worksheet.write_number(row, col, *v as f64).map(|_| ()),
|
||||
Data::Float(v) => worksheet.write_number(row, col, *v).map(|_| ()),
|
||||
Data::Bool(b) => worksheet.write_boolean(row, col, *b).map(|_| ()),
|
||||
_ => Ok(()),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
/target
|
||||
@@ -0,0 +1,12 @@
|
||||
[package]
|
||||
name = "zip"
|
||||
version = "0.1.1"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
clap = { version = "4.5", features = ["derive"] }
|
||||
zip = "2.2"
|
||||
walkdir = "2.5"
|
||||
arboard = "3.4"
|
||||
clipboard-win = "5.4"
|
||||
winapi = { version = "0.3", features = ["winuser"] }
|
||||
+422
@@ -0,0 +1,422 @@
|
||||
# zip - ZIP 打包工具 📦
|
||||
|
||||
一个快速、易用的命令行 ZIP 打包工具,支持将文件或文件夹打包成 zip 压缩文件。
|
||||
|
||||
## ✨ 功能特性
|
||||
|
||||
- 📁 **灵活输入**: 支持单个文件、多个文件或整个文件夹
|
||||
- 🗜️ **高效压缩**: 使用 Deflate 压缩算法
|
||||
- 📋 **剪贴板集成**: 可选择将生成的 zip 文件复制到剪贴板(Windows)
|
||||
- 🎯 **智能命名**: 自动生成有意义的文件名
|
||||
- 📊 **详细反馈**: 显示打包进度和统计信息
|
||||
- ⚡ **高性能**: 基于 Rust 开发,速度快,资源占用低
|
||||
- 🔒 **跨平台**: 支持 Windows、macOS、Linux
|
||||
|
||||
## 📦 安装
|
||||
|
||||
### 从源码编译
|
||||
|
||||
```bash
|
||||
cd zip
|
||||
cargo build --release
|
||||
```
|
||||
|
||||
编译后的可执行文件位于 `target/release/zip.exe` (Windows) 或 `target/release/zip` (Unix)
|
||||
|
||||
### 添加到系统路径(可选)
|
||||
|
||||
将可执行文件复制到系统 PATH 目录,或将其所在目录添加到 PATH 环境变量中。
|
||||
|
||||
## 🚀 快速开始
|
||||
|
||||
### 基本用法
|
||||
|
||||
```bash
|
||||
# 打包单个文件
|
||||
zip myfile.txt
|
||||
|
||||
# 打包文件夹
|
||||
zip myfolder
|
||||
|
||||
# 打包多个文件
|
||||
zip file1.txt file2.txt file3.txt
|
||||
```
|
||||
|
||||
### 指定输出文件名
|
||||
|
||||
```bash
|
||||
# 自定义输出的 zip 文件名
|
||||
zip myfile.txt -n archive.zip
|
||||
|
||||
# 打包文件夹并指定名称
|
||||
zip myfolder -n backup.zip
|
||||
```
|
||||
|
||||
### 复制到剪贴板
|
||||
|
||||
```bash
|
||||
# 打包后自动复制文件到剪贴板(Windows)
|
||||
zip myfile.txt -v
|
||||
|
||||
# 打包文件夹并复制到剪贴板
|
||||
zip myfolder -v -n backup.zip
|
||||
```
|
||||
|
||||
## 📖 命令参数
|
||||
|
||||
```bash
|
||||
zip [OPTIONS] <PATH...>
|
||||
|
||||
参数:
|
||||
<PATH...> 📁 要打包的文件或文件夹路径(可以指定多个,必需)
|
||||
-n, --name <FILE> 📝 输出的 zip 文件名(默认自动生成)
|
||||
-v, --clipboard 📋 复制 zip 文件路径到剪贴板
|
||||
-h, --help 显示帮助信息
|
||||
-V, --version 显示版本信息
|
||||
```
|
||||
|
||||
## 💡 使用示例
|
||||
|
||||
### 示例 1: 打包单个文件
|
||||
|
||||
```bash
|
||||
zip document.pdf
|
||||
```
|
||||
|
||||
**输出:**
|
||||
- 生成 `document.zip`
|
||||
- 位置: 与源文件相同的目录
|
||||
|
||||
### 示例 2: 打包整个文件夹
|
||||
|
||||
```bash
|
||||
zip project-folder
|
||||
```
|
||||
|
||||
**输出:**
|
||||
- 生成 `project-folder.zip`
|
||||
- 包含文件夹内的所有文件和子文件夹
|
||||
- 保留目录结构
|
||||
|
||||
### 示例 3: 打包多个文件
|
||||
|
||||
```bash
|
||||
zip readme.md config.json main.py
|
||||
```
|
||||
|
||||
**输出:**
|
||||
- 生成 `readme.zip`(以第一个文件命名)
|
||||
- 包含所有指定的文件
|
||||
|
||||
### 示例 4: 自定义输出文件名
|
||||
|
||||
```bash
|
||||
zip important-docs -n docs-backup-2024.zip
|
||||
```
|
||||
|
||||
**输出:**
|
||||
- 生成 `docs-backup-2024.zip`
|
||||
- 在当前目录创建
|
||||
|
||||
### 示例 5: 打包并复制到剪贴板(Windows)
|
||||
|
||||
```bash
|
||||
zip my-project -v -n project-archive.zip
|
||||
```
|
||||
|
||||
**输出:**
|
||||
- 生成 `project-archive.zip`
|
||||
- 文件路径自动复制到剪贴板
|
||||
- 可以直接在文件资源管理器中粘贴
|
||||
|
||||
### 示例 6: 批量备份多个文件夹
|
||||
|
||||
```bash
|
||||
zip folder1 folder2 folder3 -n multi-backup.zip
|
||||
```
|
||||
|
||||
**输出:**
|
||||
- 生成 `multi-backup.zip`
|
||||
- 包含所有三个文件夹的内容
|
||||
|
||||
## 📊 输出说明
|
||||
|
||||
### 控制台输出示例
|
||||
|
||||
```
|
||||
📦 开始打包...
|
||||
📂 源文件/文件夹: 3 个
|
||||
1. "file1.txt"
|
||||
2. "file2.txt"
|
||||
3. "myfolder"
|
||||
📄 输出文件: "archive.zip"
|
||||
|
||||
✓ file1.txt
|
||||
✓ file2.txt
|
||||
✓ myfolder/subfolder/file3.txt
|
||||
✓ myfolder/image.png
|
||||
✅ 打包完成!
|
||||
📊 共打包 4 个文件
|
||||
💾 文件大小: 1.25 MB
|
||||
📍 保存位置: C:\Users\username\Documents\archive.zip
|
||||
📋 已复制文件到剪切板: C:\Users\username\Documents\archive.zip
|
||||
```
|
||||
|
||||
### 文件命名规则
|
||||
|
||||
如果不指定 `-n` 参数:
|
||||
- **单个文件**: 使用文件的基本名 + `.zip`
|
||||
- `document.pdf` → `document.zip`
|
||||
- **多个文件**: 使用第一个文件的基本名 + `.zip`
|
||||
- `file1.txt file2.txt` → `file1.zip`
|
||||
- **文件夹**: 使用文件夹名 + `.zip`
|
||||
- `myfolder` → `myfolder.zip`
|
||||
|
||||
如果指定了 `-n` 参数:
|
||||
- 使用指定的完整路径和文件名
|
||||
|
||||
## 🎯 应用场景
|
||||
|
||||
### 1. 项目备份
|
||||
|
||||
```bash
|
||||
# 快速备份当前项目
|
||||
zip . -n project-backup-$(date +%Y%m%d).zip
|
||||
|
||||
# Windows PowerShell
|
||||
zip . -n "project-backup-$(Get-Date -Format 'yyyyMMdd').zip"
|
||||
```
|
||||
|
||||
### 2. 文件分享
|
||||
|
||||
```bash
|
||||
# 打包多个文档准备分享
|
||||
zip report.docx data.xlsx charts.png -n weekly-report.zip
|
||||
|
||||
# 复制路径到剪贴板,方便通过聊天软件发送
|
||||
zip weekly-report.zip -v
|
||||
```
|
||||
|
||||
### 3. 代码归档
|
||||
|
||||
```bash
|
||||
# 打包源代码(排除 node_modules 等需要在 .gitignore 中配置)
|
||||
zip src tests package.json -n source-code.zip
|
||||
```
|
||||
|
||||
### 4. 日志收集
|
||||
|
||||
```bash
|
||||
# 打包日志文件用于问题排查
|
||||
zip logs/ -n debug-logs.zip
|
||||
```
|
||||
|
||||
### 5. 资源打包
|
||||
|
||||
```bash
|
||||
# 打包游戏或应用资源
|
||||
zip assets/ configs/ -n game-resources.zip
|
||||
```
|
||||
|
||||
## 📁 目录结构处理
|
||||
|
||||
### 打包文件夹时的结构保留
|
||||
|
||||
假设你有以下目录结构:
|
||||
|
||||
```
|
||||
myproject/
|
||||
├── src/
|
||||
│ ├── main.rs
|
||||
│ └── utils.rs
|
||||
├── Cargo.toml
|
||||
└── README.md
|
||||
```
|
||||
|
||||
执行 `zip myproject` 后,zip 文件内部结构为:
|
||||
|
||||
```
|
||||
myproject/
|
||||
├── src/
|
||||
│ ├── main.rs
|
||||
│ └── utils.rs
|
||||
├── Cargo.toml
|
||||
└── README.md
|
||||
```
|
||||
|
||||
解压时会完整保留目录结构。
|
||||
|
||||
### 打包多个独立文件
|
||||
|
||||
执行 `zip file1.txt file2.txt` 后,zip 文件内部:
|
||||
|
||||
```
|
||||
file1.txt
|
||||
file2.txt
|
||||
```
|
||||
|
||||
文件直接放在根目录,不包含路径前缀。
|
||||
|
||||
## ⚙️ 技术细节
|
||||
|
||||
### 压缩算法
|
||||
|
||||
- **方法**: Deflate
|
||||
- **压缩级别**: 默认级别(平衡速度和压缩率)
|
||||
- **权限保留**: Unix 系统保留文件权限(0o755)
|
||||
|
||||
### 依赖库
|
||||
|
||||
- **clap**: 命令行参数解析
|
||||
- **zip**: ZIP 文件读写
|
||||
- **walkdir**: 递归遍历目录
|
||||
- **clipboard-win**: Windows 剪贴板操作(仅 Windows)
|
||||
- **arboard**: 跨平台剪贴板操作(非 Windows)
|
||||
- **winapi**: Windows API 调用(仅 Windows)
|
||||
|
||||
### 性能特点
|
||||
|
||||
- **流式写入**: 边读取边压缩,内存占用低
|
||||
- **并行处理**: 文件遍历使用 walkdir 优化
|
||||
- **增量打包**: 逐个添加文件,支持大文件
|
||||
|
||||
### 平台差异
|
||||
|
||||
#### Windows
|
||||
- 使用 `clipboard-win` 和 `winapi` 实现文件复制到剪贴板
|
||||
- 支持直接复制文件对象(可在资源管理器中粘贴)
|
||||
- 自动重试机制处理剪贴板占用问题
|
||||
|
||||
#### macOS / Linux
|
||||
- 使用 `arboard` 实现文本复制到剪贴板
|
||||
- 复制的是文件路径字符串
|
||||
- 需要手动在文件管理器中打开路径
|
||||
|
||||
## ⚠️ 注意事项
|
||||
|
||||
1. **隐藏文件**: 默认会打包隐藏文件(以 `.` 开头的文件)
|
||||
2. **符号链接**: 符号链接会被跟随,打包实际文件
|
||||
3. **空目录**: 空目录可能不会被包含在 zip 中
|
||||
4. **文件锁定**: 确保要打包的文件没有被其他程序独占锁定
|
||||
5. **磁盘空间**: 确保有足够的磁盘空间存放生成的 zip 文件
|
||||
6. **特殊字符**: 文件名中的特殊字符会被保留,但某些系统可能不兼容
|
||||
|
||||
## 🔧 常见问题
|
||||
|
||||
### Q1: 如何排除某些文件或文件夹?
|
||||
|
||||
目前版本不支持排除功能。建议:
|
||||
- 先将要打包的文件复制到临时目录
|
||||
- 或使用其他工具如 7-Zip、WinRAR
|
||||
|
||||
未来版本可能会添加 `--exclude` 参数。
|
||||
|
||||
### Q2: 压缩包太大怎么办?
|
||||
|
||||
- 检查是否包含了不必要的大文件(如视频、数据库)
|
||||
- 考虑先压缩大文件再打包
|
||||
- 使用专业的压缩工具进行二次压缩
|
||||
|
||||
### Q3: 如何在 Linux/macOS 上使用剪贴板功能?
|
||||
|
||||
在非 Windows 系统上,`-v` 参数会复制文件路径文本。你可以:
|
||||
- 在终端中使用 `xdg-open` (Linux) 或 `open` (macOS) 打开路径
|
||||
- 或在文件管理器中使用"前往文件夹"功能粘贴路径
|
||||
|
||||
### Q4: 打包速度慢?
|
||||
|
||||
- 检查是否有大量小文件(会增加 overhead)
|
||||
- 确认磁盘读写速度
|
||||
- 考虑使用 SSD 而非 HDD
|
||||
|
||||
### Q5: 如何处理中文文件名?
|
||||
|
||||
工具完全支持 UTF-8 编码的中文文件名,在 Windows、macOS、Linux 上都能正常工作。
|
||||
|
||||
## 📝 完整工作流示例
|
||||
|
||||
### 工作流 1: 日常备份脚本
|
||||
|
||||
创建一个批处理文件 `backup.bat` (Windows):
|
||||
|
||||
```batch
|
||||
@echo off
|
||||
set TIMESTAMP=%date:~0,4%%date:~5,2%%date:~8,2%_%time:~0,2%%time:~3,2%
|
||||
set TIMESTAMP=%TIMESTAMP: =0%
|
||||
|
||||
echo 正在备份项目...
|
||||
zip my-project -n "backup-%TIMESTAMP%.zip" -v
|
||||
|
||||
if %errorlevel% equ 0 (
|
||||
echo ✅ 备份成功!
|
||||
) else (
|
||||
echo ❌ 备份失败!
|
||||
)
|
||||
```
|
||||
|
||||
### 工作流 2: 项目发布准备
|
||||
|
||||
```bash
|
||||
# 1. 清理构建产物
|
||||
cargo clean
|
||||
|
||||
# 2. 打包源代码
|
||||
zip src/ Cargo.toml README.md LICENSE -n myproject-source.zip
|
||||
|
||||
# 3. 构建发布版本
|
||||
cargo build --release
|
||||
|
||||
# 4. 打包可执行文件
|
||||
zip target/release/myapp -n myproject-v1.0.0.zip
|
||||
|
||||
# 5. 复制路径到剪贴板
|
||||
zip myproject-v1.0.0.zip -v
|
||||
```
|
||||
|
||||
### 工作流 3: 日志收集和问题报告
|
||||
|
||||
```bash
|
||||
# 1. 创建临时目录
|
||||
mkdir temp-logs
|
||||
|
||||
# 2. 复制相关日志文件
|
||||
cp logs/*.log temp-logs/
|
||||
cp config.yaml temp-logs/
|
||||
|
||||
# 3. 打包
|
||||
zip temp-logs -n bug-report-logs.zip
|
||||
|
||||
# 4. 清理临时目录
|
||||
rm -rf temp-logs
|
||||
|
||||
# 5. 复制到剪贴板方便上传
|
||||
zip bug-report-logs.zip -v
|
||||
```
|
||||
|
||||
## 🤝 贡献
|
||||
|
||||
欢迎提交 Issue 和 Pull Request!
|
||||
|
||||
可能的改进方向:
|
||||
- 添加 `--exclude` 参数支持排除文件
|
||||
- 支持压缩级别选择
|
||||
- 添加密码保护功能
|
||||
- 支持分卷压缩
|
||||
- 显示压缩进度条
|
||||
|
||||
## 📄 许可证
|
||||
|
||||
本项目采用 MIT 许可证。
|
||||
|
||||
## 🙏 致谢
|
||||
|
||||
感谢以下开源项目:
|
||||
- [clap](https://github.com/clap-rs/clap) - 优秀的命令行参数解析库
|
||||
- [zip](https://github.com/zip-rs/zip) - ZIP 文件格式支持
|
||||
- [walkdir](https://github.com/BurntSushi/walkdir) - 高效的目录遍历
|
||||
- [clipboard-win](https://github.com/DoumanAsh/clipboard-win) - Windows 剪贴板支持
|
||||
- [arboard](https://github.com/1Password/arboard) - 跨平台剪贴板支持
|
||||
|
||||
---
|
||||
|
||||
**Made with ❤️ using Rust**
|
||||
+257
@@ -0,0 +1,257 @@
|
||||
use clap::Parser;
|
||||
use std::path::PathBuf;
|
||||
use std::fs::File;
|
||||
use std::io::{Read, Write};
|
||||
use walkdir::WalkDir;
|
||||
use zip::write::SimpleFileOptions;
|
||||
|
||||
/// ZIP 打包工具 - 将文件或文件夹打包成 zip 格式
|
||||
#[derive(Parser, Debug)]
|
||||
#[command(name = "zip")]
|
||||
#[command(author, version, about, long_about = None)]
|
||||
#[command(
|
||||
about = "📦 ZIP 打包工具 - 将文件或文件夹快速打包成 zip 格式",
|
||||
long_about = "📦 ZIP 打包工具\n\n将指定的文件或文件夹打包成 zip 压缩文件。\n支持单个文件、多个文件或整个文件夹的打包。\n\n示例:\n zip src\n zip file1.txt file2.txt -n archive.zip\n zip myfolder -v -n backup.zip"
|
||||
)]
|
||||
struct Args {
|
||||
/// 📁 要打包的文件或文件夹路径(可以指定多个)
|
||||
#[arg(required = true, value_name = "PATH")]
|
||||
sources: Vec<PathBuf>,
|
||||
|
||||
/// 📝 输出的 zip 文件名(默认自动生成)
|
||||
#[arg(short = 'n', long = "name", value_name = "FILE")]
|
||||
name: Option<PathBuf>,
|
||||
|
||||
/// 📋 复制 zip 文件路径到剪切板
|
||||
#[arg(short = 'v', long = "clipboard")]
|
||||
clipboard: bool,
|
||||
}
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let args = Args::parse();
|
||||
|
||||
// 验证输入路径
|
||||
for source in &args.sources {
|
||||
if !source.exists() {
|
||||
eprintln!("❌ 错误: 路径不存在: {:?}", source);
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// 生成输出文件名
|
||||
let output_path = if let Some(name) = &args.name {
|
||||
name.clone()
|
||||
} else {
|
||||
// 根据第一个源文件/文件夹生成默认名称
|
||||
let first_source = &args.sources[0];
|
||||
let stem = first_source
|
||||
.file_stem()
|
||||
.and_then(|s| s.to_str())
|
||||
.unwrap_or("archive");
|
||||
|
||||
let mut path = if args.sources.len() == 1 {
|
||||
first_source.parent().unwrap_or(std::path::Path::new(".")).to_path_buf()
|
||||
} else {
|
||||
std::env::current_dir()?
|
||||
};
|
||||
path.push(format!("{}.zip", stem));
|
||||
path
|
||||
};
|
||||
|
||||
println!("\n📦 开始打包...");
|
||||
println!("📂 源文件/文件夹: {} 个", args.sources.len());
|
||||
for (i, source) in args.sources.iter().enumerate() {
|
||||
println!(" {}. {:?}", i + 1, source);
|
||||
}
|
||||
println!("📄 输出文件: {:?}\n", output_path);
|
||||
|
||||
// 创建 zip 文件
|
||||
let zip_file = File::create(&output_path)?;
|
||||
let mut zip_writer = zip::ZipWriter::new(zip_file);
|
||||
|
||||
let options = SimpleFileOptions::default()
|
||||
.compression_method(zip::CompressionMethod::Deflated)
|
||||
.unix_permissions(0o755);
|
||||
|
||||
let mut file_count = 0;
|
||||
let mut total_size = 0u64;
|
||||
|
||||
// 遍历所有源路径
|
||||
for source in &args.sources {
|
||||
if source.is_file() {
|
||||
// 处理单个文件
|
||||
add_file_to_zip(&mut zip_writer, source, "", options, &mut file_count, &mut total_size)?;
|
||||
} else if source.is_dir() {
|
||||
// 处理文件夹
|
||||
add_dir_to_zip(&mut zip_writer, source, options, &mut file_count, &mut total_size)?;
|
||||
}
|
||||
}
|
||||
|
||||
zip_writer.finish()?;
|
||||
|
||||
println!("✅ 打包完成!");
|
||||
println!("📊 共打包 {} 个文件", file_count);
|
||||
println!("💾 文件大小: {}", format_bytes(total_size));
|
||||
println!("📍 保存位置: {:?}", output_path.canonicalize()?);
|
||||
|
||||
// 如果需要复制到剪切板
|
||||
if args.clipboard {
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
// 在 Windows 上复制文件到剪切板
|
||||
use clipboard_win::{formats::FileList, Setter};
|
||||
use winapi::um::winuser::{OpenClipboard, CloseClipboard, EmptyClipboard};
|
||||
use winapi::shared::windef::HWND;
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
|
||||
// 获取绝对路径(不带 \\?\ 前缀)
|
||||
let abs_path = if output_path.is_absolute() {
|
||||
output_path.clone()
|
||||
} else {
|
||||
std::env::current_dir()?.join(&output_path)
|
||||
};
|
||||
|
||||
// 转换为标准字符串格式
|
||||
let path_str = abs_path.to_string_lossy().to_string();
|
||||
let paths: Vec<&str> = vec![path_str.as_str()];
|
||||
|
||||
// 尝试多次打开剪贴板并复制文件
|
||||
for attempt in 0..5 {
|
||||
unsafe {
|
||||
// 尝试打开剪贴板
|
||||
if OpenClipboard(std::ptr::null_mut() as HWND) != 0 {
|
||||
// 清空剪贴板
|
||||
EmptyClipboard();
|
||||
|
||||
// 尝试写入文件列表
|
||||
match FileList.write_clipboard(&paths) {
|
||||
Ok(_) => {
|
||||
println!("📋 已复制文件到剪切板: {}", path_str);
|
||||
CloseClipboard();
|
||||
break;
|
||||
}
|
||||
Err(e) => {
|
||||
CloseClipboard();
|
||||
if attempt < 4 {
|
||||
thread::sleep(Duration::from_millis(200));
|
||||
} else {
|
||||
eprintln!("⚠️ 复制文件失败: {}", e);
|
||||
eprintln!("💡 提示: 请手动复制文件或使用资源管理器打开该位置");
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if attempt < 4 {
|
||||
thread::sleep(Duration::from_millis(200));
|
||||
} else {
|
||||
eprintln!("⚠️ 无法打开剪贴板,可能被其他程序占用");
|
||||
eprintln!("💡 提示: 请关闭其他使用剪贴板的程序后重试");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
{
|
||||
let canonical_path = output_path.canonicalize()?;
|
||||
let path_str = canonical_path.to_string_lossy().to_string();
|
||||
use arboard::Clipboard;
|
||||
match Clipboard::new() {
|
||||
Ok(mut clipboard) => {
|
||||
match clipboard.set_text(path_str.clone()) {
|
||||
Ok(_) => println!("📋 已复制路径到剪切板: {}", path_str),
|
||||
Err(e) => eprintln!("⚠️ 复制到剪切板失败: {}", e),
|
||||
}
|
||||
}
|
||||
Err(e) => eprintln!("⚠️ 无法访问剪切板: {}", e),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
println!();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 将单个文件添加到 zip
|
||||
fn add_file_to_zip(
|
||||
writer: &mut zip::ZipWriter<File>,
|
||||
file_path: &std::path::Path,
|
||||
base_path: &str,
|
||||
options: SimpleFileOptions,
|
||||
file_count: &mut usize,
|
||||
total_size: &mut u64,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let file_name = if base_path.is_empty() {
|
||||
file_path.file_name().unwrap().to_string_lossy().to_string()
|
||||
} else {
|
||||
format!("{}/{}", base_path, file_path.file_name().unwrap().to_string_lossy())
|
||||
};
|
||||
|
||||
writer.start_file(file_name, options)?;
|
||||
|
||||
let mut file = File::open(file_path)?;
|
||||
let file_size = file.metadata()?.len();
|
||||
*total_size += file_size;
|
||||
|
||||
let mut buffer = Vec::new();
|
||||
file.read_to_end(&mut buffer)?;
|
||||
writer.write_all(&buffer)?;
|
||||
|
||||
*file_count += 1;
|
||||
println!(" ✓ {}", file_path.display());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 将整个文件夹添加到 zip
|
||||
fn add_dir_to_zip(
|
||||
writer: &mut zip::ZipWriter<File>,
|
||||
dir_path: &std::path::Path,
|
||||
options: SimpleFileOptions,
|
||||
file_count: &mut usize,
|
||||
total_size: &mut u64,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
for entry in WalkDir::new(dir_path).into_iter().filter_map(|e| e.ok()) {
|
||||
let path = entry.path();
|
||||
|
||||
if path.is_file() {
|
||||
// 计算相对路径
|
||||
let relative_path = path.strip_prefix(dir_path.parent().unwrap_or(dir_path))?;
|
||||
let file_name = relative_path.to_string_lossy().replace('\\', "/");
|
||||
|
||||
writer.start_file(file_name.clone(), options)?;
|
||||
|
||||
let mut file = File::open(path)?;
|
||||
let file_size = file.metadata()?.len();
|
||||
*total_size += file_size;
|
||||
|
||||
let mut buffer = Vec::new();
|
||||
file.read_to_end(&mut buffer)?;
|
||||
writer.write_all(&buffer)?;
|
||||
|
||||
*file_count += 1;
|
||||
println!(" ✓ {}", path.display());
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 格式化字节大小
|
||||
fn format_bytes(bytes: u64) -> String {
|
||||
const KB: u64 = 1024;
|
||||
const MB: u64 = KB * 1024;
|
||||
const GB: u64 = MB * 1024;
|
||||
|
||||
if bytes >= GB {
|
||||
format!("{:.2} GB", bytes as f64 / GB as f64)
|
||||
} else if bytes >= MB {
|
||||
format!("{:.2} MB", bytes as f64 / MB as f64)
|
||||
} else if bytes >= KB {
|
||||
format!("{:.2} KB", bytes as f64 / KB as f64)
|
||||
} else {
|
||||
format!("{} B", bytes)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user