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