Signal early command termination due '-h' or '--help' option via TerminatedByHelpException. This allows tests using CLIGitCommand differentiate between unexpected command parsing errors and expected command cancellation "on help" (which also allows validation of expected/unexpected help messages). Additional side-effect: jgit supports now git style of handling help option: any unexpected command line options before help are reported as errors, but after help ignored. Bug: 484951 Change-Id: If45c41c0d32895ab6822a7ff9d851877dcef5771 Signed-off-by: Andrey Loskutov <loskutov@gmx.de>tags/v4.2.0.201601211800-r
import org.eclipse.jgit.internal.storage.file.FileRepository; | import org.eclipse.jgit.internal.storage.file.FileRepository; | ||||
import org.eclipse.jgit.lib.Repository; | import org.eclipse.jgit.lib.Repository; | ||||
import org.eclipse.jgit.pgm.TextBuiltin.TerminatedByHelpException; | |||||
import org.eclipse.jgit.pgm.internal.CLIText; | import org.eclipse.jgit.pgm.internal.CLIText; | ||||
import org.eclipse.jgit.pgm.opt.CmdLineParser; | import org.eclipse.jgit.pgm.opt.CmdLineParser; | ||||
import org.eclipse.jgit.pgm.opt.SubcommandHandler; | import org.eclipse.jgit.pgm.opt.SubcommandHandler; | ||||
System.arraycopy(args, 1, argv, 0, args.length - 1); | System.arraycopy(args, 1, argv, 0, args.length - 1); | ||||
CLIGitCommand bean = new CLIGitCommand(); | CLIGitCommand bean = new CLIGitCommand(); | ||||
final CmdLineParser clp = new CmdLineParser(bean); | |||||
final CmdLineParser clp = new TestCmdLineParser(bean); | |||||
clp.parseArgument(argv); | clp.parseArgument(argv); | ||||
final TextBuiltin cmd = bean.getSubcommand(); | final TextBuiltin cmd = bean.getSubcommand(); | ||||
ByteArrayOutputStream baos = new ByteArrayOutputStream(); | ByteArrayOutputStream baos = new ByteArrayOutputStream(); | ||||
cmd.outs = baos; | cmd.outs = baos; | ||||
ByteArrayOutputStream errs = new ByteArrayOutputStream(); | |||||
cmd.errs = errs; | |||||
boolean seenHelp = TextBuiltin.containsHelp(argv); | |||||
if (cmd.requiresRepository()) | if (cmd.requiresRepository()) | ||||
cmd.init(db, null); | cmd.init(db, null); | ||||
else | else | ||||
try { | try { | ||||
cmd.execute(bean.getArguments().toArray( | cmd.execute(bean.getArguments().toArray( | ||||
new String[bean.getArguments().size()])); | new String[bean.getArguments().size()])); | ||||
} catch (TerminatedByHelpException e) { | |||||
seenHelp = true; | |||||
// this is not a failure, command execution should just not happen | |||||
} finally { | } finally { | ||||
if (cmd.outw != null) | |||||
if (cmd.outw != null) { | |||||
cmd.outw.flush(); | cmd.outw.flush(); | ||||
} | |||||
if (cmd.errw != null) { | |||||
cmd.errw.flush(); | |||||
} | |||||
if (seenHelp) { | |||||
return errs.toByteArray(); | |||||
} else if (errs.size() > 0) { | |||||
// forward the errors to the standard err | |||||
System.err.print(errs.toString()); | |||||
} | |||||
} | } | ||||
return baos.toByteArray(); | return baos.toByteArray(); | ||||
} | } | ||||
return list.toArray(new String[list.size()]); | return list.toArray(new String[list.size()]); | ||||
} | } | ||||
static class TestCmdLineParser extends CmdLineParser { | |||||
public TestCmdLineParser(Object bean) { | |||||
super(bean); | |||||
} | |||||
@Override | |||||
protected boolean containsHelp(String... args) { | |||||
return false; | |||||
} | |||||
} | |||||
} | } |
package org.eclipse.jgit.pgm; | package org.eclipse.jgit.pgm; | ||||
import static org.junit.Assert.assertArrayEquals; | import static org.junit.Assert.assertArrayEquals; | ||||
import static org.junit.Assert.assertFalse; | |||||
import static org.junit.Assert.assertTrue; | |||||
import java.util.Arrays; | |||||
import org.eclipse.jgit.api.Git; | import org.eclipse.jgit.api.Git; | ||||
import org.eclipse.jgit.lib.CLIRepositoryTestCase; | import org.eclipse.jgit.lib.CLIRepositoryTestCase; | ||||
assertArrayEquals(new String[] { "v1.0-0-g6fd41be", "" }, | assertArrayEquals(new String[] { "v1.0-0-g6fd41be", "" }, | ||||
execute("git describe --long HEAD")); | execute("git describe --long HEAD")); | ||||
} | } | ||||
@Test | |||||
public void testHelpArgumentBeforeUnknown() throws Exception { | |||||
String[] output = execute("git describe -h -XYZ"); | |||||
String all = Arrays.toString(output); | |||||
assertTrue("Unexpected help output: " + all, | |||||
all.contains("jgit describe")); | |||||
assertFalse("Unexpected help output: " + all, all.contains("fatal")); | |||||
} | |||||
@Test | |||||
public void testHelpArgumentAfterUnknown() throws Exception { | |||||
String[] output = execute("git describe -XYZ -h"); | |||||
String all = Arrays.toString(output); | |||||
assertTrue("Unexpected help output: " + all, | |||||
all.contains("jgit describe")); | |||||
assertTrue("Unexpected help output: " + all, all.contains("fatal")); | |||||
} | |||||
} | } |
} | } | ||||
private void execute(final String[] argv) throws Exception { | private void execute(final String[] argv) throws Exception { | ||||
final CmdLineParser clp = new CmdLineParser(this); | |||||
final CmdLineParser clp = new SubcommandLineParser(this); | |||||
PrintWriter writer = new PrintWriter(System.err); | PrintWriter writer = new PrintWriter(System.err); | ||||
try { | try { | ||||
clp.parseArgument(argv); | clp.parseArgument(argv); | ||||
} | } | ||||
} | } | ||||
} | } | ||||
/** | |||||
* Parser for subcommands which doesn't stop parsing on help options and so | |||||
* proceeds all specified options | |||||
*/ | |||||
static class SubcommandLineParser extends CmdLineParser { | |||||
public SubcommandLineParser(Object bean) { | |||||
super(bean); | |||||
} | |||||
@Override | |||||
protected boolean containsHelp(String... args) { | |||||
return false; | |||||
} | |||||
} | |||||
} | } |
} | } | ||||
@Override | @Override | ||||
public void printUsageAndExit(final String message, final CmdLineParser clp) | |||||
public void printUsage(final String message, final CmdLineParser clp) | |||||
throws IOException { | throws IOException { | ||||
errw.println(message); | errw.println(message); | ||||
errw.println("jgit remote [--verbose (-v)] [--help (-h)]"); //$NON-NLS-1$ | errw.println("jgit remote [--verbose (-v)] [--help (-h)]"); //$NON-NLS-1$ | ||||
errw.println(); | errw.println(); | ||||
errw.flush(); | errw.flush(); | ||||
throw die(true); | |||||
} | } | ||||
private void print(List<RemoteConfig> remotes) throws IOException { | private void print(List<RemoteConfig> remotes) throws IOException { |
*/ | */ | ||||
protected void parseArguments(final String[] args) throws IOException { | protected void parseArguments(final String[] args) throws IOException { | ||||
final CmdLineParser clp = new CmdLineParser(this); | final CmdLineParser clp = new CmdLineParser(this); | ||||
help = containsHelp(args); | |||||
try { | try { | ||||
clp.parseArgument(args); | clp.parseArgument(args); | ||||
} catch (CmdLineException err) { | } catch (CmdLineException err) { | ||||
if (!help) { | |||||
this.errw.println(MessageFormat.format(CLIText.get().fatalError, err.getMessage())); | |||||
throw die(true, err); | |||||
this.errw.println(MessageFormat.format(CLIText.get().fatalError, err.getMessage())); | |||||
if (help) { | |||||
printUsage("", clp); //$NON-NLS-1$ | |||||
} | } | ||||
throw die(true, err); | |||||
} | } | ||||
if (help) { | if (help) { | ||||
printUsageAndExit(clp); | |||||
printUsage("", clp); //$NON-NLS-1$ | |||||
throw new TerminatedByHelpException(); | |||||
} | } | ||||
argWalk = clp.getRevWalkGently(); | argWalk = clp.getRevWalkGently(); | ||||
* @throws IOException | * @throws IOException | ||||
*/ | */ | ||||
public void printUsageAndExit(final String message, final CmdLineParser clp) throws IOException { | public void printUsageAndExit(final String message, final CmdLineParser clp) throws IOException { | ||||
printUsage(message, clp); | |||||
throw die(true); | |||||
} | |||||
/** | |||||
* @param message | |||||
* non null | |||||
* @param clp | |||||
* parser used to print options | |||||
* @throws IOException | |||||
* @since 4.2 | |||||
*/ | |||||
protected void printUsage(final String message, final CmdLineParser clp) | |||||
throws IOException { | |||||
errw.println(message); | errw.println(message); | ||||
errw.print("jgit "); //$NON-NLS-1$ | errw.print("jgit "); //$NON-NLS-1$ | ||||
errw.print(commandName); | errw.print(commandName); | ||||
errw.println(); | errw.println(); | ||||
errw.flush(); | errw.flush(); | ||||
throw die(true); | |||||
} | } | ||||
/** | /** | ||||
dst = dst.substring(R_REMOTES.length()); | dst = dst.substring(R_REMOTES.length()); | ||||
return dst; | return dst; | ||||
} | } | ||||
/** | |||||
* @param args | |||||
* non null | |||||
* @return true if the given array contains help option | |||||
* @since 4.2 | |||||
*/ | |||||
public static boolean containsHelp(String[] args) { | |||||
for (String str : args) { | |||||
if (str.equals("-h") || str.equals("--help")) { //$NON-NLS-1$ //$NON-NLS-2$ | |||||
return true; | |||||
} | |||||
} | |||||
return false; | |||||
} | |||||
/** | |||||
* Exception thrown by {@link TextBuiltin} if it proceeds 'help' option | |||||
* | |||||
* @since 4.2 | |||||
*/ | |||||
public static class TerminatedByHelpException extends Die { | |||||
private static final long serialVersionUID = 1L; | |||||
/** | |||||
* Default constructor | |||||
*/ | |||||
public TerminatedByHelpException() { | |||||
super(true); | |||||
} | |||||
} | |||||
} | } |
import java.lang.reflect.Field; | import java.lang.reflect.Field; | ||||
import java.util.ArrayList; | import java.util.ArrayList; | ||||
import java.util.Collections; | |||||
import java.util.Iterator; | |||||
import java.util.List; | |||||
import org.kohsuke.args4j.Argument; | |||||
import org.kohsuke.args4j.CmdLineException; | |||||
import org.kohsuke.args4j.IllegalAnnotationError; | |||||
import org.kohsuke.args4j.NamedOptionDef; | |||||
import org.kohsuke.args4j.Option; | |||||
import org.kohsuke.args4j.OptionDef; | |||||
import org.kohsuke.args4j.spi.OptionHandler; | |||||
import org.kohsuke.args4j.spi.Setter; | |||||
import org.eclipse.jgit.lib.ObjectId; | import org.eclipse.jgit.lib.ObjectId; | ||||
import org.eclipse.jgit.lib.Repository; | import org.eclipse.jgit.lib.Repository; | ||||
import org.eclipse.jgit.pgm.TextBuiltin; | import org.eclipse.jgit.pgm.TextBuiltin; | ||||
import org.eclipse.jgit.revwalk.RevWalk; | import org.eclipse.jgit.revwalk.RevWalk; | ||||
import org.eclipse.jgit.transport.RefSpec; | import org.eclipse.jgit.transport.RefSpec; | ||||
import org.eclipse.jgit.treewalk.AbstractTreeIterator; | import org.eclipse.jgit.treewalk.AbstractTreeIterator; | ||||
import org.kohsuke.args4j.Argument; | |||||
import org.kohsuke.args4j.CmdLineException; | |||||
import org.kohsuke.args4j.IllegalAnnotationError; | |||||
import org.kohsuke.args4j.NamedOptionDef; | |||||
import org.kohsuke.args4j.Option; | |||||
import org.kohsuke.args4j.OptionDef; | |||||
import org.kohsuke.args4j.spi.OptionHandler; | |||||
import org.kohsuke.args4j.spi.Setter; | |||||
/** | /** | ||||
* Extended command line parser which handles --foo=value arguments. | * Extended command line parser which handles --foo=value arguments. | ||||
private RevWalk walk; | private RevWalk walk; | ||||
private boolean seenHelp; | |||||
/** | /** | ||||
* Creates a new command line owner that parses arguments/options and set | * Creates a new command line owner that parses arguments/options and set | ||||
* them into the given object. | * them into the given object. | ||||
} | } | ||||
tmp.add(str); | tmp.add(str); | ||||
if (containsHelp(args)) { | |||||
// suppress exceptions on required parameters if help is present | |||||
seenHelp = true; | |||||
// stop argument parsing here | |||||
break; | |||||
} | |||||
} | } | ||||
List<OptionHandler> backup = null; | |||||
if (seenHelp) { | |||||
backup = unsetRequiredOptions(); | |||||
} | |||||
try { | |||||
super.parseArgument(tmp.toArray(new String[tmp.size()])); | |||||
} finally { | |||||
// reset "required" options to defaults for correct command printout | |||||
if (backup != null && !backup.isEmpty()) { | |||||
restoreRequiredOptions(backup); | |||||
} | |||||
seenHelp = false; | |||||
} | |||||
} | |||||
private List<OptionHandler> unsetRequiredOptions() { | |||||
List<OptionHandler> options = getOptions(); | |||||
List<OptionHandler> backup = new ArrayList<>(options); | |||||
for (Iterator<OptionHandler> iterator = options.iterator(); iterator | |||||
.hasNext();) { | |||||
OptionHandler handler = iterator.next(); | |||||
if (handler.option instanceof NamedOptionDef | |||||
&& handler.option.required()) { | |||||
iterator.remove(); | |||||
} | |||||
} | |||||
return backup; | |||||
} | |||||
private void restoreRequiredOptions(List<OptionHandler> backup) { | |||||
List<OptionHandler> options = getOptions(); | |||||
options.clear(); | |||||
options.addAll(backup); | |||||
} | |||||
super.parseArgument(tmp.toArray(new String[tmp.size()])); | |||||
/** | |||||
* @param args | |||||
* non null | |||||
* @return true if the given array contains help option | |||||
* @since 4.2 | |||||
*/ | |||||
protected boolean containsHelp(final String... args) { | |||||
return TextBuiltin.containsHelp(args); | |||||
} | } | ||||
/** | /** | ||||
return walk; | return walk; | ||||
} | } | ||||
static class MyOptionDef extends OptionDef { | |||||
class MyOptionDef extends OptionDef { | |||||
public MyOptionDef(OptionDef o) { | public MyOptionDef(OptionDef o) { | ||||
super(o.usage(), o.metaVar(), o.required(), o.handler(), o | super(o.usage(), o.metaVar(), o.required(), o.handler(), o | ||||
return metaVar(); | return metaVar(); | ||||
} | } | ||||
} | } | ||||
@Override | |||||
public boolean required() { | |||||
return seenHelp ? false : super.required(); | |||||
} | |||||
} | } | ||||
@Override | @Override | ||||
return super.createOptionHandler(new MyOptionDef(o), setter); | return super.createOptionHandler(new MyOptionDef(o), setter); | ||||
} | } | ||||
@SuppressWarnings("unchecked") | |||||
private List<OptionHandler> getOptions() { | |||||
List<OptionHandler> options = null; | |||||
try { | |||||
Field field = org.kohsuke.args4j.CmdLineParser.class | |||||
.getDeclaredField("options"); //$NON-NLS-1$ | |||||
field.setAccessible(true); | |||||
options = (List<OptionHandler>) field.get(this); | |||||
} catch (NoSuchFieldException | SecurityException | |||||
| IllegalArgumentException | IllegalAccessException e) { | |||||
// ignore | |||||
} | |||||
if (options == null) { | |||||
return Collections.emptyList(); | |||||
} | |||||
return options; | |||||
} | |||||
} | } |