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 */ 018 019package org.apache.oozie.cli; 020 021import org.apache.commons.cli.MissingOptionException; 022import org.apache.commons.cli.Options; 023import org.apache.commons.cli.GnuParser; 024import org.apache.commons.cli.ParseException; 025import org.apache.commons.cli.CommandLine; 026import org.apache.commons.cli.HelpFormatter; 027 028import java.io.OutputStreamWriter; 029import java.io.Writer; 030import java.nio.charset.StandardCharsets; 031import java.util.Arrays; 032import java.util.Map; 033import java.util.LinkedHashMap; 034import java.text.MessageFormat; 035import java.io.PrintWriter; 036import java.util.HashSet; 037import java.util.Set; 038 039/** 040 * Command line parser based on Apache common-cli 1.x that supports subcommands. 041 */ 042public class CLIParser { 043 private static final String LEFT_PADDING = " "; 044 045 private String cliName; 046 private String[] cliHelp; 047 private Map<String, Options> commands = new LinkedHashMap<>(); 048 private Map<String, Boolean> commandWithArgs = new LinkedHashMap<>(); 049 private Map<String, String> commandsHelp = new LinkedHashMap<>(); 050 051 /** 052 * Create a parser. 053 * 054 * @param cliName name of the parser, for help purposes. 055 * @param cliHelp help for the CLI. 056 */ 057 public CLIParser(String cliName, String[] cliHelp) { 058 this.cliName = cliName; 059 this.cliHelp = cliHelp; 060 } 061 062 /** 063 * Add a command to the parser. 064 * 065 * @param command comand name. 066 * @param argsHelp command arguments help. 067 * @param commandHelp command description. 068 * @param commandOptions command options. 069 * @param hasArguments true if this command has arguments 070 */ 071 public void addCommand(String command, String argsHelp, String commandHelp, Options commandOptions, 072 boolean hasArguments) { 073 String helpMsg = argsHelp + ((hasArguments) ? "<ARGS> " : "") + ": " + commandHelp; 074 commandsHelp.put(command, helpMsg); 075 commands.put(command, commandOptions); 076 commandWithArgs.put(command, hasArguments); 077 } 078 079 /** 080 * Bean that represents a parsed command. 081 */ 082 public class Command { 083 private String name; 084 private CommandLine commandLine; 085 086 private Command(String name, CommandLine commandLine) { 087 this.name = name; 088 this.commandLine = commandLine; 089 } 090 091 /** 092 * Return the command name. 093 * 094 * @return the command name. 095 */ 096 public String getName() { 097 return name; 098 } 099 100 /** 101 * Return the command line. 102 * 103 * @return the command line. 104 */ 105 public CommandLine getCommandLine() { 106 return commandLine; 107 } 108 } 109 110 /** 111 * Parse a array of arguments into a command. 112 * 113 * @param args array of arguments. 114 * @return the parsed Command. 115 * @throws ParseException thrown if the arguments could not be parsed. 116 */ 117 public Command parse(String[] args) throws ParseException { 118 if (args.length == 0) { 119 throw new ParseException("missing sub-command"); 120 } 121 else { 122 if (commands.containsKey(args[0])) { 123 GnuParser parser ; 124 String[] minusCommand = new String[args.length - 1]; 125 System.arraycopy(args, 1, minusCommand, 0, minusCommand.length); 126 127 if (args[0].equals(OozieCLI.JOB_CMD)) { 128 validdateArgs(args, minusCommand); 129 parser = new OozieGnuParser(true); 130 } 131 else { 132 parser = new OozieGnuParser(false); 133 } 134 135 return new Command(args[0], parser.parse(commands.get(args[0]), minusCommand, 136 commandWithArgs.get(args[0]))); 137 } 138 else { 139 throw new ParseException(MessageFormat.format("invalid sub-command [{0}]", args[0])); 140 } 141 } 142 } 143 144 public void validdateArgs(final String[] args, String[] minusCommand) throws ParseException { 145 try { 146 GnuParser parser = new OozieGnuParser(false); 147 parser.parse(commands.get(args[0]), minusCommand, commandWithArgs.get(args[0])); 148 } 149 catch (MissingOptionException e) { 150 if (Arrays.toString(args).contains("-dryrun")) { 151 // ignore this, else throw exception 152 //Dryrun is also part of update sub-command. CLI parses dryrun as sub-command and throws 153 //Missing Option Exception, if -dryrun is used as command. It's ok to skip exception only for dryrun. 154 } 155 else { 156 throw e; 157 } 158 } 159 } 160 161 public String shortHelp() { 162 return "use 'help [sub-command]' for help details"; 163 } 164 165 /** 166 * Print the help for the parser to standard output. 167 * @param commandLine the command line 168 */ 169 public void showHelp(CommandLine commandLine) { 170 Writer writer = new OutputStreamWriter(System.out, StandardCharsets.UTF_8); 171 PrintWriter pw = new PrintWriter(writer); 172 pw.println("usage: "); 173 for (String s : cliHelp) { 174 pw.println(LEFT_PADDING + s); 175 } 176 pw.println(); 177 HelpFormatter formatter = new HelpFormatter(); 178 Set<String> commandsToPrint = commands.keySet(); 179 String[] args = commandLine.getArgs(); 180 if (args.length > 0 && commandsToPrint.contains(args[0])) { 181 commandsToPrint = new HashSet<>(); 182 commandsToPrint.add(args[0]); 183 } 184 for (String comm : commandsToPrint) { 185 Options opts = commands.get(comm); 186 String s = LEFT_PADDING + cliName + " " + comm + " "; 187 if (opts.getOptions().size() > 0) { 188 pw.println(s + "<OPTIONS> " + commandsHelp.get(comm)); 189 formatter.printOptions(pw, 100, opts, s.length(), 3); 190 } 191 else { 192 pw.println(s + commandsHelp.get(comm)); 193 } 194 pw.println(); 195 } 196 pw.flush(); 197 } 198 199 static class OozieGnuParser extends GnuParser { 200 private boolean ignoreMissingOption; 201 202 public OozieGnuParser(final boolean ignoreMissingOption) { 203 this.ignoreMissingOption = ignoreMissingOption; 204 } 205 206 @Override 207 protected void checkRequiredOptions() throws MissingOptionException { 208 if (!ignoreMissingOption) { 209 super.checkRequiredOptions(); 210 } 211 } 212 } 213 214}