遇到一个比较小的命令行解析需求,特意写了个较小的lib.

Options

用于定义参数的类,包含Option子类描述一个参数

代码如下:

package top.evalexp.ami.cmd;
 
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.function.Consumer;
import java.util.function.Function;
 
public class Options {
    // shortNameMap: map shortname to Option object
    private Map<String, Option> shortNameMap = new HashMap<>();
    // longNameMap: map longname to Option object
    private Map<String, Option> longNameMap = new HashMap<>();
 
    /**
     * Options.Type
     * Specify the Option's Type
     */
    public interface Type {
        Object BOOLEAN = Boolean.FALSE;
        Object INTEGER = 0;
        Object DOUBLE = Double.NaN;
        Object STRING = "";
    }
 
    static {
        // add common caster
        Parser.addCaster(Type.BOOLEAN.getClass(), Boolean::parseBoolean);
        Parser.addCaster(Type.INTEGER.getClass(), Integer::parseInt);
        Parser.addCaster(Type.DOUBLE.getClass(), Double::parseDouble);
        Parser.addCaster(Type.STRING.getClass(), String::valueOf);
    }
 
    public void forEach(Consumer<Option> consumer) {
        for (Map.Entry<String, Option> entry : longNameMap.entrySet()) {
            consumer.accept(entry.getValue());
        }
    }
 
    public Options() {
    }
 
    /**
     * add an option
     * @param option    Option object
     * @return this object, for chain call
     * @param <T> Option Type
     */
    public  <T> Options addOption(Option<T> option) {
        if (option.getName() == null || option.getName().isEmpty()) {
            throw new IllegalArgumentException("option name or type is null or empty");
        }
        if (shortNameMap.containsKey(option.getShortName()) || longNameMap.containsKey(option.getName())) {
            throw new IllegalArgumentException("option shortName or longName is already exist");
        }
        if (option.getShortName() != null) shortNameMap.put(option.getShortName(), option);
        longNameMap.put(option.getName(), option);
        return this;
    }
 
    /**
     * add an option by name, shortname
     * @param name  argument name
     * @param shortName argument shortname
     * @param type  argument type, only for type use, value is ignored
     * @return  this object, for chain call
     * @param <T>   Option type
     */
    public <T> Options addOption(String name, String shortName, T type) {
        return this.addOption(new Option<T>(name, shortName, null, null, type));
    }
 
    /**
     * add an option by name, shortname
     * @param name  argument name
     * @param shortName argument shortname
     * @param defaultValue argument default value
     * @param type  argument type, only for type use, value is ignored
     * @return  this object, for chain call
     * @param <T>   Option type
     */
    public <T> Options addOption(String name, String shortName, T type, T defaultValue) {
        return this.addOption(new Option<T>(name, shortName, defaultValue, null, type));
    }
 
    /**
     * add an option by name
     * @param name  argument name
     * @param type  argument type, only for type use, value is ignored
     * @return this object, for chain call
     * @param <T>   Option type
     */
    public <T> Options addOption(String name, T type) {
        return this.addOption(new Option<T>(name, null, null, null, type));
    }
 
    /**
     * add an option by name
     * @param name  argument name
     * @param defaultValue argument default value
     * @param type  argument type, only for type use, value is ignored
     * @return this object, for chain call
     * @param <T>   Option type
     */
    public <T> Options addOption(String name, T type, T defaultValue) {
        return this.addOption(new Option<T>(name, null, defaultValue, null, type));
    }
 
    /**
     * add an option by name, checker
     * @param name  argument name
     * @param checker   argument checker
     * @param type  argument type, only for type use, value is ignored
     * @return  this object, for chain call
     * @param <T>   Option type
     */
    public <T> Options addOption(String name, Function<T, Boolean> checker, T type) {
        return this.addOption(new Option<T>(name, null, null, checker, type));
    }
 
    /**
     * add an option by name, checker
     * @param name  argument name
     * @param defaultValue argument default value
     * @param checker   argument checker
     * @param type  argument type, only for type use, value is ignored
     * @return  this object, for chain call
     * @param <T>   Option type
     */
    public <T> Options addOption(String name, Function<T, Boolean> checker, T type, T defaultValue) {
        return this.addOption(new Option<T>(name, null, defaultValue, checker, type));
    }
 
    /**
     * add an option by name, shortname, checker
     * @param name  argument name
     * @param shortName argument shortname
     * @param checker   argument checker
     * @param type  argument type, only for type use, value is ignored
     * @return  this object, for chain call
     * @param <T>   Option type
     */
    public <T> Options addOption(String name, String shortName, Function<T, Boolean> checker, T type) {
        return this.addOption(new Option<T>(name, shortName, null, checker, type));
    }
 
    /**
     * add an option by name, shortname, checker
     * @param name  argument name
     * @param shortName argument shortname
     * @param defaultValue argument default value
     * @param checker   argument checker
     * @param type  argument type, only for type use, value is ignored
     * @return  this object, for chain call
     * @param <T>   Option type
     */
    public <T> Options addOption(String name, String shortName, Function<T, Boolean> checker, T type, T defaultValue) {
        return this.addOption(new Option<T>(name, shortName, defaultValue, checker, type));
    }
 
    /**
     *  get an option by its shortname
     * @param shortName argument shortname
     * @return  Option
     */
    public Option getOptionByShortName(String shortName) {
        return shortNameMap.get(shortName);
    }
 
    /**
     * get an option byte its long name
     * @param longName argument long name
     * @return  Option
     */
    public Option getOptionByLongName(String longName) {
        return longNameMap.get(longName);
    }
 
    /**
     * Argument Option class
     * @param <T> Option Type
     */
    public static class Option<T> {
        private String name;
        private String shortName;
        private T type;
        private Function<T, Boolean> checker;
        private T defaultValue;
        public Option(String name, String shortName, T defaultValue, Function<T, Boolean> checker, T type) {
            this.name = name;
            this.shortName = shortName;
            this.defaultValue = defaultValue;
            this.checker = checker;
            this.type = type;
        }
 
        public String getName() {
            return name;
        }
        public String getShortName() {
            return shortName;
        }
 
        public boolean check(T value) {
            return this.checker == null || checker.apply(value);
        }
 
        public T getType() {
            return type;
        }
 
        public T getDefaultValue() {
            return defaultValue;
        }
    }
}
 

Parser

用于实际解析的类

代码如下:

package top.evalexp.ami.cmd;
 
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.function.Function;
 
public class Parser {
    String[] args;
    Options options;
 
    // caster map
    private static Map<Class, Function<String, Object>> cast = new HashMap<>();
 
    /**
     * add a caster
     * @param type caster target type
     * @param caster caster object, a function
     */
    public static void addCaster(Class<?> type, Function<String, Object> caster) {
        cast.put(type, caster);
    }
 
    /**
     * Initializes a newly created Parser object without any pre-settings
     */
    public Parser() {}
 
    /**
     * Initializes a newly created Parser object
     * @param args program args
     */
    public Parser(String[] args) {
        this(args, null);
    }
 
    /**
     * Initializes a newly created Parser object
     * @param options commandline argument options
     */
    public Parser(Options options) {
        this(null, options);
    }
 
    /**
     * Initializes a newly created Parser object
     * @param args program args
     * @param options commandline argument options
     */
    public Parser(String[] args, Options options) {
        this.args = args;
        this.options = options;
    }
 
    /**
     * set the commandline argument options
     * @param options commandline argument options
     * @return this object, for chain call
     */
    public Parser setOptions(Options options) {
        this.options = options;
        return this;
    }
 
    /**
     * set the program arguments
     * @param args program args
     * @return this object, for chain call
     */
    public Parser setArgs(String[] args) {
        this.args = args;
        return this;
    }
 
    /**
     * parse the program argument
     * @return a newly Argument object
     */
    public Argument parse() {
        Argument argument = new Argument();
        // set default value
        this.options.forEach(option -> {
            if (option.getDefaultValue() != null) {
                if (!option.check(option.getDefaultValue())) {
                    System.err.printf("Warning: \"%s\" argument with default value \"%s\" is illegal%n", option.getName(), option.getDefaultValue());
                    return ;
                }
                argument.put(option.getName(), option.getDefaultValue());
            }
        });
        for (int i = 0; i < this.args.length; i++) {
            String arg = args[i];
            if (arg.startsWith("-")) {
                Options.Option option = null;
                if (arg.startsWith("--")) {
                    option = this.options.getOptionByLongName(arg.substring(2));
                } else if (arg.startsWith("-")) {
                    option = this.options.getOptionByShortName(arg.substring(1));
                }
                if (option == null) continue;
                String value = null;
                if (!(option.getType() instanceof Boolean)) {
                    value =  (i + 1 < args.length && !args[i + 1].startsWith("-")) ? args[++i] : null;
                } else {
                    argument.put(option.getName(), true);
                    continue;
                }
                this.putValue(argument, option, value);
            }
        }
 
        return argument;
    }
 
    private void putValue(Argument argument, Options.Option option, String value) {
        if (value != null && !value.isEmpty() && Parser.cast.containsKey(option.getType().getClass())) {
            Object castValue = Parser.cast.get(option.getType().getClass()).apply(value);
            if (!option.check(castValue)) {
                System.err.printf("Warning: \"%s\" argument with value \"%s\" is illegal%n", option.getName(), castValue);
                return ;
            }
            argument.put(option.getName(), castValue);
        }
    }
}
 

Argument

用于简化参数操作的最终参数解析结果类

代码如下:

package top.evalexp.ami.cmd;
 
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
 
public class Argument {
    private final Map<String, Object> arguments = new HashMap<>();
    private final List<Binding<?>> bindings = new ArrayList<>();
 
    /**
     * Binding bind argument value
     * @param <T> Value type
     */
    public static class Binding<T> {
        private T value;
        private String name;
 
        public Binding(String name) {
            this.name = name;
        }
 
        public T get() {
            return value;
        }
    }
 
    /**
     * BindingContext the bind variable context
     */
    public static class BindingContext {
        private final List<Binding<?>> bindings = new ArrayList<>();
        private Argument parent;
        private BindingContext(Argument parent) {
            this.parent = parent;
        }
 
        /**
         * set binding
         * @param binding binding
         * @return context, chain call
         */
        public BindingContext withBinding(Binding<?> binding) {
            bindings.add(binding);
            return this;
        }
 
        /**
         * set all argument to binding value
         */
        public void bind() {
            for (Binding binding : bindings) {
                binding.value = parent.get(binding.name);
            }
        }
    }
 
    /**
     * get an argument value
     * @param key argument name
     * @return argument value
     */
    public Object get(String key) {
        return arguments.getOrDefault(key, null);
    }
 
    /**
     * put an argument value
     * @param key argument name
     * @param value argument value
     */
    protected void put(String key, Object value) {
        arguments.put(key, value);
    }
 
    public BindingContext newBindingContext() {
        return new BindingContext(this);
    }
 
    /**
     * Stringify the object, contains all argument value
     * @return String
     */
    @Override
    public String toString() {
        StringBuilder builder = new StringBuilder();
        builder.append("Argument{");
        for (Map.Entry<String, Object> entry : arguments.entrySet()) {
            builder.append('"').append(entry.getKey()).append('"').append(": ");
            Object value = entry.getValue();
            if (value instanceof String) {
                builder.append("\"").append(value).append("\", ");
            } else {
                builder.append(value).append(", ");
            }
        }
        if (builder.length() > 9) {
            builder.delete(builder.length() - 2, builder.length());
        }
        builder.append("}");
        return builder.toString();
    }
}