龙空技术网

用Rust清理eclipse自动升级后的重复插件

寒江雪归客 97

前言:

现时兄弟们对“eclipse运行出现ant build”大致比较看重,姐妹们都想要分析一些“eclipse运行出现ant build”的相关内容。那么小编同时在网络上网罗了一些关于“eclipse运行出现ant build””的相关知识,希望我们能喜欢,各位老铁们快快来了解一下吧!

1. 简介

eclipse自动升级版本之后,在/eclipse/plugins目录仍然会保留旧版本的插件,想要写一个脚本清理插件,正好最近刚学习rust编程,便用rust开发了一个eclipse插件清理工具eclean

本文简单介绍清理工具的开发过程,详细源代码可以在github下载并自行编译:

git clone  ./ecleanercargo 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.14sPS E:\GitHub\ecleaner> .\target\debug\eclean.exe --helpeclean 1.1.0Steven 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 outputARGS:    <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:/eclipseerror: The following required arguments were not provided:    <BACKUP>USAGE:    eclean.exe [FLAGS] <DIR> <BACKUP>For more information try --helpPS E:\GitHub\ecleaner>
4. 版本号识别4.1 eclipse版本号

我们观察eclipse插件的版本号,由4部分构成:major.minor.patch.build:

major:主版本号minor:子版本号patch:补丁版本号build:构建版本号,可忽略

例如:org.apache.axis1.4.0.v201411182030,org.eclipse.core.runtime3.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结构体需要实现EqOrdPartialEqPartialOrd四个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目录,识别所有插件并创建PluginSetfind_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语言学习入门有一定的难度,但掌握之后开发效率还是比较高效的,编译的程序体积小又易于跨平台使用,还是一门很不错的语言的。

标签: #eclipse运行出现ant build