001/*
002 * Licensed to the Apache Software Foundation (ASF) under one
003 * or more contributor license agreements.  See the NOTICE file
004 * distributed with this work for additional information
005 * regarding copyright ownership.  The ASF licenses this file
006 * to you under the Apache License, Version 2.0 (the
007 * "License"); you may not use this file except in compliance
008 * with the License.  You may obtain a copy of the License at
009 *
010 *     http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing, software
013 * distributed under the License is distributed on an "AS IS" BASIS,
014 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
015 * See the License for the specific language governing permissions and
016 * limitations under the License.
017 */
018package org.apache.hadoop.hbase.util;
019
020import java.io.IOException;
021import java.util.ArrayList;
022import java.util.Collections;
023import java.util.Comparator;
024import java.util.HashMap;
025import java.util.List;
026import java.util.Objects;
027import org.apache.hadoop.conf.Configuration;
028import org.apache.hadoop.hbase.HBaseConfiguration;
029import org.apache.hadoop.util.Tool;
030import org.apache.hadoop.util.ToolRunner;
031import org.apache.yetus.audience.InterfaceAudience;
032import org.slf4j.Logger;
033import org.slf4j.LoggerFactory;
034
035import org.apache.hbase.thirdparty.org.apache.commons.cli.BasicParser;
036import org.apache.hbase.thirdparty.org.apache.commons.cli.CommandLine;
037import org.apache.hbase.thirdparty.org.apache.commons.cli.CommandLineParser;
038import org.apache.hbase.thirdparty.org.apache.commons.cli.DefaultParser;
039import org.apache.hbase.thirdparty.org.apache.commons.cli.HelpFormatter;
040import org.apache.hbase.thirdparty.org.apache.commons.cli.MissingOptionException;
041import org.apache.hbase.thirdparty.org.apache.commons.cli.Option;
042import org.apache.hbase.thirdparty.org.apache.commons.cli.Options;
043import org.apache.hbase.thirdparty.org.apache.commons.cli.ParseException;
044
045/**
046 * Common base class used for HBase command-line tools. Simplifies workflow and command-line
047 * argument parsing.
048 */
049@InterfaceAudience.Private
050public abstract class AbstractHBaseTool implements Tool {
051  public static final int EXIT_SUCCESS = 0;
052  public static final int EXIT_FAILURE = 1;
053
054  public static final String SHORT_HELP_OPTION = "h";
055  public static final String LONG_HELP_OPTION = "help";
056
057  private static final Option HELP_OPTION =
058    new Option("h", "help", false, "Prints help for this tool.");
059
060  private static final Logger LOG = LoggerFactory.getLogger(AbstractHBaseTool.class);
061
062  protected final Options options = new Options();
063
064  protected Configuration conf = null;
065
066  protected String[] cmdLineArgs = null;
067
068  // To print options in order they were added in help text.
069  private HashMap<Option, Integer> optionsOrder = new HashMap<>();
070  private int optionsCount = 0;
071
072  public class OptionsOrderComparator implements Comparator<Option> {
073    @Override
074    public int compare(Option o1, Option o2) {
075      return optionsOrder.get(o1) - optionsOrder.get(o2);
076    }
077  }
078
079  /**
080   * Override this to add command-line options using {@link #addOptWithArg} and similar methods.
081   */
082  protected abstract void addOptions();
083
084  /**
085   * This method is called to process the options after they have been parsed.
086   */
087  protected abstract void processOptions(CommandLine cmd);
088
089  /** The "main function" of the tool */
090  protected abstract int doWork() throws Exception;
091
092  /**
093   * For backward compatibility. DO NOT use it for new tools. We have options in existing tools
094   * which can't be ported to Apache CLI's {@link Option}. (because they don't pass validation, for
095   * e.g. "-copy-to". "-" means short name which doesn't allow '-' in name). This function is to
096   * allow tools to have, for time being, parameters which can't be parsed using {@link Option}.
097   * Overrides should consume all valid legacy arguments. If the param 'args' is not empty on
098   * return, it means there were invalid options, in which case we'll exit from the tool. Note that
099   * it's called before {@link #processOptions(CommandLine)}, which means new options' values will
100   * override old ones'.
101   */
102  protected void processOldArgs(List<String> args) {
103  }
104
105  @Override
106  public Configuration getConf() {
107    return conf;
108  }
109
110  @Override
111  public void setConf(Configuration conf) {
112    this.conf = conf;
113  }
114
115  @Override
116  public int run(String[] args) throws IOException {
117    cmdLineArgs = args;
118
119    Objects.requireNonNull(conf, "Tool configuration is not initialized");
120
121    CommandLine cmd;
122    List<String> argsList = new ArrayList<>(args.length);
123    Collections.addAll(argsList, args);
124    // For backward compatibility of args which can't be parsed as Option. See javadoc for
125    // processOldArgs(..)
126    processOldArgs(argsList);
127    try {
128      addOptions();
129      if (isHelpCommand(args)) {
130        printUsage();
131        return EXIT_SUCCESS;
132      }
133      String[] remainingArgs = new String[argsList.size()];
134      argsList.toArray(remainingArgs);
135      cmd = newParser().parse(options, remainingArgs);
136    } catch (MissingOptionException e) {
137      LOG.error(e.getMessage());
138      LOG.error("Use -h or --help for usage instructions.");
139      return EXIT_FAILURE;
140    } catch (ParseException e) {
141      LOG.error("Error when parsing command-line arguments", e);
142      LOG.error("Use -h or --help for usage instructions.");
143      return EXIT_FAILURE;
144    }
145
146    processOptions(cmd);
147
148    int ret;
149    try {
150      ret = doWork();
151    } catch (Exception e) {
152      LOG.error("Error running command-line tool", e);
153      return EXIT_FAILURE;
154    }
155    return ret;
156  }
157
158  /**
159   * Create the parser to use for parsing and validating the command line. Since commons-cli lacks
160   * the capability to validate arbitrary combination of options, it may be helpful to bake custom
161   * logic into a specialized parser implementation. See LoadTestTool for examples.
162   * @return a new parser specific to the current tool
163   */
164  protected CommandLineParser newParser() {
165    return new DefaultParser();
166  }
167
168  private boolean isHelpCommand(String[] args) throws ParseException {
169    Options helpOption = new Options().addOption(HELP_OPTION);
170    // this parses the command line but doesn't throw an exception on unknown options
171    CommandLine cl = new DefaultParser().parse(helpOption, args, true);
172    return cl.getOptions().length != 0;
173  }
174
175  protected CommandLine parseArgs(String[] args) throws ParseException {
176    options.addOption(SHORT_HELP_OPTION, LONG_HELP_OPTION, false, "Show usage");
177    addOptions();
178    CommandLineParser parser = new BasicParser();
179    return parser.parse(options, args);
180  }
181
182  protected void printUsage() {
183    printUsage("hbase " + getClass().getName() + " <options>", "Options:", "");
184  }
185
186  protected void printUsage(final String usageStr, final String usageHeader,
187    final String usageFooter) {
188    HelpFormatter helpFormatter = new HelpFormatter();
189    helpFormatter.setWidth(120);
190    helpFormatter.setOptionComparator(new OptionsOrderComparator());
191    helpFormatter.printHelp(usageStr, usageHeader, options, usageFooter);
192  }
193
194  protected void addOption(Option option) {
195    options.addOption(option);
196    optionsOrder.put(option, optionsCount++);
197  }
198
199  protected void addRequiredOption(Option option) {
200    option.setRequired(true);
201    addOption(option);
202  }
203
204  protected void addRequiredOptWithArg(String opt, String description) {
205    Option option = new Option(opt, true, description);
206    option.setRequired(true);
207    addOption(option);
208  }
209
210  protected void addRequiredOptWithArg(String shortOpt, String longOpt, String description) {
211    Option option = new Option(shortOpt, longOpt, true, description);
212    option.setRequired(true);
213    addOption(option);
214  }
215
216  protected void addOptNoArg(String opt, String description) {
217    addOption(new Option(opt, false, description));
218  }
219
220  protected void addOptNoArg(String shortOpt, String longOpt, String description) {
221    addOption(new Option(shortOpt, longOpt, false, description));
222  }
223
224  protected void addOptWithArg(String opt, String description) {
225    addOption(new Option(opt, true, description));
226  }
227
228  protected void addOptWithArg(String shortOpt, String longOpt, String description) {
229    addOption(new Option(shortOpt, longOpt, true, description));
230  }
231
232  public int getOptionAsInt(CommandLine cmd, String opt, int defaultValue) {
233    return getOptionAsInt(cmd, opt, defaultValue, 10);
234  }
235
236  public int getOptionAsInt(CommandLine cmd, String opt, int defaultValue, int radix) {
237    if (cmd.hasOption(opt)) {
238      return Integer.parseInt(cmd.getOptionValue(opt), radix);
239    } else {
240      return defaultValue;
241    }
242  }
243
244  public long getOptionAsLong(CommandLine cmd, String opt, int defaultValue) {
245    return getOptionAsLong(cmd, opt, defaultValue, 10);
246  }
247
248  public long getOptionAsLong(CommandLine cmd, String opt, int defaultValue, int radix) {
249    if (cmd.hasOption(opt)) {
250      return Long.parseLong(cmd.getOptionValue(opt), radix);
251    } else {
252      return defaultValue;
253    }
254  }
255
256  public double getOptionAsDouble(CommandLine cmd, String opt, double defaultValue) {
257    if (cmd.hasOption(opt)) {
258      return Double.parseDouble(cmd.getOptionValue(opt));
259    } else {
260      return defaultValue;
261    }
262  }
263
264  /**
265   * Parse a number and enforce a range.
266   */
267  public static long parseLong(String s, long minValue, long maxValue) {
268    long l = Long.parseLong(s);
269    if (l < minValue || l > maxValue) {
270      throw new IllegalArgumentException(
271        "The value " + l + " is out of range [" + minValue + ", " + maxValue + "]");
272    }
273    return l;
274  }
275
276  public static int parseInt(String s, int minValue, int maxValue) {
277    return (int) parseLong(s, minValue, maxValue);
278  }
279
280  /** Call this from the concrete tool class's main function. */
281  protected void doStaticMain(String args[]) {
282    int ret;
283    try {
284      ret = ToolRunner.run(HBaseConfiguration.create(), this, args);
285    } catch (Exception ex) {
286      LOG.error("Error running command-line tool", ex);
287      ret = EXIT_FAILURE;
288    }
289    System.exit(ret);
290  }
291
292}