用Rust清理eclipse自动升级后的重复插件
1. 简介
eclipse自动升级版本之后,在/eclipse/plugins
目录仍然会保留旧版本的插件,想要写一个脚本清理插件,正好最近刚学习rust编程,便用rust开发了一个eclipse插件清理工具eclean
。
本文简单介绍清理工具的开发过程,详细源代码可以在github下载并自行编译:
git clone https://github.com/leexgone/ecleaner.git
cd ./ecleaner
cargo build --release
工具支持清理eclipse升级后plugins目录下的冗余插件。
- 清理eclipse插件目录并将清理插件备份:
eclean c:/eclipse e:/backup/eclipse
- 检测eclipse目录下是否含有可清理的插件:
eclean -t c:/eclipse
- 更多使用命令查阅:
eclean --help
2. 创建工程
使用cargo new elean
创建工程,调整Cargo.toml
内容并在src目录下创建lib.rs
文件。
3. 命令行解析
eclean是一个命令行工具,首先我们需要支持命令行参数的解析。
rust的clap库是一套功能强大的命令行参数解析库,这里我们使用clap解析命令行参数。
3.1 引用clap
库
在Cargo.toml
里加入clap依赖:
[dependencies]
clap = "2.33.3"
3.2 创建Config
结构
编辑lib.rs
代码,定义Config
结构存储命令配置信息,使用clap解析命令参数:
use std::{collections::HashMap, error::Error, fs, io::{self, ErrorKind}, path::{Path, PathBuf}, usize};
use std::fmt::Display;
use clap::{App, Arg};
pub struct Config {
dir: String,
backup: String,
verbose: bool,
test: bool,
force: bool,
}
impl Display for Config {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "[dir = {}, backup = {}, verbose = {}, test = {}, force = {}]", self.dir, self.backup, self.verbose, self.test, self.force)
}
}
impl Config {
pub fn new() -> Result<Config, String> {
let matches = App::new("eclean")
.version("1.1.0")
.author("Steven Lee <leexgone@163.com>")
.about("Clean up the duplicated plugins in eclipse plugins directory.")
.arg(Arg::with_name("DIR")
.help("The eclipse root directory to be cleaned. The `/plugins` directory should be under this directory.")
.required(true)
.index(1))
.arg(Arg::with_name("BACKUP")
.help("Specify a backup directory to store the removed plugins.")
.required_unless("test")
.index(2))
.arg(Arg::with_name("verbose")
.short("v")
.long("verbose")
.help("Use verbose output"))
.arg(Arg::with_name("test")
.short("t")
.long("test")
.help("Scan and find the duplicated plugins, but do nothing"))
.arg(Arg::with_name("force")
.short("f")
.long("force")
.help("Clean up the duplicated plugins automatically. Never prompt."))
.get_matches();
let dir = matches.value_of("DIR").unwrap();
let backup = matches.value_of("BACKUP").unwrap_or("");
let verbose = matches.is_present("verbose");
let test = matches.is_present("test");
let force = matches.is_present("force");
let root_path = Path::new(dir);
if !root_path.is_dir() {
let msg = format!("DIR '{}' does not exist", dir);
return Err(msg);
}
if !test {
let backup_path = Path::new(backup);
if !backup_path.is_dir() {
let msg = format!("BACKUP dir '{}' does not exist", backup);
return Err(msg);
}
}
Ok(Config {
dir: String::from(dir),
backup: String::from(backup),
verbose,
test,
force,
})
}
}
Config
结构存储了用户参数命令的配置信息:
- dir:eclipse目录(必须)
- backup: 清理插件备份目录(必须,在test模式下可以忽略)
- verbose:输出详细日志
- test:仅检测eclipse插件目录,不执行清理操作
- force:不询问用户强制清理插件
这里我们对用户的输入进行了检测,在目录不存在提示错误;而必须参数的校验则通过clap
完成即可。
3.3 调整main
代码
在main.rs
中加入参数命令的解析代码:
use std::process;
use eclean::Config;
fn main() {
let config = Config::new().unwrap_or_else(|err| {
eprintln!("Error when parsing arguments: {}.", err);
process::exit(1);
});
// ...
}
3.4 测试效果
PS E:\GitHub\ecleaner> cargo build
Finished dev [unoptimized + debuginfo] target(s) in 0.14s
PS E:\GitHub\ecleaner> .\target\debug\eclean.exe --help
eclean 1.1.0
Steven Lee <leexgone@163.com>
Clean up the duplicated plugins in eclipse plugins directory.
USAGE:
eclean.exe [FLAGS] <DIR> <BACKUP>
FLAGS:
-h, --help Prints help information
-t, --test Scan and find the duplicated plugins, but do nothing
-V, --version Prints version information
-v, --verbose Use verbose output
ARGS:
<DIR> The eclipse root directory to be cleaned. The `/plugins` directory should be under this directory.
<BACKUP> Specify a backup directory to store the removed plugins.
PS E:\GitHub\ecleaner> .\target\debug\eclean.exe d:/eclipse
error: The following required arguments were not provided:
<BACKUP>
USAGE:
eclean.exe [FLAGS] <DIR> <BACKUP>
For more information try --help
PS E:\GitHub\ecleaner>
4. 版本号识别
4.1 eclipse版本号
我们观察eclipse插件的版本号,由4部分构成:major.minor.patch.build
:
- major:主版本号
- minor:次版本号
- patch: 补丁版本号
- build:构建版本号,可忽略
例如:org.apache.axis_1.4.0.v201411182030
,org.eclipse.core.runtime_3.20.0.v20201027-1526
.jar。
这里,我们设计Version
结构存储版本信息,因为部分插件没有提供build版本号,我们使用Option<String>
存储。
pub struct Version {
pub major: usize,
pub minor: usize,
pub patch: usize,
pub build: Option<String>,
}
4.2 Version
实现
在清理插件时,我们需要对多个版本的插件版本进行排序,这里我们通过Version
实现排序trait来实现。
为支持排序,Version
结构体需要实现Eq
、Ord
、PartialEq
和PartialOrd
四个trait。
Version
的解析方法比较简单,通过字符串的分隔即可实现,这里我们仅实现版本号字符串的解析,整体插件名称的解析在后面处理。
我们在工程里增加version.rs
文件,编写Version
结构体代码:
use std::{error::Error, fmt::Display};
#[derive(Debug)]
#[derive(Eq)]
pub struct Version {
pub major: usize,
pub minor: usize,
pub patch: usize,
pub build: Option<String>,
}
impl Version {
pub fn new(major: usize, minor: usize, patch: usize, build: Option<&str>) -> Version {
Version {
major,
minor,
patch,
build: if let Some(text) = build {
Some(String::from(text))
} else {
None
}
}
}
pub fn parse(expr: &str) -> Result<Version, Box<dyn Error>> {
let mut version = Version {
major: 0,
minor: 0,
patch: 0,
build: None,
};
for (i, val) in expr.split('.').enumerate() {
match i {
0 => {
version.major = val.parse()?;
}
1 => {
version.minor = val.parse()?;
}
2 => {
version.patch = val.parse()?;
}
3 => {
version.build = Some(String::from(val));
}
_ => {
}
}
};
Ok(version)
}
}
impl Display for Version {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if let Some(suffix) = self.build.as_ref() {
write!(f, "{}.{}.{}.{}", self.major, self.minor, self.patch, suffix)
} else {
write!(f, "{}.{}.{}", self.major, self.minor, self.patch)
}
}
}
impl Ord for Version {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
let ret = self.major.cmp(&other.major);
if ret != std::cmp::Ordering::Equal {
return ret;
}
let ret = self.minor.cmp(&other.minor);
if ret != std::cmp::Ordering::Equal {
return ret;
}
let ret = self.build.cmp(&other.build);
if ret != std::cmp::Ordering::Equal {
return ret;
}
let self_build = if let Some(build) = self.build.as_ref() {
build
} else {
""
};
let other_build = if let Some(build) = other.build.as_ref() {
build
} else {
""
};
self_build.cmp(other_build)
}
}
impl PartialOrd for Version {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl PartialEq for Version {
fn eq(&self, other: &Self) -> bool {
self.cmp(other) == std::cmp::Ordering::Equal
}
}
4.3 模块声明
因为我们引入了一个新的源代码文件version.rs
,为保证编译通过,需要在lib.rs
文件开头增加如下声明:
mod version;
5. 插件识别
eclipse的插件有两种形式:
- 以目录形式打包的插件,目录名类似
org.apache.ant_1.10.9.v20201106-1946
- 以JAR包形式打包的插件,文件名类似
org.eclipse.core.runtime_3.20.0.v20201027-1526.jar
所有的插件命名以plugin-name_version-expr的形式命名,为进行插件的扫描和比较,我们需要记录插件的标识和版本号;此外,还需要记录插件的目录或文件名,以用于后续的清理操作。
5.1 Plugin
结构
创建plugin.rs
源代码文件,声明Plugin
结构体:
pub struct Plugin {
pub path: PathBuf,
pub name: String,
pub version: Version,
}
在lib.rs
中增加plugin
模块声明:
mod plugin;
5.2 解析插件
要解析插件的名称和版本号,需要将插件文件名(目录名)拆分,因为插件名称和版本号中都可能含有_
符号,这里不能简单的使用字符分隔。
在这里我们需要使用正则表达式进行识别,首先引入rust的regex库,在Cargo.toml
里增加插件引用:
[dependencies]
regex = "1.4.3"
然后修改plugin.rs
中的代码,Plugin::new()
函数实现解析代码,解析时我们需要区分目录和文件的插件形式。
use std::{error::Error, fmt::Display, fs, io::ErrorKind, path::PathBuf, usize};
use regex::Regex;
use super::version::Version;
#[derive(Debug)]
pub struct Plugin {
pub path: PathBuf,
pub name: String,
pub version: Version,
}
impl Display for Plugin {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}({})", self.name, self.version)
}
}
macro_rules! return_err {
($msg:expr) => {
{
let e = std::io::Error::new(ErrorKind::Other, $msg);
return Err(Box::new(e));
}
}
}
impl Plugin {
pub fn new(path: PathBuf) -> Result<Plugin, Box<dyn Error>> {
let filename: String;
let name = if path.is_file() {
path.file_stem()
} else {
path.file_name()
};
match name {
Some(stem) => {
filename = String::from(stem.to_str().unwrap());
},
None => {
return_err!(format!("Error parsing plugin: {}", path.display()));
}
}
let regex = Regex::new("_\\d+[.]\\d+[.]\\d+")?;
let (name, version) = if let Some(m) = regex.find(&filename) {
let plugin_name = &filename[0..m.start()];
let version_expr = &filename[m.start() + 1..];
match Version::parse(version_expr) {
Ok(version) => {
(
String::from(plugin_name),
version,
)
},
Err(e) => {
return_err!(format!("Error parsings plugin `{}`: {}", path.display(), e));
}
}
} else {
(
filename,
Version::new(0, 0, 0, None)
)
};
Ok(Plugin {
path,
name,
version
})
}
}
其中,我们定义了return_err宏简化异常处理
5.3 插件操作
重复插件清理时,需要将插件从/eclipse/plugins
目录移动到备份目录中,我们为Plugin
结构体提供move_to()
方法实现插件迁移。由于插件可能是目录形式,使用递归的方式进行移动。
impl Plugin {
pub fn move_to(&self, target: &PathBuf) -> Result<usize, Box<dyn Error>> {
let count = Plugin::copy_all(&self.path, target)?;
self.remove()?;
Ok(count)
}
fn copy_all(root: &PathBuf, target: &PathBuf) -> Result<usize, Box<dyn Error>> {
let mut count: usize = 0;
let mut dest_path = target.clone();
dest_path.push(root.file_name().unwrap());
if root.is_file() {
fs::copy(&root, &dest_path)?;
count += 1;
} else if root.is_dir() {
if !dest_path.exists() {
fs::create_dir(&dest_path)?;
}
for entry in root.read_dir()? {
let entry = entry?;
let sub_path = entry.path();
count += Plugin::copy_all(&sub_path, &dest_path)?;
}
}
Ok(count)
}
fn remove(&self) -> Result<(), Box<dyn Error>> {
if self.path.is_file() {
fs::remove_file(&self.path)?;
} else if self.path.is_dir() {
fs::remove_dir_all(&self.path)?;
}
Ok(())
}
}
6. 插件清理
6.1 插件扫描
要清理重复的插件,首先需要对eclipse的所有插件进行扫描,我们定义PluginSet
结构体存储插件并封装清理操作,使用哈希表来存储扫描后的插件。
修改lib.rs
代码,增加PluginSet
定义与实现:
macro_rules! log {
($enabled:expr) => {
{if $enabled { println!(); }}
};
($enabled:expr, $($arg:tt)*) => {
{if $enabled { println!($($arg)*); }}
};
}
#[derive(Debug)]
struct PluginSet {
plugins: HashMap<String, Vec<Plugin>>,
}
impl PluginSet {
fn new(dir: &str, verbose: bool) -> Result<PluginSet, Box<dyn Error>> {
let plugin_path = PathBuf::from(format!("{}/plugins", dir));
if !plugin_path.is_dir() {
let e = std::io::Error::new(ErrorKind::NotFound, format!("Can not find `plugins` dir under `{}` dir", dir));
return Err(Box::new(e));
}
let mut plugins: HashMap<String, Vec<Plugin>> = HashMap::new();
log!(verbose, "Search plugins under dir `{}`...", plugin_path.display());
for entry in plugin_path.read_dir()? {
let entry = entry?;
let path = entry.path();
let plugin = Plugin::new(path)?;
log!(verbose, ">> {}", plugin);
if let Some(list) = plugins.get_mut(&plugin.name) {
list.push(plugin);
} else {
plugins.insert(plugin.name.clone(), vec![plugin]);
}
}
for list in plugins.values_mut() {
list.sort_by(|a, b| a.version.cmp(&b.version));
}
Ok(PluginSet { plugins })
}
fn find_duplicates(&self) -> Vec<&Vec<Plugin>> {
self.plugins.values().filter(|list| list.len() > 1).collect()
}
fn print_dupicates(duplicates: &Vec<&Vec<Plugin>>) {
println!("{} duplicated plugins found:", duplicates.len());
for (i, list) in duplicates.iter().enumerate() {
let id = i + 1;
let plugins = *list;
let keep = plugins.last().unwrap();
print!(" {}\t{} [KEEP: {}; DISCARD: ", id, keep.name, keep.version);
for (p, plugin) in plugins.iter().enumerate() {
if p == plugins.len() - 1 {
break;
}
if p > 0 {
print!(", ");
}
print!("{}", plugin.version);
}
println!("]");
}
}
fn remove_duplicates(duplicates: &Vec<&Vec<Plugin>>, backup: &str, verbose: bool) -> Result<(), Box<dyn Error>> {
let backup_path: PathBuf = [backup, "plugins"].iter().collect();
if !backup_path.exists() {
fs::create_dir(&backup_path)?;
log!(verbose, "Create backup dir: {}", backup_path.display());
}
let mut count = 0;
for list in duplicates {
let plugins = *list;
let keep = plugins.last().unwrap();
log!(verbose, "Cleaning up `{}`, lastest: v{}...", keep.name, keep.version);
for (i, plugin) in plugins.iter().enumerate() {
if i == plugins.len() - 1 {
break;
}
let file_count = plugin.move_to(&backup_path)?;
log!(verbose, " remove version v{}, {} files deleted.", plugin.version, file_count);
count += 1;
}
}
println!("{} plugins have been cleaned up successfully!", count);
Ok(())
}
}
其中:
new()
方法扫描指定的eclipse目录,识别所有插件并创建PluginSet
find_duplicates()
方法过滤存在重复版本的插件列表print_dupicates()
方法打印存在重复版本的插件信息remove_duplicates()
方法执行清理,将旧版本插件移动到备份目录中
6.2 执行清理
在lib.rs
中增加插件清理方法:
pub fn run(config: Config) -> Result<(), Box<dyn Error>> {
let plugin_set = PluginSet::new(&config.dir, config.verbose)?;
let duplicates = plugin_set.find_duplicates();
if duplicates.is_empty() {
println!("There are no duplidated plugins.")
} else {
PluginSet::print_dupicates(&duplicates);
if !config.test {
if config.force || prompt(duplicates.len()) {
PluginSet::remove_duplicates(&duplicates, &config.backup, config.verbose)?;
}
}
}
Ok(())
}
fn prompt(size: usize) -> bool {
println!("{} plugins will be removed to the backup dir. Continue to remove these plugins? [Y/n] ", size);
let mut answer = String::new();
io::stdin().read_line(&mut answer).expect("Invalidate input");
let answer = answer.trim();
"Y".eq_ignore_ascii_case(answer) || "YES".eq_ignore_ascii_case(answer)
}
最后,我们调整main.rs
,完成所有实现:
use std::process;
use eclean::Config;
fn main() {
let config = Config::new().unwrap_or_else(|err| {
eprintln!("Error when parsing arguments: {}.", err);
process::exit(1);
});
if let Err(e) = eclean::run(config) {
eprintln!("Error when cleaning up: {}", e);
process::exit(2);
}
}
7. 总结
我们使用rust开发了一个eclean工具,开发过程中使用了clap和regex库。
rust语言学习入门有一定的难度,但掌握之后开发效率还是比较高效的,编译的程序体积小又易于跨平台使用,还是一门很不错的语言的。