All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.wl4g.infra.common.cli.CommandLineTool Maven / Gradle / Ivy

The newest version!
/*
 * Copyright 2017 ~ 2025 the original author or authors. James Wong 
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.wl4g.infra.common.cli;

import static com.wl4g.infra.common.collection.CollectionUtils2.safeArrayToList;
import static com.wl4g.infra.common.lang.Assert2.hasTextOf;
import static com.wl4g.infra.common.lang.Assert2.isTrueOf;
import static com.wl4g.infra.common.lang.Assert2.notNull;
import static com.wl4g.infra.common.lang.Exceptions.getStackTraceAsString;
import static com.wl4g.infra.common.log.SmartLoggerFactory.getLogger;
import static com.wl4g.infra.common.reflect.ReflectionUtils2.findField;
import static com.wl4g.infra.common.reflect.ReflectionUtils2.findMethod;
import static com.wl4g.infra.common.reflect.ReflectionUtils2.getField;
import static com.wl4g.infra.common.reflect.ReflectionUtils2.invokeMethod;
import static com.wl4g.infra.common.reflect.ReflectionUtils2.makeAccessible;
import static java.lang.String.format;
import static java.util.Objects.isNull;
import static java.util.Objects.nonNull;
import static java.util.stream.Collectors.toList;
import static org.apache.commons.lang3.StringUtils.equalsAnyIgnoreCase;
import static org.apache.commons.lang3.StringUtils.isBlank;
import static org.apache.commons.lang3.StringUtils.trimToEmpty;

import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Properties;

import javax.annotation.Nullable;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;

import org.apache.commons.cli.CommandLine;
import org.apache.commons.cli.DefaultParser;
import org.apache.commons.cli.HelpFormatter;
import org.apache.commons.cli.Option;
import org.apache.commons.cli.Options;
import org.apache.commons.cli.ParseException;
import org.slf4j.Logger;

import lombok.AllArgsConstructor;
import lombok.Getter;

/**
 * Command utility.
 * 
 * @author James Wong James Wong 
 * @version 2019年12月29日 v1.0.0
 * @see
 */
public class CommandLineTool {

    /**
     * New create builder. {@link Builder}
     * 
     * @return
     */
    public final static Builder builder() {
        return new Builder();
    }

    /**
     * Command line builder tool.
     * 
     * @author Wangl.sir
     * @version v1.0.0 2019-09-08
     * @since
     */
    public final static class Builder {
        protected final Logger log = getLogger(getClass());

        private int width = 120;
        private final RemovableOptions options = new RemovableOptions();

        /**
         * ADD command option with not default value(ie:required).
         * 
         * @param longOpt
         *            Long option.
         * @param help
         * @return
         */
        public Builder mustLongOption(@NotBlank String longOpt, @Nullable String help) {
            return mustOption(null, longOpt, help);
        }

        /**
         * ADD command option with not default value(ie:required).
         * 
         * @param shortOpt
         *            Short option.
         * @param longOpt
         *            Long option.
         * @param help
         * @return
         */
        public Builder mustOption(@Nullable String shortOpt, @NotBlank String longOpt, @Nullable String help) {
            options.addOption(new HelpOption(shortOpt, longOpt, null, true, help));
            return this;
        }

        /**
         * ADD command option with default value(ie:not required).
         * 
         * @param longOpt
         *            Long option.
         * @param defaultValue
         *            Null means there is no default value, that is, the
         *            parameter is required
         * @param help
         * @return
         */
        public Builder longOption(@NotBlank String longOpt, @Nullable String defaultValue, @Nullable String help) {
            return option(null, longOpt, defaultValue, help);
        }

        /**
         * ADD command option with default value(ie:not required).
         * 
         * @param shortOpt
         *            Short option.
         * @param longOpt
         *            Long option.
         * @param defaultValue
         *            Null means there is no default value, that is, the
         *            parameter is required
         * @param help
         * @return
         */
        public Builder option(
                @Nullable String shortOpt,
                @NotBlank String longOpt,
                @Nullable String defaultValue,
                @Nullable String help) {
            options.addOption(new HelpOption(shortOpt, longOpt, defaultValue, false, help));
            return this;
        }

        /**
         * Remove option from options.
         * 
         * @param opt
         * @param longOpt
         * @return
         */
        public Builder removeOption(@Nullable String shortOpt, @NotBlank String longOpt) {
            notNull(options, "Options did not initialize creation");
            Option option = new Option(shortOpt, hasTextOf(longOpt, "longOpt"), true, "");
            options.removeOption(option);
            return this;
        }

        public Builder helpIfEmpty(String[] args, boolean exit) {
            if (isNull(args) || args.length == 0) {
                help("", "", exit);
            }
            return this;
        }

        public Builder helpIfEmpty(String[] args) {
            return helpIfEmpty(args, true);
        }

        public Builder width(@Min(0) int width) {
            isTrueOf(width >= 0, "width >= 0");
            this.width = width;
            return this;
        }

        public void help(String header, String footer, boolean exit) {
            final HelpFormatter formatter = new HelpFormatter();
            formatter.setSyntaxPrefix("\nUsage: [OPTIONS] ...\n");
            formatter.printHelp(width, "\n", header, options, footer);
            if (exit) {
                System.exit(1);
            }
        }

        public CommandLineFacade build(String args[]) {
            // If there is only arguments 'help,--help' then print usage and
            // exit.
            if (checkForHelp(args)) {
                help("", "", true);
                return null;
            }

            final boolean isDebug = nonNull(System.getProperty("debug"));
            try {
                // Parsing to command line.
                Properties props = new Properties();
                options.getOptions().forEach(opt -> props.setProperty(opt.getLongOpt(), trimToEmpty(opt.getValue())));
                CommandLine line = new DefaultParser().parse(options, args, props);

                // Debug arguments pre-parse logs.
                if (isDebug) {
                    List printArgs = safeArrayToList(line.getOptions()).stream().map(o -> {
                        String value = o.getValue();
                        value = isBlank(value) ? ((HelpOption) o).getDefaultValue() : value;
                        return "-".concat(o.getOpt()).concat(",--").concat(o.getLongOpt()).concat("=").concat(trimToEmpty(value));
                    }).collect(toList());
                    System.out.printf("%s prepare parse: %s\n\n", new Date().toString(), printArgs);
                }

                return new CommandLineFacade(line, this);
            } catch (ParseException e) {
                help("", (isDebug ? getStackTraceAsString(e) : e.getLocalizedMessage()), true);
            }

            return null;
        }

        private boolean checkForHelp(String args[]) {
            return isNull(args) || (args.length == 1 && equalsAnyIgnoreCase(args[0], "help", "--help"));
        }
    }

    @Getter
    public static class HelpOption extends Option {
        private static final long serialVersionUID = 1950613325131445963L;
        private final String defaultValue;

        public HelpOption(@Nullable String shortOpt, @NotBlank String longOpt, @Nullable String defaultValue, boolean required,
                @Nullable String help) throws IllegalArgumentException {
            super(shortOpt, hasTextOf(longOpt, "longOpt"), true, help);
            // isTrue(shortOpt.length()==1,format("Bad short option: '%s' (%s),
            // non
            // GNU specification, name length must be 1", shortOpt, help));
            this.defaultValue = defaultValue;
            setRequired(required);
            if (!isRequired()) {
                setArgName("default=" + defaultValue);
            } else {
                setArgName("required");
            }
        }
    }

    public static class RemovableOptions extends Options {
        private static final long serialVersionUID = -3292319664089354481L;

        /**
         * Remove an option instance
         *
         * @param option
         *            the option that is to be added
         * @return the resulting Options instance
         */
        public RemovableOptions removeOption(@Nullable Option option) {
            if (!isNull(option)) {
                getShortOpts().remove(option.getOpt());
                getLongOpts().remove(option.getLongOpt());
                getRequiredOpts().remove(option.getOpt());
            }
            return this;
        }

        @SuppressWarnings("unchecked")
        final private Map getShortOpts() {
            Field field = findField(Options.class, "shortOpts");
            return (Map) getField(field, this);
        }

        @SuppressWarnings("unchecked")
        final private Map getLongOpts() {
            Field field = findField(Options.class, "longOpts");
            return (Map) getField(field, this);
        }

        @SuppressWarnings("unchecked")
        final private Map getRequiredOpts() {
            Field field = findField(Options.class, "requiredOpts");
            return (Map) getField(field, this);
        }
    }

    @AllArgsConstructor
    public static class CommandLineFacade {
        private final CommandLine line;
        private final Builder builder;

        public String get(@NotBlank String opt) throws ParseException {
            return getString(opt);
        }

        public String getString(@NotBlank String opt) throws ParseException {
            return getCheckOptionValue(opt);
        }

        public Long getLong(@NotBlank String opt) throws ParseException {
            String value = getCheckOptionValue(opt);
            return isBlank(value) ? null : Long.parseLong(value);
        }

        public Integer getInteger(@NotBlank String opt) throws ParseException {
            String value = getCheckOptionValue(opt);
            return isBlank(value) ? null : Integer.parseInt(value);
        }

        public Float getFloat(@NotBlank String opt) throws ParseException {
            String value = getCheckOptionValue(opt);
            return isBlank(value) ? null : Float.parseFloat(value);
        }

        public Double getDouble(@NotBlank String opt) throws ParseException {
            String value = getCheckOptionValue(opt);
            return isBlank(value) ? null : Double.parseDouble(value);
        }

        public Boolean getBoolean(@NotBlank String opt) throws ParseException {
            String value = getCheckOptionValue(opt);
            return isBlank(value) ? null : Boolean.parseBoolean(value);
        }

        public > E getEnum(@NotBlank String opt, @NotNull Class enumClass) throws ParseException {
            String value = getCheckOptionValue(opt);
            return isBlank(value) ? null
                    : safeArrayToList(enumClass.getEnumConstants()).stream()
                            .filter(e -> equalsAnyIgnoreCase(value, e.name()))
                            .findFirst()
                            .orElse(null);
        }

        private String getCheckOptionValue(String opt) throws ParseException {
            hasTextOf(opt, "opt");

            // Check for use opt invalid?
            if (!safeArrayToList(line.getOptions()).stream()
                    .anyMatch(o -> equalsAnyIgnoreCase(opt, o.getOpt(), o.getLongOpt()))) {
                throw new ParseException(format("\nUsing undeclared options: %s\n", opt));
            }

            // Gets argument values from line and default value in turn.
            String value = line.getOptionValue(opt);
            if (isBlank(value)) {
                makeAccessible(resolveOptionMethod);
                HelpOption option = (HelpOption) invokeMethod(resolveOptionMethod, line, opt);
                if (nonNull(option)) {
                    value = option.getDefaultValue();
                    if (option.isRequired() && isNull(value)) {
                        String errmsg = format("\nBad command option: '-%s,--%s' is missing. Please use: help,--help\n",
                                option.getOpt(), option.getLongOpt());
                        builder.help("", errmsg, true);
                    }
                }
            }
            return value;
        }
    }

    private static final Method resolveOptionMethod = findMethod(CommandLine.class, "resolveOption", String.class);

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy