用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结构体需要实现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目录,识别所有插件并创建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工具,开发过程中使用了clapregex库。

rust语言学习入门有一定的难度,但掌握之后开发效率还是比较高效的,编译的程序体积小又易于跨平台使用,还是一门很不错的语言的。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值