diff --git a/baksmali/src/main/java/org/jf/baksmali/DeodexCommand.java b/baksmali/src/main/java/org/jf/baksmali/DeodexCommand.java index 92f5ed02..e7d38c06 100644 --- a/baksmali/src/main/java/org/jf/baksmali/DeodexCommand.java +++ b/baksmali/src/main/java/org/jf/baksmali/DeodexCommand.java @@ -38,12 +38,18 @@ import org.jf.dexlib2.analysis.CustomInlineMethodResolver; import org.jf.dexlib2.analysis.InlineMethodResolver; import org.jf.dexlib2.dexbacked.DexBackedOdexFile; import org.jf.dexlib2.iface.DexFile; +import org.jf.util.jcommander.ExtendedParameter; +import org.jf.util.jcommander.ExtendedParameters; import javax.annotation.Nonnull; import java.io.File; import java.io.IOException; +import java.util.List; @Parameters(commandDescription = "Deodexes an odex/oat file") +@ExtendedParameters( + commandName = "deodex", + commandAliases = { "de", "x" }) public class DeodexCommand extends DisassembleCommand { @Parameter(names = "--check-package-private-access", description = "Use the package-private access check when calculating vtable indexes. This should " + @@ -54,10 +60,11 @@ public class DeodexCommand extends DisassembleCommand { description = "Specify a file containing a custom inline method table to use. See the " + "\"deodexerant\" tool in the smali github repository to dump the inline method table from a " + "device that uses dalvik.") + @ExtendedParameter(argumentNames = "file") private String inlineTable; - public DeodexCommand(@Nonnull JCommander jc) { - super(jc); + public DeodexCommand(@Nonnull List commandAncestors) { + super(commandAncestors); } @Override protected BaksmaliOptions getOptions(DexFile dexFile) { diff --git a/baksmali/src/main/java/org/jf/baksmali/DexInputCommand.java b/baksmali/src/main/java/org/jf/baksmali/DexInputCommand.java index 30823761..207dbe53 100644 --- a/baksmali/src/main/java/org/jf/baksmali/DexInputCommand.java +++ b/baksmali/src/main/java/org/jf/baksmali/DexInputCommand.java @@ -31,19 +31,26 @@ package org.jf.baksmali; +import com.beust.jcommander.JCommander; import org.jf.dexlib2.DexFileFactory; import org.jf.dexlib2.dexbacked.DexBackedDexFile; import org.jf.dexlib2.dexbacked.OatFile; +import org.jf.util.jcommander.Command; import javax.annotation.Nonnull; import java.io.File; import java.io.IOException; +import java.util.List; /** * This class implements common functionality for commands that need to load a dex file based on * command line input */ -public abstract class DexInputCommand implements Command { +public abstract class DexInputCommand extends Command { + + public DexInputCommand(@Nonnull List commandAncestors) { + super(commandAncestors); + } /** * Parses a dex file input from the user and loads the given dex file. diff --git a/baksmali/src/main/java/org/jf/baksmali/DisassembleCommand.java b/baksmali/src/main/java/org/jf/baksmali/DisassembleCommand.java index 8995887a..bdd9e85a 100644 --- a/baksmali/src/main/java/org/jf/baksmali/DisassembleCommand.java +++ b/baksmali/src/main/java/org/jf/baksmali/DisassembleCommand.java @@ -45,6 +45,8 @@ import org.jf.dexlib2.iface.DexFile; import org.jf.dexlib2.util.SyntheticAccessorResolver; import org.jf.util.StringWrapper; import org.jf.util.jcommander.CommaColonParameterSplitter; +import org.jf.util.jcommander.ExtendedParameter; +import org.jf.util.jcommander.ExtendedParameters; import javax.annotation.Nonnull; import java.io.File; @@ -53,29 +55,32 @@ import java.util.List; import java.util.Map; @Parameters(commandDescription = "Disassembles a dex file.") +@ExtendedParameters( + commandName = "disassemble", + commandAliases = { "dis", "d" }) public class DisassembleCommand extends DexInputCommand { - @Nonnull private final JCommander jc; - @Parameter(names = {"-h", "-?", "--help"}, help = true, description = "Show usage information for this command.") private boolean help; @Parameter(names = {"-a", "--api"}, description = "The numeric api level of the file being disassembled.") + @ExtendedParameter(argumentNames = "api") private int apiLevel = 15; @Parameter(names = "--debug-info", arity = 1, description = "Whether to include debug information in the output (.local, .param, .line, etc.). Use " + "--debug-info=false to disable.") + @ExtendedParameter(argumentNames = "boolean") private boolean debugInfo = true; @Parameter(names = {"-b", "--bootclasspath"}, - description = "A comma/colon separated list of the bootclasspath jar/oat files to include in the " + - "classpath when analyzing the dex file. This will override any automatic selection of " + - "bootclasspath files that baksmali would otherwise perform. This is analogous to Android's " + - "BOOTCLASSPATH environment variable.", + description = "A comma/colon separated list of the jar/oat files to include in the " + + "bootclasspath when analyzing the dex file. If not specified, baksmali will attempt to choose an " + + "appropriate default. This is analogous to Android's BOOTCLASSPATH environment variable.", splitter = CommaColonParameterSplitter.class) + @ExtendedParameter(argumentNames = "classpath") private List bootClassPath = null; @Parameter(names = {"-c", "--classpath"}, @@ -83,16 +88,17 @@ public class DisassembleCommand extends DexInputCommand { "when analyzing the dex file. These will be added to the classpath after any bootclasspath " + "entries.", splitter = CommaColonParameterSplitter.class) + @ExtendedParameter(argumentNames = "classpath") private List classPath = Lists.newArrayList(); @Parameter(names = {"-d", "--classpath-dir"}, description = "A directory to search for classpath files. This option can be used multiple times to " + "specify multiple directories to search. They will be searched in the order they are provided.") + @ExtendedParameter(argumentNames = "dirs") private List classPathDirectories = Lists.newArrayList("."); @Parameter(names = {"--code-offsets"}, - description = "Add comments to the disassembly containing the code offset within the method for each " + - "instruction.") + description = "Add a comment before each instruction with it's code offset within the method.") private boolean codeOffsets = false; @Parameter(names = "--resolve-resources", arity=1, @@ -100,11 +106,13 @@ public class DisassembleCommand extends DexInputCommand { "comment with the name of the resource being referenced. The value should be a comma/colon" + "separated list of prefix=file pairs. For example R=res/values/public.xml:android.R=" + "$ANDROID_HOME/platforms/android-19/data/res/values/public.xml") + @ExtendedParameter(argumentNames = "resource spec") private List resourceIdFiles = Lists.newArrayList(); @Parameter(names = {"-j", "--jobs"}, description = "The number of threads to use. Defaults to the number of cores available.", validateWith = PositiveInteger.class) + @ExtendedParameter(argumentNames = "n") private int jobs = Runtime.getRuntime().availableProcessors(); @Parameter(names = {"-l", "--use-locals"}, @@ -113,7 +121,9 @@ public class DisassembleCommand extends DexInputCommand { private boolean localsDirective = false; @Parameter(names = "--accessor-comments", arity = 1, - description = "Generate helper comments for synthetic accessors. Use --accessor-comments=false to disable.") + description = "Generate helper comments for synthetic accessors. Use --accessor-comments=false to " + + "disable.") + @ExtendedParameter(argumentNames = "boolean") private boolean accessorComments = true; @Parameter(names = "--normalize-virtual-methods", @@ -123,17 +133,20 @@ public class DisassembleCommand extends DexInputCommand { @Parameter(names = {"-o", "--output"}, description = "The directory to write the disassembled files to.") + @ExtendedParameter(argumentNames = "dir") private String outputDir = "out"; @Parameter(names = "--parameter-registers", arity = 1, description = "Use the pNN syntax for registers that refer to a method parameter on method entry. Use" + "--parameter-registers=false to disable.") + @ExtendedParameter(argumentNames = "boolean") private boolean parameterRegisters = true; @Parameter(names = {"-r", "--register-info"}, arity=1, description = "Add comments before/after each instruction with information about register types. " + "The value is a comma-separated list of any of ALL, ALLPRE, ALLPOST, ARGS, DEST, MERGE and " + "FULLMERGE. See \"baksmali help register-info\" for more information.") + @ExtendedParameter(argumentNames = "register info specifier") private List registerInfoTypes = Lists.newArrayList(); @Parameter(names = "--sequential-labels", @@ -142,7 +155,7 @@ public class DisassembleCommand extends DexInputCommand { private boolean sequentialLabels = false; @Parameter(names = "--implicit-references", - description = "Use implicit (without the class name) method and field references for methods and " + + description = "Use implicit method and field references (without the class name) for methods and " + "fields from the current class.") private boolean implicitReferences = false; @@ -151,24 +164,25 @@ public class DisassembleCommand extends DexInputCommand { "supported in the Android runtime yet.") private boolean experimentalOpcodes = false; - @Parameter(description = " - A dex/apk/oat/odex file. For apk or oat files that contain multiple dex " + + @Parameter(description = "A dex/apk/oat/odex file. For apk or oat files that contain multiple dex " + "files, you can specify which dex file to disassemble by appending the name of the dex file with a " + "colon. E.g. \"something.apk:classes2.dex\"") + @ExtendedParameter(argumentNames = "file") private List inputList = Lists.newArrayList(); - public DisassembleCommand(@Nonnull JCommander jc) { - this.jc = jc; + public DisassembleCommand(@Nonnull List commandAncestors) { + super(commandAncestors); } public void run() { if (help || inputList == null || inputList.isEmpty()) { - jc.usage(jc.getParsedCommand()); + usage(); return; } if (inputList.size() > 1) { System.err.println("Too many files specified"); - jc.usage(jc.getParsedCommand()); + usage(); return; } @@ -261,7 +275,7 @@ public class DisassembleCommand extends DexInputCommand { int separatorIndex = resourceIdFileSpec.indexOf('='); if (separatorIndex == -1) { System.err.println(String.format("Invalid resource id spec: %s", resourceIdFileSpec)); - jc.usage(jc.getParsedCommand()); + usage(); System.exit(-1); } String prefix = resourceIdFileSpec.substring(0, separatorIndex); @@ -308,7 +322,7 @@ public class DisassembleCommand extends DexInputCommand { options.registerInfo |= BaksmaliOptions.FULLMERGE; } else { System.err.println(String.format("Invalid register info type: %s", registerInfoType)); - jc.usage(jc.getParsedCommand()); + usage(); System.exit(-1); } diff --git a/baksmali/src/main/java/org/jf/baksmali/DumpCommand.java b/baksmali/src/main/java/org/jf/baksmali/DumpCommand.java index e1d9da24..f3555db8 100644 --- a/baksmali/src/main/java/org/jf/baksmali/DumpCommand.java +++ b/baksmali/src/main/java/org/jf/baksmali/DumpCommand.java @@ -41,15 +41,19 @@ import org.jf.dexlib2.dexbacked.OatFile; import org.jf.dexlib2.dexbacked.raw.RawDexFile; import org.jf.dexlib2.dexbacked.raw.util.DexAnnotator; import org.jf.util.ConsoleUtil; +import org.jf.util.jcommander.Command; +import org.jf.util.jcommander.ExtendedParameter; +import org.jf.util.jcommander.ExtendedParameters; import javax.annotation.Nonnull; import java.io.*; +import java.util.List; @Parameters(commandDescription = "Prints an annotated hex dump for the given dex file") -public class DumpCommand implements Command { - - @Nonnull - private final JCommander jc; +@ExtendedParameters( + commandName = "dump", + commandAliases = "du") +public class DumpCommand extends Command { @Parameter(names = {"-h", "-?", "--help"}, help = true, description = "Show usage information for this command.") @@ -57,6 +61,7 @@ public class DumpCommand implements Command { @Parameter(names = {"-a", "--api"}, description = "The numeric api level of the file being disassembled.") + @ExtendedParameter(argumentNames = "api") private int apiLevel = 15; @Parameter(names = "--experimental", @@ -67,15 +72,16 @@ public class DumpCommand implements Command { @Parameter(description = " - A dex/apk/oat/odex file. For apk or oat files that contain multiple dex " + "files, you can specify which dex file to disassemble by appending the name of the dex file with a " + "colon. E.g. \"something.apk:classes2.dex\"") + @ExtendedParameter(argumentNames = "file") private String input; - public DumpCommand(@Nonnull JCommander jc) { - this.jc = jc; + public DumpCommand(@Nonnull List commandAncestors) { + super(commandAncestors); } public void run() { if (help || input == null || input.isEmpty()) { - jc.usage("dump"); + usage(); return; } diff --git a/baksmali/src/main/java/org/jf/baksmali/HelpCommand.java b/baksmali/src/main/java/org/jf/baksmali/HelpCommand.java index 612e0a1d..b6c334a1 100644 --- a/baksmali/src/main/java/org/jf/baksmali/HelpCommand.java +++ b/baksmali/src/main/java/org/jf/baksmali/HelpCommand.java @@ -37,27 +37,37 @@ import com.beust.jcommander.Parameters; import com.google.common.collect.Lists; import org.jf.util.ConsoleUtil; import org.jf.util.StringWrapper; +import org.jf.util.jcommander.*; import javax.annotation.Nonnull; import java.util.List; @Parameters(commandDescription = "Shows usage information") -public class HelpCommand implements Command { - @Nonnull private final JCommander jc; +@ExtendedParameters( + commandName = "help", + commandAliases = "h") +public class HelpCommand extends Command { - public HelpCommand(@Nonnull JCommander jc) { - this.jc = jc; + public HelpCommand(@Nonnull List commandAncestors) { + super(commandAncestors); } - @Parameter(description = "If specified, only show the usage information for the given commands") + @Parameter(description = "If specified, show the detailed usage information for the given commands") + @ExtendedParameter(argumentNames = "commands") private List commands = Lists.newArrayList(); public void run() { + JCommander parentJc = commandAncestors.get(commandAncestors.size() - 1); + if (commands == null || commands.isEmpty()) { - jc.usage(); + System.out.println(new HelpFormatter() + .width(ConsoleUtil.getConsoleWidth()) + .format(commandAncestors)); } else { + boolean printedHelp = false; for (String cmd : commands) { if (cmd.equals("register-info")) { + printedHelp = true; String registerInfoHelp = "The --register-info parameter will cause baksmali to generate " + "comments before and after every instruction containing register type " + "information about some subset of registers. This parameter optionally accepts a " + @@ -80,16 +90,30 @@ public class HelpCommand implements Command { System.out.println(line); } } else { - jc.usage(cmd); + JCommander command = ExtendedCommands.getSubcommand(parentJc, cmd); + if (command == null) { + System.err.println("No such command: " + cmd); + } else { + printedHelp = true; + System.out.println(new HelpFormatter() + .width(ConsoleUtil.getConsoleWidth()) + .format(((Command)command.getObjects().get(0)).getCommandHierarchy())); + } } } + if (!printedHelp) { + System.out.println(new HelpFormatter() + .width(ConsoleUtil.getConsoleWidth()) + .format(commandAncestors)); + } } } @Parameters(hidden = true) + @ExtendedParameters(commandName = "hlep") public static class HlepCommand extends HelpCommand { - public HlepCommand(@Nonnull JCommander jc) { - super(jc); + public HlepCommand(@Nonnull List commandAncestors) { + super(commandAncestors); } } } diff --git a/baksmali/src/main/java/org/jf/baksmali/ListClassPathCommand.java b/baksmali/src/main/java/org/jf/baksmali/ListClassPathCommand.java index d41973ac..6a2290cd 100644 --- a/baksmali/src/main/java/org/jf/baksmali/ListClassPathCommand.java +++ b/baksmali/src/main/java/org/jf/baksmali/ListClassPathCommand.java @@ -39,36 +39,41 @@ import org.jf.dexlib2.Opcodes; import org.jf.dexlib2.dexbacked.DexBackedDexFile; import org.jf.dexlib2.dexbacked.DexBackedOdexFile; import org.jf.dexlib2.dexbacked.OatFile; +import org.jf.util.jcommander.Command; +import org.jf.util.jcommander.ExtendedParameter; +import org.jf.util.jcommander.ExtendedParameters; import javax.annotation.Nonnull; import java.io.*; import java.util.List; @Parameters(commandDescription = "Lists the stored classpath entries in an odex/oat file.") -public class ListClassPathCommand implements Command { - - @Nonnull private final JCommander jc; +@ExtendedParameters( + commandName = "classpath", + commandAliases = { "bootclasspath", "cp", "bcp" }) +public class ListClassPathCommand extends Command { @Parameter(names = {"-h", "-?", "--help"}, help = true, description = "Show usage information") private boolean help; - @Parameter(description = " - An oat/odex file") + @Parameter(description = "An oat/odex file") + @ExtendedParameter(argumentNames = "file") private List inputList = Lists.newArrayList(); - public ListClassPathCommand(@Nonnull JCommander jc) { - this.jc = jc; + public ListClassPathCommand(@Nonnull List commandAncestors) { + super(commandAncestors); } @Override public void run() { if (help || inputList == null || inputList.isEmpty()) { - jc.usage(jc.getParsedCommand()); + usage(); return; } if (inputList.size() > 1) { System.err.println("Too many files specified"); - jc.usage(jc.getParsedCommand()); + usage(); return; } diff --git a/baksmali/src/main/java/org/jf/baksmali/ListClassesCommand.java b/baksmali/src/main/java/org/jf/baksmali/ListClassesCommand.java index 90f76130..8b24934b 100644 --- a/baksmali/src/main/java/org/jf/baksmali/ListClassesCommand.java +++ b/baksmali/src/main/java/org/jf/baksmali/ListClassesCommand.java @@ -38,37 +38,41 @@ import com.google.common.collect.Lists; import org.jf.dexlib2.dexbacked.DexBackedDexFile; import org.jf.dexlib2.iface.reference.Reference; import org.jf.dexlib2.util.ReferenceUtil; +import org.jf.util.jcommander.ExtendedParameter; +import org.jf.util.jcommander.ExtendedParameters; import javax.annotation.Nonnull; import java.util.List; @Parameters(commandDescription = "Lists the classes in a dex file.") +@ExtendedParameters( + commandName = "classes", + commandAliases = { "class", "c" }) public class ListClassesCommand extends DexInputCommand { - @Nonnull private final JCommander jc; - @Parameter(names = {"-h", "-?", "--help"}, help = true, description = "Show usage information") private boolean help; - @Parameter(description = " - A dex/apk/oat/odex file. For apk or oat files that contain multiple dex " + + @Parameter(description = "A dex/apk/oat/odex file. For apk or oat files that contain multiple dex " + "files, you can specify which dex file to disassemble by appending the name of the dex file with a " + "colon. E.g. \"something.apk:classes2.dex\"") + @ExtendedParameter(argumentNames = "file") private List inputList = Lists.newArrayList(); - public ListClassesCommand(@Nonnull JCommander jc) { - this.jc = jc; + public ListClassesCommand(@Nonnull List commandAncestors) { + super(commandAncestors); } @Override public void run() { if (help || inputList == null || inputList.isEmpty()) { - jc.usage(jc.getParsedCommand()); + usage(); return; } if (inputList.size() > 1) { System.err.println("Too many files specified"); - jc.usage(jc.getParsedCommand()); + usage(); return; } diff --git a/baksmali/src/main/java/org/jf/baksmali/ListCommand.java b/baksmali/src/main/java/org/jf/baksmali/ListCommand.java index cf063d9c..62290976 100644 --- a/baksmali/src/main/java/org/jf/baksmali/ListCommand.java +++ b/baksmali/src/main/java/org/jf/baksmali/ListCommand.java @@ -34,43 +34,53 @@ package org.jf.baksmali; import com.beust.jcommander.JCommander; import com.beust.jcommander.Parameter; import com.beust.jcommander.Parameters; +import org.jf.baksmali.ListHelpCommand.ListHlepCommand; +import org.jf.util.jcommander.Command; +import org.jf.util.jcommander.ExtendedCommands; +import org.jf.util.jcommander.ExtendedParameters; import javax.annotation.Nonnull; +import java.util.List; @Parameters(commandDescription = "Lists various objects in a dex file.") -public class ListCommand implements Command { - - @Nonnull private final JCommander jc; - @Nonnull private JCommander subJc; +@ExtendedParameters( + commandName = "list", + commandAliases = "l") +public class ListCommand extends Command { @Parameter(names = {"-h", "-?", "--help"}, help = true, description = "Show usage information") private boolean help; - public ListCommand(@Nonnull JCommander jc) { - this.jc = jc; + public ListCommand(@Nonnull List commandAncestors) { + super(commandAncestors); } public void registerSubCommands() { - subJc = jc.getCommands().get("list"); - subJc.addCommand("strings", new ListStringsCommand(subJc), "string", "str", "s"); - subJc.addCommand("methods", new ListMethodsCommand(subJc), "method", "m"); - subJc.addCommand("fields", new ListFieldsCommand(subJc), "field", "f"); - subJc.addCommand("types", new ListTypesCommand(subJc), "type", "t"); - subJc.addCommand("classes", new ListClassesCommand(subJc), "class", "c"); - subJc.addCommand("dex", new ListDexCommand(subJc), "d"); - subJc.addCommand("vtables", new ListVtablesCommand(subJc), "vtable", "v"); - subJc.addCommand("fieldoffsets", new ListFieldOffsetsCommand(subJc), "fieldoffset", "fo"); - subJc.addCommand("classpath", new ListClassPathCommand(subJc), "bootclasspath", "cp", "bcp"); + JCommander subJc = getJCommander(); + List hierarchy = getCommandHierarchy(); + + ExtendedCommands.addExtendedCommand(subJc, new ListStringsCommand(hierarchy)); + ExtendedCommands.addExtendedCommand(subJc, new ListMethodsCommand(hierarchy)); + ExtendedCommands.addExtendedCommand(subJc, new ListFieldsCommand(hierarchy)); + ExtendedCommands.addExtendedCommand(subJc, new ListTypesCommand(hierarchy)); + ExtendedCommands.addExtendedCommand(subJc, new ListClassesCommand(hierarchy)); + ExtendedCommands.addExtendedCommand(subJc, new ListDexCommand(hierarchy)); + ExtendedCommands.addExtendedCommand(subJc, new ListVtablesCommand(hierarchy)); + ExtendedCommands.addExtendedCommand(subJc, new ListFieldOffsetsCommand(hierarchy)); + ExtendedCommands.addExtendedCommand(subJc, new ListClassPathCommand(hierarchy)); + ExtendedCommands.addExtendedCommand(subJc, new ListHelpCommand(hierarchy)); + ExtendedCommands.addExtendedCommand(subJc, new ListHlepCommand(hierarchy)); } @Override public void run() { - if (help || subJc.getParsedCommand() == null) { - subJc.usage(); + JCommander jc = getJCommander(); + if (help || jc.getParsedCommand() == null) { + usage(); return; } - Command command = (Command)subJc.getCommands().get(subJc.getParsedCommand()).getObjects().get(0); + Command command = (Command)jc.getCommands().get(jc.getParsedCommand()).getObjects().get(0); command.run(); } } diff --git a/baksmali/src/main/java/org/jf/baksmali/ListDexCommand.java b/baksmali/src/main/java/org/jf/baksmali/ListDexCommand.java index 8c870341..65aa1ec8 100644 --- a/baksmali/src/main/java/org/jf/baksmali/ListDexCommand.java +++ b/baksmali/src/main/java/org/jf/baksmali/ListDexCommand.java @@ -38,6 +38,9 @@ import com.google.common.collect.Lists; import org.jf.dexlib2.dexbacked.OatFile; import org.jf.dexlib2.dexbacked.raw.HeaderItem; import org.jf.dexlib2.dexbacked.raw.OdexHeaderItem; +import org.jf.util.jcommander.Command; +import org.jf.util.jcommander.ExtendedParameter; +import org.jf.util.jcommander.ExtendedParameters; import javax.annotation.Nonnull; import java.io.*; @@ -48,30 +51,32 @@ import java.util.zip.ZipException; import java.util.zip.ZipFile; @Parameters(commandDescription = "Lists the dex files in an apk/oat file.") -public class ListDexCommand implements Command { - - @Nonnull private final JCommander jc; +@ExtendedParameters( + commandName = "dex", + commandAliases = "d") +public class ListDexCommand extends Command { @Parameter(names = {"-h", "-?", "--help"}, help = true, description = "Show usage information") private boolean help; - @Parameter(description = " - An apk or oat file.") + @Parameter(description = "An apk or oat file.") + @ExtendedParameter(argumentNames = "file") private List inputList = Lists.newArrayList(); - public ListDexCommand(@Nonnull JCommander jc) { - this.jc = jc; + public ListDexCommand(@Nonnull List commandAncestors) { + super(commandAncestors); } @Override public void run() { if (help || inputList == null || inputList.isEmpty()) { - jc.usage(jc.getParsedCommand()); + usage(); return; } if (inputList.size() > 1) { System.err.println("Too many files specified"); - jc.usage(jc.getParsedCommand()); + usage(); return; } diff --git a/baksmali/src/main/java/org/jf/baksmali/ListFieldOffsetsCommand.java b/baksmali/src/main/java/org/jf/baksmali/ListFieldOffsetsCommand.java index 8540ae19..e39f09b4 100644 --- a/baksmali/src/main/java/org/jf/baksmali/ListFieldOffsetsCommand.java +++ b/baksmali/src/main/java/org/jf/baksmali/ListFieldOffsetsCommand.java @@ -43,6 +43,8 @@ import org.jf.dexlib2.iface.DexFile; import org.jf.dexlib2.iface.reference.FieldReference; import org.jf.util.SparseArray; import org.jf.util.jcommander.CommaColonParameterSplitter; +import org.jf.util.jcommander.ExtendedParameter; +import org.jf.util.jcommander.ExtendedParameters; import javax.annotation.Nonnull; import java.io.IOException; @@ -50,40 +52,46 @@ import java.util.ArrayList; import java.util.List; @Parameters(commandDescription = "Lists the instance field offsets for classes in a dex file.") +@ExtendedParameters( + commandName = "fieldoffsets", + commandAliases = { "fieldoffset", "fo" }) public class ListFieldOffsetsCommand extends DexInputCommand { - @Nonnull private final JCommander jc; - @Parameter(names = {"-h", "-?", "--help"}, help = true, description = "Show usage information") private boolean help; @Parameter(names = {"-a", "--api"}, - description = "The numeric api level of the file being loaded.") + description = "The numeric api level of the file being disassembled.") + @ExtendedParameter(argumentNames = "api") private int apiLevel = 15; - @Parameter(description = " - A dex/apk/oat/odex file. For apk or oat files that contain multiple dex " + + @Parameter(description = "A dex/apk/oat/odex file. For apk or oat files that contain multiple dex " + "files, you can specify which dex file to disassemble by appending the name of the dex file with a " + "colon. E.g. \"something.apk:classes2.dex\"") + @ExtendedParameter(argumentNames = "file") private List inputList = Lists.newArrayList(); @Parameter(names = {"-b", "--bootclasspath"}, - description = "A comma/colon separated list of the bootclasspath jar/oat files to include in the " + - "classpath when analyzing the dex file. This will override any automatic selection of " + - "bootclasspath files that baksmali would otherwise perform. This is analogous to Android's " + - "BOOTCLASSPATH environment variable.", + description = "A comma/colon separated list of the jar/oat files to include in the " + + "bootclasspath when analyzing the dex file. If not specified, baksmali will attempt to choose an " + + "appropriate default. This is analogous to Android's BOOTCLASSPATH environment variable.", splitter = CommaColonParameterSplitter.class) - private List bootClassPath = new ArrayList(); + @ExtendedParameter(argumentNames = "classpath") + private List bootClassPath = null; @Parameter(names = {"-c", "--classpath"}, description = "A comma/colon separated list of additional jar/oat files to include in the classpath " + "when analyzing the dex file. These will be added to the classpath after any bootclasspath " + "entries.", splitter = CommaColonParameterSplitter.class) + @ExtendedParameter(argumentNames = "classpath") private List classPath = new ArrayList(); @Parameter(names = {"-d", "--classpath-dir"}, - description = "baksmali will search these directories in order for any classpath entries.") + description = "A directory to search for classpath files. This option can be used multiple times to " + + "specify multiple directories to search. They will be searched in the order they are provided.") + @ExtendedParameter(argumentNames = "dirs") private List classPathDirectories = Lists.newArrayList("."); @Parameter(names = "--check-package-private-access", @@ -96,19 +104,19 @@ public class ListFieldOffsetsCommand extends DexInputCommand { "supported in the Android runtime yet.") private boolean experimentalOpcodes = false; - public ListFieldOffsetsCommand(@Nonnull JCommander jc) { - this.jc = jc; + public ListFieldOffsetsCommand(@Nonnull List commandAncestors) { + super(commandAncestors); } @Override public void run() { if (help || inputList == null || inputList.isEmpty()) { - jc.usage(jc.getParsedCommand()); + usage(); return; } if (inputList.size() > 1) { System.err.println("Too many files specified"); - jc.usage(jc.getParsedCommand()); + usage(); return; } diff --git a/baksmali/src/main/java/org/jf/baksmali/ListFieldsCommand.java b/baksmali/src/main/java/org/jf/baksmali/ListFieldsCommand.java index 10924172..c4d090dd 100644 --- a/baksmali/src/main/java/org/jf/baksmali/ListFieldsCommand.java +++ b/baksmali/src/main/java/org/jf/baksmali/ListFieldsCommand.java @@ -34,12 +34,17 @@ package org.jf.baksmali; import com.beust.jcommander.JCommander; import com.beust.jcommander.Parameters; import org.jf.dexlib2.ReferenceType; +import org.jf.util.jcommander.ExtendedParameters; import javax.annotation.Nonnull; +import java.util.List; @Parameters(commandDescription = "Lists the fields in a dex file's field table.") +@ExtendedParameters( + commandName = "fields", + commandAliases = { "field", "f" }) public class ListFieldsCommand extends ListReferencesCommand { - public ListFieldsCommand(@Nonnull JCommander jc) { - super(jc, ReferenceType.FIELD); + public ListFieldsCommand(@Nonnull List commandAncestors) { + super(commandAncestors, ReferenceType.FIELD); } } diff --git a/baksmali/src/main/java/org/jf/baksmali/ListHelpCommand.java b/baksmali/src/main/java/org/jf/baksmali/ListHelpCommand.java new file mode 100644 index 00000000..2e642861 --- /dev/null +++ b/baksmali/src/main/java/org/jf/baksmali/ListHelpCommand.java @@ -0,0 +1,92 @@ +/* + * Copyright 2016, Google Inc. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * Neither the name of Google Inc. nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.jf.baksmali; + +import com.beust.jcommander.JCommander; +import com.beust.jcommander.Parameter; +import com.beust.jcommander.Parameters; +import com.google.common.collect.Iterables; +import org.jf.util.ConsoleUtil; +import org.jf.util.jcommander.*; + +import javax.annotation.Nonnull; +import java.util.List; + +@Parameters(commandDescription = "Shows usage information") +@ExtendedParameters( + commandName = "help", + commandAliases = "h") +public class ListHelpCommand extends Command { + + @Parameter(description = "If specified, show the detailed usage information for the given commands") + @ExtendedParameter(argumentNames = "commands") + private List commands; + + public ListHelpCommand(@Nonnull List commandAncestors) { + super(commandAncestors); + } + + public void run() { + if (commands == null || commands.isEmpty()) { + System.out.println(new HelpFormatter() + .width(ConsoleUtil.getConsoleWidth()) + .format(commandAncestors)); + } else { + boolean printedHelp = false; + JCommander parentJc = Iterables.getLast(commandAncestors); + for (String cmd : commands) { + JCommander command = ExtendedCommands.getSubcommand(parentJc, cmd); + if (command == null) { + System.err.println("No such command: " + cmd); + } else { + printedHelp = true; + System.out.println(new HelpFormatter() + .width(ConsoleUtil.getConsoleWidth()) + .format(((Command)command.getObjects().get(0)).getCommandHierarchy())); + } + } + if (!printedHelp) { + System.out.println(new HelpFormatter() + .width(ConsoleUtil.getConsoleWidth()) + .format(commandAncestors)); + } + } + } + + @Parameters(hidden = true) + @ExtendedParameters(commandName = "hlep") + public static class ListHlepCommand extends ListHelpCommand { + public ListHlepCommand(@Nonnull List commandAncestors) { + super(commandAncestors); + } + } +} diff --git a/baksmali/src/main/java/org/jf/baksmali/ListMethodsCommand.java b/baksmali/src/main/java/org/jf/baksmali/ListMethodsCommand.java index afb5ed65..603e7647 100644 --- a/baksmali/src/main/java/org/jf/baksmali/ListMethodsCommand.java +++ b/baksmali/src/main/java/org/jf/baksmali/ListMethodsCommand.java @@ -34,12 +34,17 @@ package org.jf.baksmali; import com.beust.jcommander.JCommander; import com.beust.jcommander.Parameters; import org.jf.dexlib2.ReferenceType; +import org.jf.util.jcommander.ExtendedParameters; import javax.annotation.Nonnull; +import java.util.List; @Parameters(commandDescription = "Lists the methods in a dex file's method table.") +@ExtendedParameters( + commandName = "methods", + commandAliases = { "method", "m" }) public class ListMethodsCommand extends ListReferencesCommand { - public ListMethodsCommand(@Nonnull JCommander jc) { - super(jc, ReferenceType.METHOD); + public ListMethodsCommand(@Nonnull List commandAncestors) { + super(commandAncestors, ReferenceType.METHOD); } } \ No newline at end of file diff --git a/baksmali/src/main/java/org/jf/baksmali/ListReferencesCommand.java b/baksmali/src/main/java/org/jf/baksmali/ListReferencesCommand.java index 27bab5d8..5eeaa822 100644 --- a/baksmali/src/main/java/org/jf/baksmali/ListReferencesCommand.java +++ b/baksmali/src/main/java/org/jf/baksmali/ListReferencesCommand.java @@ -37,38 +37,39 @@ import com.google.common.collect.Lists; import org.jf.dexlib2.dexbacked.DexBackedDexFile; import org.jf.dexlib2.iface.reference.Reference; import org.jf.dexlib2.util.ReferenceUtil; +import org.jf.util.jcommander.ExtendedParameter; import javax.annotation.Nonnull; import java.util.List; public abstract class ListReferencesCommand extends DexInputCommand { - @Nonnull private final JCommander jc; private final int referenceType; @Parameter(names = {"-h", "-?", "--help"}, help = true, description = "Show usage information") private boolean help; - @Parameter(description = " - A dex/apk/oat/odex file. For apk or oat files that contain multiple dex " + + @Parameter(description = "A dex/apk/oat/odex file. For apk or oat files that contain multiple dex " + "files, you can specify which dex file to disassemble by appending the name of the dex file with a " + "colon. E.g. \"something.apk:classes2.dex\"") + @ExtendedParameter(argumentNames = "file") private List inputList = Lists.newArrayList(); - public ListReferencesCommand(@Nonnull JCommander jc, int referenceType) { - this.jc = jc; + public ListReferencesCommand(@Nonnull List commandAncestors, int referenceType) { + super(commandAncestors); this.referenceType = referenceType; } @Override public void run() { if (help || inputList == null || inputList.isEmpty()) { - jc.usage(jc.getParsedCommand()); + usage(); return; } if (inputList.size() > 1) { System.err.println("Too many files specified"); - jc.usage(jc.getParsedCommand()); + usage(); return; } diff --git a/baksmali/src/main/java/org/jf/baksmali/ListStringsCommand.java b/baksmali/src/main/java/org/jf/baksmali/ListStringsCommand.java index 97152f31..8694f911 100644 --- a/baksmali/src/main/java/org/jf/baksmali/ListStringsCommand.java +++ b/baksmali/src/main/java/org/jf/baksmali/ListStringsCommand.java @@ -34,12 +34,17 @@ package org.jf.baksmali; import com.beust.jcommander.JCommander; import com.beust.jcommander.Parameters; import org.jf.dexlib2.ReferenceType; +import org.jf.util.jcommander.ExtendedParameters; import javax.annotation.Nonnull; +import java.util.List; @Parameters(commandDescription = "Lists the strings in a dex file's string table.") +@ExtendedParameters( + commandName = "strings", + commandAliases = { "string", "str", "s" }) public class ListStringsCommand extends ListReferencesCommand { - public ListStringsCommand(@Nonnull JCommander jc) { - super(jc, ReferenceType.STRING); + public ListStringsCommand(@Nonnull List commandAncestors) { + super(commandAncestors, ReferenceType.STRING); } } \ No newline at end of file diff --git a/baksmali/src/main/java/org/jf/baksmali/ListTypesCommand.java b/baksmali/src/main/java/org/jf/baksmali/ListTypesCommand.java index cbbca32a..fbff2f29 100644 --- a/baksmali/src/main/java/org/jf/baksmali/ListTypesCommand.java +++ b/baksmali/src/main/java/org/jf/baksmali/ListTypesCommand.java @@ -34,12 +34,17 @@ package org.jf.baksmali; import com.beust.jcommander.JCommander; import com.beust.jcommander.Parameters; import org.jf.dexlib2.ReferenceType; +import org.jf.util.jcommander.ExtendedParameters; import javax.annotation.Nonnull; +import java.util.List; @Parameters(commandDescription = "Lists the type ids in a dex file's type table.") +@ExtendedParameters( + commandName = "types", + commandAliases = { "type", "t" }) public class ListTypesCommand extends ListReferencesCommand { - public ListTypesCommand(@Nonnull JCommander jc) { - super(jc, ReferenceType.TYPE); + public ListTypesCommand(@Nonnull List commandAncestors) { + super(commandAncestors, ReferenceType.TYPE); } } diff --git a/baksmali/src/main/java/org/jf/baksmali/ListVtablesCommand.java b/baksmali/src/main/java/org/jf/baksmali/ListVtablesCommand.java index 80d41650..ed3400b2 100644 --- a/baksmali/src/main/java/org/jf/baksmali/ListVtablesCommand.java +++ b/baksmali/src/main/java/org/jf/baksmali/ListVtablesCommand.java @@ -42,6 +42,8 @@ import org.jf.dexlib2.iface.ClassDef; import org.jf.dexlib2.iface.DexFile; import org.jf.dexlib2.iface.Method; import org.jf.util.jcommander.CommaColonParameterSplitter; +import org.jf.util.jcommander.ExtendedParameter; +import org.jf.util.jcommander.ExtendedParameters; import javax.annotation.Nonnull; import java.io.IOException; @@ -49,40 +51,46 @@ import java.util.ArrayList; import java.util.List; @Parameters(commandDescription = "Lists the virtual method tables for classes in a dex file.") +@ExtendedParameters( + commandName = "vtables", + commandAliases = { "vtable", "v" }) public class ListVtablesCommand extends DexInputCommand { - @Nonnull private final JCommander jc; - @Parameter(names = {"-h", "-?", "--help"}, help = true, description = "Show usage information") private boolean help; @Parameter(names = {"-a", "--api"}, description = "The numeric api level of the file being loaded.") + @ExtendedParameter(argumentNames = "api") public int apiLevel = 15; - @Parameter(description = " - A dex/apk/oat/odex file. For apk or oat files that contain multiple dex " + + @Parameter(description = "A dex/apk/oat/odex file. For apk or oat files that contain multiple dex " + "files, you can specify which dex file to disassemble by appending the name of the dex file with a " + "colon. E.g. \"something.apk:classes2.dex\"") + @ExtendedParameter(argumentNames = "file") private List inputList = Lists.newArrayList(); @Parameter(names = {"-b", "--bootclasspath"}, - description = "A comma/colon separated list of the bootclasspath jar/oat files to include in the " + - "classpath when analyzing the dex file. This will override any automatic selection of " + - "bootclasspath files that baksmali would otherwise perform. This is analogous to Android's " + - "BOOTCLASSPATH environment variable.", + description = "A comma/colon separated list of the jar/oat files to include in the " + + "bootclasspath when analyzing the dex file. If not specified, baksmali will attempt to choose an " + + "appropriate default. This is analogous to Android's BOOTCLASSPATH environment variable.", splitter = CommaColonParameterSplitter.class) - private List bootClassPath = new ArrayList(); + @ExtendedParameter(argumentNames = "classpath") + private List bootClassPath = null; @Parameter(names = {"-c", "--classpath"}, description = "A comma/colon separated list of additional jar/oat files to include in the classpath " + "when analyzing the dex file. These will be added to the classpath after any bootclasspath " + "entries.", splitter = CommaColonParameterSplitter.class) + @ExtendedParameter(argumentNames = "classpath") private List classPath = new ArrayList(); @Parameter(names = {"-d", "--classpath-dir"}, - description = "baksmali will search these directories in order for any classpath entries.") + description = "A directory to search for classpath files. This option can be used multiple times to " + + "specify multiple directories to search. They will be searched in the order they are provided.") + @ExtendedParameter(argumentNames = "dirs") private List classPathDirectories = Lists.newArrayList("."); @Parameter(names = "--check-package-private-access", @@ -97,21 +105,22 @@ public class ListVtablesCommand extends DexInputCommand { @Parameter(names = "--classes", description = "A comma separated list of classes: Only print the vtable for these classes") + @ExtendedParameter(argumentNames = "classes") private String classes = null; - public ListVtablesCommand(@Nonnull JCommander jc) { - this.jc = jc; + public ListVtablesCommand(@Nonnull List commandAncestors) { + super(commandAncestors); } @Override public void run() { if (help || inputList == null || inputList.isEmpty()) { - jc.usage(jc.getParsedCommand()); + usage(); return; } if (inputList.size() > 1) { System.err.println("Too many files specified"); - jc.usage(jc.getParsedCommand()); + usage(); return; } diff --git a/baksmali/src/main/java/org/jf/baksmali/Main.java b/baksmali/src/main/java/org/jf/baksmali/Main.java index 977049d6..cf0064a2 100644 --- a/baksmali/src/main/java/org/jf/baksmali/Main.java +++ b/baksmali/src/main/java/org/jf/baksmali/Main.java @@ -33,47 +33,71 @@ package org.jf.baksmali; import com.beust.jcommander.JCommander; import com.beust.jcommander.Parameter; +import com.google.common.collect.Lists; import org.jf.baksmali.HelpCommand.HlepCommand; +import org.jf.util.jcommander.Command; +import org.jf.util.jcommander.ExtendedCommands; +import org.jf.util.jcommander.ExtendedParameters; import java.io.IOException; import java.io.InputStream; +import java.util.List; import java.util.Properties; -public class Main { +@ExtendedParameters( + includeParametersInUsage = true, + commandName = "baksmali", + postfixDescription = "See baksmali help for more information about a specific command") +public class Main extends Command { public static final String VERSION = loadVersion(); - @Parameter(names = {"-h", "-?", "--help"}, help = true, + @Parameter(names = {"--help", "-h", "-?"}, help = true, description = "Show usage information") private boolean help; - @Parameter(names = {"-v", "--version"}, help = true, + @Parameter(names = {"--version", "-v"}, help = true, description = "Print the version of baksmali and then exit") public boolean version; + private JCommander jc; + + public Main() { + super(Lists.newArrayList()); + } + + @Override public void run() { + } + + @Override protected JCommander getJCommander() { + return jc; + } + public static void main(String[] args) { Main main = new Main(); JCommander jc = new JCommander(main); + main.jc = jc; + jc.setProgramName("baksmali"); + List commandHierarchy = main.getCommandHierarchy(); - jc.addCommand("disassemble", new DisassembleCommand(jc), "dis", "d"); - jc.addCommand("deodex", new DeodexCommand(jc), "de", "x"); - jc.addCommand("dump", new DumpCommand(jc), "du"); - jc.addCommand("help", new HelpCommand(jc), "h"); - jc.addCommand("hlep", new HlepCommand(jc)); + ExtendedCommands.addExtendedCommand(jc, new DisassembleCommand(commandHierarchy)); + ExtendedCommands.addExtendedCommand(jc, new DeodexCommand(commandHierarchy)); + ExtendedCommands.addExtendedCommand(jc, new DumpCommand(commandHierarchy)); + ExtendedCommands.addExtendedCommand(jc, new HelpCommand(commandHierarchy)); + ExtendedCommands.addExtendedCommand(jc, new HlepCommand(commandHierarchy)); - ListCommand listCommand = new ListCommand(jc); - jc.addCommand("list", listCommand, "l"); + ListCommand listCommand = new ListCommand(commandHierarchy); + ExtendedCommands.addExtendedCommand(jc, listCommand); listCommand.registerSubCommands(); jc.parse(args); - if (jc.getParsedCommand() == null || main.help) { - jc.usage(); - return; - } - if (main.version) { version(); + } + + if (jc.getParsedCommand() == null || main.help) { + main.usage(); return; } diff --git a/smali/src/main/java/org/jf/smali/AssembleCommand.java b/smali/src/main/java/org/jf/smali/AssembleCommand.java index e3cf4a13..263c4cea 100644 --- a/smali/src/main/java/org/jf/smali/AssembleCommand.java +++ b/smali/src/main/java/org/jf/smali/AssembleCommand.java @@ -35,15 +35,19 @@ import com.beust.jcommander.JCommander; import com.beust.jcommander.Parameter; import com.beust.jcommander.Parameters; import com.beust.jcommander.validators.PositiveInteger; +import org.jf.util.jcommander.Command; +import org.jf.util.jcommander.ExtendedParameter; +import org.jf.util.jcommander.ExtendedParameters; import javax.annotation.Nonnull; import java.io.IOException; import java.util.List; @Parameters(commandDescription = "Assembles smali files into a dex file.") -public class AssembleCommand implements Command { - - @Nonnull private final JCommander jc; +@ExtendedParameters( + commandName = "assemble", + commandAliases = { "ass", "as", "a" }) +public class AssembleCommand extends Command { @Parameter(names = {"-h", "-?", "--help"}, help = true, description = "Show usage information for this command.") @@ -52,14 +56,17 @@ public class AssembleCommand implements Command { @Parameter(names = {"-j", "--jobs"}, description = "The number of threads to use. Defaults to the number of cores available.", validateWith = PositiveInteger.class) + @ExtendedParameter(argumentNames = "n") private int jobs = Runtime.getRuntime().availableProcessors(); @Parameter(names = {"-a", "--api"}, description = "The numeric api level to use while assembling.") + @ExtendedParameter(argumentNames = "api") private int apiLevel = 15; @Parameter(names = {"-o", "--output"}, - description = "The location of the dex file to write.") + description = "The name/path of the dex file to write.") + @ExtendedParameter(argumentNames = "file") private String output = "out.dex"; @Parameter(names = "--experimental", @@ -75,17 +82,18 @@ public class AssembleCommand implements Command { description = "Allows the odex opcodes that dalvik doesn't reject to be assembled.") private boolean allowOdexOpcodes; - @Parameter(description = "[|]+ - Assembles the given files. If a directory is specified, it will be " + - "recursively searched for any file with a .smali prefix") + @Parameter(description = "Assembles the given files. If a directory is specified, it will be " + + "recursively searched for any files with a .smali prefix") + @ExtendedParameter(argumentNames = "[|]+") private List input; - public AssembleCommand(@Nonnull JCommander jc) { - this.jc = jc; + public AssembleCommand(@Nonnull List commandAncestors) { + super(commandAncestors); } @Override public void run() { if (help || input == null || input.isEmpty()) { - jc.usage(jc.getParsedCommand()); + usage(); return; } diff --git a/smali/src/main/java/org/jf/smali/HelpCommand.java b/smali/src/main/java/org/jf/smali/HelpCommand.java index 28085a3c..429a7dfd 100644 --- a/smali/src/main/java/org/jf/smali/HelpCommand.java +++ b/smali/src/main/java/org/jf/smali/HelpCommand.java @@ -34,35 +34,59 @@ package org.jf.smali; import com.beust.jcommander.JCommander; import com.beust.jcommander.Parameter; import com.beust.jcommander.Parameters; +import org.jf.util.ConsoleUtil; +import org.jf.util.jcommander.*; import javax.annotation.Nonnull; import java.util.List; @Parameters(commandDescription = "Shows usage information") -public class HelpCommand implements Command { - @Nonnull private final JCommander jc; +@ExtendedParameters( + commandName = "help", + commandAliases = "h") +public class HelpCommand extends Command { - public HelpCommand(@Nonnull JCommander jc) { - this.jc = jc; - } - - @Parameter(description = "If specified, only show the usage information for the given commands") + @Parameter(description = "If specified, show the detailed usage information for the given commands") + @ExtendedParameter(argumentNames = "commands") private List commands; + public HelpCommand(@Nonnull List commandAncestors) { + super(commandAncestors); + } + public void run() { + JCommander parentJc = commandAncestors.get(commandAncestors.size() - 1); + if (commands == null || commands.isEmpty()) { - jc.usage(); + System.out.println(new HelpFormatter() + .width(ConsoleUtil.getConsoleWidth()) + .format(commandAncestors)); } else { + boolean printedHelp = false; for (String cmd : commands) { - jc.usage(cmd); + JCommander command = ExtendedCommands.getSubcommand(parentJc, cmd); + if (command == null) { + System.err.println("No such command: " + cmd); + } else { + printedHelp = true; + System.out.println(new HelpFormatter() + .width(ConsoleUtil.getConsoleWidth()) + .format(((Command)command.getObjects().get(0)).getCommandHierarchy())); + } + } + if (!printedHelp) { + System.out.println(new HelpFormatter() + .width(ConsoleUtil.getConsoleWidth()) + .format(commandAncestors)); } } } @Parameters(hidden = true) + @ExtendedParameters(commandName = "hlep") public static class HlepCommand extends HelpCommand { - public HlepCommand(@Nonnull JCommander jc) { - super(jc); + public HlepCommand(@Nonnull List commandAncestors) { + super(commandAncestors); } } } diff --git a/smali/src/main/java/org/jf/smali/Main.java b/smali/src/main/java/org/jf/smali/Main.java index 08a1b10e..6b56fddb 100644 --- a/smali/src/main/java/org/jf/smali/Main.java +++ b/smali/src/main/java/org/jf/smali/Main.java @@ -33,13 +33,22 @@ package org.jf.smali; import com.beust.jcommander.JCommander; import com.beust.jcommander.Parameter; +import com.google.common.collect.Lists; import org.jf.smali.HelpCommand.HlepCommand; +import org.jf.util.jcommander.Command; +import org.jf.util.jcommander.ExtendedCommands; +import org.jf.util.jcommander.ExtendedParameters; import java.io.IOException; import java.io.InputStream; +import java.util.List; import java.util.Properties; -public class Main { +@ExtendedParameters( + includeParametersInUsage = true, + commandName = "smali", + postfixDescription = "See smali help for more information about a specific command") +public class Main extends Command { public static final String VERSION = loadVersion(); @Parameter(names = {"-h", "-?", "--help"}, help = true, @@ -50,24 +59,39 @@ public class Main { description = "Print the version of baksmali and then exit") public boolean version; + private JCommander jc; + + @Override public void run() { + } + + @Override protected JCommander getJCommander() { + return jc; + } + + public Main() { + super(Lists.newArrayList()); + } + public static void main(String[] args) { Main main = new Main(); JCommander jc = new JCommander(main); + main.jc = jc; + jc.setProgramName("smali"); + List commandHierarchy = main.getCommandHierarchy(); - jc.addCommand("assemble", new AssembleCommand(jc), "a", "as"); - jc.addCommand("help", new HelpCommand(jc), "h"); - jc.addCommand("hlep", new HlepCommand(jc)); + ExtendedCommands.addExtendedCommand(jc, new AssembleCommand(commandHierarchy)); + ExtendedCommands.addExtendedCommand(jc, new HelpCommand(commandHierarchy)); + ExtendedCommands.addExtendedCommand(jc, new HlepCommand(commandHierarchy)); jc.parse(args); - if (jc.getParsedCommand() == null || main.help) { - jc.usage(); - return; - } - if (main.version) { version(); + } + + if (jc.getParsedCommand() == null || main.help) { + main.usage(); return; } diff --git a/util/src/main/java/org/jf/util/OldWrappedIndentingWriter.java b/util/src/main/java/org/jf/util/OldWrappedIndentingWriter.java new file mode 100644 index 00000000..f4577179 --- /dev/null +++ b/util/src/main/java/org/jf/util/OldWrappedIndentingWriter.java @@ -0,0 +1,184 @@ +/* + * Copyright 2013, Google Inc. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * * Neither the name of Google Inc. nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.jf.util; + +import java.io.FilterWriter; +import java.io.IOException; +import java.io.Writer; + +/** + * Writer that wraps another writer and passes width-limited and + * optionally-prefixed output to its subordinate. When lines are + * wrapped they are automatically indented based on the start of the + * line. + */ +public final class OldWrappedIndentingWriter extends FilterWriter { + /** null-ok; optional prefix for every line */ + private final String prefix; + + /** > 0; the maximum output width */ + private final int width; + + /** > 0; the maximum indent */ + private final int maxIndent; + + /** >= 0; current output column (zero-based) */ + private int column; + + /** whether indent spaces are currently being collected */ + private boolean collectingIndent; + + /** >= 0; current indent amount */ + private int indent; + + /** + * Constructs an instance. + * + * @param out non-null; writer to send final output to + * @param width >= 0; the maximum output width (not including + * prefix), or 0 for no maximum + * @param prefix non-null; the prefix for each line + */ + public OldWrappedIndentingWriter(Writer out, int width, String prefix) { + super(out); + + if (out == null) { + throw new NullPointerException("out == null"); + } + + if (width < 0) { + throw new IllegalArgumentException("width < 0"); + } + + if (prefix == null) { + throw new NullPointerException("prefix == null"); + } + + this.width = (width != 0) ? width : Integer.MAX_VALUE; + this.maxIndent = width >> 1; + this.prefix = (prefix.length() == 0) ? null : prefix; + + bol(); + } + + /** + * Constructs a no-prefix instance. + * + * @param out non-null; writer to send final output to + * @param width >= 0; the maximum output width (not including + * prefix), or 0 for no maximum + */ + public OldWrappedIndentingWriter(Writer out, int width) { + this(out, width, ""); + } + + /** {@inheritDoc} */ + @Override + public void write(int c) throws IOException { + synchronized (lock) { + if (collectingIndent) { + if (c == ' ') { + indent++; + if (indent >= maxIndent) { + indent = maxIndent; + collectingIndent = false; + } + } else { + collectingIndent = false; + } + } + + if ((column == width) && (c != '\n')) { + out.write('\n'); + column = 0; + /* + * Note: No else, so this should fall through to the next + * if statement. + */ + } + + if (column == 0) { + if (prefix != null) { + out.write(prefix); + } + + if (!collectingIndent) { + for (int i = 0; i < indent; i++) { + out.write(' '); + } + column = indent; + } + } + + out.write(c); + + if (c == '\n') { + bol(); + } else { + column++; + } + } + } + + /** {@inheritDoc} */ + @Override + public void write(char[] cbuf, int off, int len) throws IOException { + synchronized (lock) { + while (len > 0) { + write(cbuf[off]); + off++; + len--; + } + } + } + + /** {@inheritDoc} */ + @Override + public void write(String str, int off, int len) throws IOException { + synchronized (lock) { + while (len > 0) { + write(str.charAt(off)); + off++; + len--; + } + } + } + + /** + * Indicates that output is at the beginning of a line. + */ + private void bol() { + column = 0; + collectingIndent = (maxIndent != 0); + indent = 0; + } +} diff --git a/util/src/main/java/org/jf/util/WrappedIndentingWriter.java b/util/src/main/java/org/jf/util/WrappedIndentingWriter.java index eb1acdae..df4575bd 100644 --- a/util/src/main/java/org/jf/util/WrappedIndentingWriter.java +++ b/util/src/main/java/org/jf/util/WrappedIndentingWriter.java @@ -1,18 +1,18 @@ /* - * Copyright 2013, Google Inc. + * Copyright 2016, Google Inc. * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: * - * * Redistributions of source code must retain the above copyright + * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. - * * Redistributions in binary form must reproduce the above + * Redistributions in binary form must reproduce the above * copyright notice, this list of conditions and the following disclaimer * in the documentation and/or other materials provided with the * distribution. - * * Neither the name of Google Inc. nor the names of its + * Neither the name of Google Inc. nor the names of its * contributors may be used to endorse or promote products derived from * this software without specific prior written permission. * @@ -31,154 +31,94 @@ package org.jf.util; +import com.google.common.collect.Lists; + import java.io.FilterWriter; import java.io.IOException; import java.io.Writer; +import java.util.List; -/** - * Writer that wraps another writer and passes width-limited and - * optionally-prefixed output to its subordinate. When lines are - * wrapped they are automatically indented based on the start of the - * line. - */ -public final class WrappedIndentingWriter extends FilterWriter { - /** null-ok; optional prefix for every line */ - private final String prefix; +public class WrappedIndentingWriter extends FilterWriter { - /** > 0; the maximum output width */ - private final int width; - - /** > 0; the maximum indent */ private final int maxIndent; + private final int maxWidth; - /** >= 0; current output column (zero-based) */ - private int column; + private int currentIndent = 0; + private final StringBuilder line = new StringBuilder(); - /** whether indent spaces are currently being collected */ - private boolean collectingIndent; - - /** >= 0; current indent amount */ - private int indent; - - /** - * Constructs an instance. - * - * @param out non-null; writer to send final output to - * @param width >= 0; the maximum output width (not including - * prefix), or 0 for no maximum - * @param prefix non-null; the prefix for each line - */ - public WrappedIndentingWriter(Writer out, int width, String prefix) { + public WrappedIndentingWriter(Writer out, int maxIndent, int maxWidth) { super(out); - - if (out == null) { - throw new NullPointerException("out == null"); - } - - if (width < 0) { - throw new IllegalArgumentException("width < 0"); - } - - if (prefix == null) { - throw new NullPointerException("prefix == null"); - } - - this.width = (width != 0) ? width : Integer.MAX_VALUE; - this.maxIndent = width >> 1; - this.prefix = (prefix.length() == 0) ? null : prefix; - - bol(); + this.maxIndent = maxIndent; + this.maxWidth = maxWidth; } - /** - * Constructs a no-prefix instance. - * - * @param out non-null; writer to send final output to - * @param width >= 0; the maximum output width (not including - * prefix), or 0 for no maximum - */ - public WrappedIndentingWriter(Writer out, int width) { - this(out, width, ""); + private void writeIndent() throws IOException { + for (int i=0; i= maxIndent) { - indent = maxIndent; - collectingIndent = false; - } - } else { - collectingIndent = false; - } - } - - if ((column == width) && (c != '\n')) { - out.write('\n'); - column = 0; - /* - * Note: No else, so this should fall through to the next - * if statement. - */ - } - - if (column == 0) { - if (prefix != null) { - out.write(prefix); - } - - if (!collectingIndent) { - for (int i = 0; i < indent; i++) { - out.write(' '); - } - column = indent; - } + private int getIndent() { + if (currentIndent < 0) { + return 0; + } + if (currentIndent > maxIndent) { + return maxIndent; + } + return currentIndent; + } + + public void indent(int indent) { + currentIndent += indent; + } + + public void deindent(int indent) { + currentIndent -= indent; + } + + private void wrapLine() throws IOException { + List wrapped = Lists.newArrayList(StringWrapper.wrapStringOnBreaks(line.toString(), maxWidth)); + out.write(wrapped.get(0), 0, wrapped.get(0).length()); + out.write('\n'); + + line.replace(0, line.length(), ""); + writeIndent(); + for (int i=1; i 1) { + write('\n'); } + write(wrapped.get(i)); + } + } + @Override public void write(int c) throws IOException { + if (c == '\n') { + out.write(line.toString()); out.write(c); - - if (c == '\n') { - bol(); - } else { - column++; + line.replace(0, line.length(), ""); + writeIndent(); + } else { + line.append((char)c); + if (line.length() > maxWidth) { + wrapLine(); } } } - /** {@inheritDoc} */ - @Override - public void write(char[] cbuf, int off, int len) throws IOException { - synchronized (lock) { - while (len > 0) { - write(cbuf[off]); - off++; - len--; - } + @Override public void write(char[] cbuf, int off, int len) throws IOException { + for (int i=0; i 0) { - write(str.charAt(off)); - off++; - len--; - } + @Override public void write(String str, int off, int len) throws IOException { + for (int i=0; i commandAncestors; + + public Command(@Nonnull List commandAncestors) { + this.commandAncestors = commandAncestors; + } + + public void usage() { + System.out.println(new HelpFormatter() + .width(ConsoleUtil.getConsoleWidth()) + .format(getCommandHierarchy())); + } + + protected JCommander getJCommander() { + JCommander parentJc = Iterables.getLast(commandAncestors); + return parentJc.getCommands().get(this.getClass().getAnnotation(ExtendedParameters.class).commandName()); + } + + public List getCommandHierarchy() { + List commandHierarchy = Lists.newArrayList(commandAncestors); + commandHierarchy.add(getJCommander()); + return commandHierarchy; + } + + public abstract void run(); +} diff --git a/util/src/main/java/org/jf/util/jcommander/ExtendedCommands.java b/util/src/main/java/org/jf/util/jcommander/ExtendedCommands.java new file mode 100644 index 00000000..523bedeb --- /dev/null +++ b/util/src/main/java/org/jf/util/jcommander/ExtendedCommands.java @@ -0,0 +1,148 @@ +/* + * Copyright 2016, Google Inc. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * Neither the name of Google Inc. nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.jf.util.jcommander; + +import com.beust.jcommander.JCommander; +import com.beust.jcommander.Parameterized; +import com.beust.jcommander.Parameters; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.lang.reflect.Field; + +/** + * Utilities related to "extended" commands - JCommander commands with additional information + */ +public class ExtendedCommands { + + @Nonnull + private static ExtendedParameters getExtendedParameters(Object command) { + ExtendedParameters anno = command.getClass().getAnnotation(ExtendedParameters.class); + if (anno == null) { + throw new IllegalStateException("All extended commands should have an ExtendedParameters annotation: " + + command.getClass().getCanonicalName()); + } + return anno; + } + + @Nonnull + public static String commandName(JCommander jc) { + return getExtendedParameters(jc.getObjects().get(0)).commandName(); + } + + @Nonnull + public static String commandName(Object command) { + return getExtendedParameters(command).commandName(); + } + + @Nonnull + public static String[] commandAliases(JCommander jc) { + return commandAliases(jc.getObjects().get(0)); + } + + @Nonnull + public static String[] commandAliases(Object command) { + return getExtendedParameters(command).commandAliases(); + } + + public static boolean includeParametersInUsage(JCommander jc) { + return includeParametersInUsage(jc.getObjects().get(0)); + } + + public static boolean includeParametersInUsage(Object command) { + return getExtendedParameters(command).includeParametersInUsage(); + } + + @Nonnull + public static String postfixDescription(JCommander jc) { + return postfixDescription(jc.getObjects().get(0)); + } + + @Nonnull + public static String postfixDescription(Object command) { + return getExtendedParameters(command).postfixDescription(); + } + + public static void addExtendedCommand(JCommander jc, Object command) { + jc.addCommand(commandName(command), command, commandAliases(command)); + } + + @Nonnull + public static String[] parameterArgumentNames(JCommander jc, Parameterized parameterized) { + // TODO: this won't work if we're using additional objects to collect parameters + + Class cls = jc.getObjects().get(0).getClass(); + Field field = null; + while (cls != Object.class) { + try { + field = cls.getDeclaredField(parameterized.getName()); + } catch (NoSuchFieldException ex) { + cls = cls.getSuperclass(); + continue; + } + break; + } + + assert field != null; + ExtendedParameter extendedParameter = field.getAnnotation(ExtendedParameter.class); + if (extendedParameter != null) { + return extendedParameter.argumentNames(); + } + + return new String[0]; + } + + @Nullable + public static JCommander getSubcommand(JCommander jc, String commandName) { + if (jc.getCommands().containsKey(commandName)) { + return jc.getCommands().get(commandName); + } else { + for (JCommander command : jc.getCommands().values()) { + for (String alias: commandAliases(command)) { + if (commandName.equals(alias)) { + return command; + } + } + } + } + return null; + } + + @Nullable + public static String getCommandDescription(@Nonnull JCommander jc) { + Parameters parameters = jc.getObjects().get(0).getClass().getAnnotation(Parameters.class); + if (parameters == null) { + return null; + } + return parameters.commandDescription(); + } +} diff --git a/baksmali/src/main/java/org/jf/baksmali/Command.java b/util/src/main/java/org/jf/util/jcommander/ExtendedParameter.java similarity index 77% rename from baksmali/src/main/java/org/jf/baksmali/Command.java rename to util/src/main/java/org/jf/util/jcommander/ExtendedParameter.java index aec139d4..81f78c22 100644 --- a/baksmali/src/main/java/org/jf/baksmali/Command.java +++ b/util/src/main/java/org/jf/util/jcommander/ExtendedParameter.java @@ -6,13 +6,13 @@ * modification, are permitted provided that the following conditions are * met: * - * * Redistributions of source code must retain the above copyright + * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. - * * Redistributions in binary form must reproduce the above + * Redistributions in binary form must reproduce the above * copyright notice, this list of conditions and the following disclaimer * in the documentation and/or other materials provided with the * distribution. - * * Neither the name of Google Inc. nor the names of its + * Neither the name of Google Inc. nor the names of its * contributors may be used to endorse or promote products derived from * this software without specific prior written permission. * @@ -29,8 +29,12 @@ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ -package org.jf.baksmali; +package org.jf.util.jcommander; -public interface Command { - void run(); +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +@Retention(RetentionPolicy.RUNTIME) +public @interface ExtendedParameter { + String[] argumentNames(); } diff --git a/smali/src/main/java/org/jf/smali/Command.java b/util/src/main/java/org/jf/util/jcommander/ExtendedParameters.java similarity index 71% rename from smali/src/main/java/org/jf/smali/Command.java rename to util/src/main/java/org/jf/util/jcommander/ExtendedParameters.java index c3af4d16..965d2b2a 100644 --- a/smali/src/main/java/org/jf/smali/Command.java +++ b/util/src/main/java/org/jf/util/jcommander/ExtendedParameters.java @@ -6,13 +6,13 @@ * modification, are permitted provided that the following conditions are * met: * - * * Redistributions of source code must retain the above copyright + * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. - * * Redistributions in binary form must reproduce the above + * Redistributions in binary form must reproduce the above * copyright notice, this list of conditions and the following disclaimer * in the documentation and/or other materials provided with the * distribution. - * * Neither the name of Google Inc. nor the names of its + * Neither the name of Google Inc. nor the names of its * contributors may be used to endorse or promote products derived from * this software without specific prior written permission. * @@ -29,8 +29,15 @@ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ -package org.jf.smali; +package org.jf.util.jcommander; -public interface Command { - void run(); -} \ No newline at end of file +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +@Retention(RetentionPolicy.RUNTIME) +public @interface ExtendedParameters { + boolean includeParametersInUsage() default false; + String commandName(); + String[] commandAliases() default { }; + String postfixDescription() default ""; +} diff --git a/util/src/main/java/org/jf/util/jcommander/HelpFormatter.java b/util/src/main/java/org/jf/util/jcommander/HelpFormatter.java new file mode 100644 index 00000000..367094c7 --- /dev/null +++ b/util/src/main/java/org/jf/util/jcommander/HelpFormatter.java @@ -0,0 +1,319 @@ +/* + * Copyright 2016, Google Inc. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * Neither the name of Google Inc. nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.jf.util.jcommander; + +import com.beust.jcommander.JCommander; +import com.beust.jcommander.ParameterDescription; +import com.beust.jcommander.Parameters; +import com.beust.jcommander.internal.Lists; +import com.google.common.base.Joiner; +import com.google.common.collect.Iterables; +import org.jf.util.WrappedIndentingWriter; + +import javax.annotation.Nonnull; +import java.io.IOException; +import java.io.StringWriter; +import java.util.Arrays; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.Map.Entry; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class HelpFormatter { + + private int width = 80; + + @Nonnull + public HelpFormatter width(int width) { + this.width = width; + return this; + } + + @Nonnull + private static ExtendedParameters getExtendedParameters(JCommander jc) { + ExtendedParameters anno = jc.getObjects().get(0).getClass().getAnnotation(ExtendedParameters.class); + if (anno == null) { + throw new IllegalStateException("All commands should have an ExtendedParameters annotation"); + } + return anno; + } + + @Nonnull + private static List getCommandAliases(JCommander jc) { + return Lists.newArrayList(getExtendedParameters(jc).commandAliases()); + } + + private static boolean includeParametersInUsage(@Nonnull JCommander jc) { + return getExtendedParameters(jc).includeParametersInUsage(); + } + + @Nonnull + private static String getPostfixDescription(@Nonnull JCommander jc) { + return getExtendedParameters(jc).postfixDescription(); + } + + private int getParameterArity(ParameterDescription param) { + if (param.getParameter().arity() > 0) { + return param.getParameter().arity(); + } + Class type = param.getParameterized().getType(); + if ((type == boolean.class || type == Boolean.class)) { + return 0; + } + return 1; + } + + private List getSortedParameters(JCommander jc) { + List parameters = Lists.newArrayList(jc.getParameters()); + + final Pattern pattern = Pattern.compile("^-*(.*)$"); + + Collections.sort(parameters, new Comparator() { + @Override public int compare(ParameterDescription o1, ParameterDescription o2) { + String s1; + Matcher matcher = pattern.matcher(o1.getParameter().names()[0]); + if (matcher.matches()) { + s1 = matcher.group(1); + } else { + throw new IllegalStateException(); + } + + String s2; + matcher = pattern.matcher(o2.getParameter().names()[0]); + if (matcher.matches()) { + s2 = matcher.group(1); + } else { + throw new IllegalStateException(); + } + + return s1.compareTo(s2); + } + }); + return parameters; + } + + @Nonnull + public String format(@Nonnull JCommander... jc) { + return format(Arrays.asList(jc)); + } + + @Nonnull + public String format(@Nonnull List commandHierarchy) { + try { + StringWriter stringWriter = new StringWriter(); + WrappedIndentingWriter writer = new WrappedIndentingWriter(stringWriter, width - 5, width); + + JCommander leafJc = Iterables.getLast(commandHierarchy); + + writer.write("usage:"); + writer.indent(2); + + for (JCommander jc: commandHierarchy) { + writer.write(" "); + writer.write(ExtendedCommands.commandName(jc)); + } + + if (includeParametersInUsage(leafJc)) { + for (ParameterDescription param : leafJc.getParameters()) { + if (!param.getParameter().hidden()) { + writer.write(" ["); + writer.write(param.getParameter().getParameter().names()[0]); + writer.write("]"); + } + } + } else { + if (!leafJc.getParameters().isEmpty()) { + writer.write(" []"); + } + } + + if (!leafJc.getCommands().isEmpty()) { + writer.write(" []]"); + } + + if (leafJc.getMainParameter() != null) { + String[] argumentNames = ExtendedCommands.parameterArgumentNames( + leafJc, leafJc.getMainParameter().getParameterized()); + if (argumentNames.length == 0) { + writer.write(" "); + } else { + String argumentName = argumentNames[0]; + boolean writeAngleBrackets = !argumentName.startsWith("<") && !argumentName.startsWith("["); + writer.write(" "); + if (writeAngleBrackets) { + writer.write("<"); + } + writer.write(argumentNames[0]); + if (writeAngleBrackets) { + writer.write(">"); + } + } + } + + writer.deindent(2); + + String commandDescription = ExtendedCommands.getCommandDescription(leafJc); + if (commandDescription != null) { + writer.write("\n"); + writer.write(commandDescription); + } + + if (!leafJc.getParameters().isEmpty() || leafJc.getMainParameter() != null) { + writer.write("\n\nOptions:"); + writer.indent(2); + for (ParameterDescription param : getSortedParameters(leafJc)) { + if (!param.getParameter().hidden()) { + writer.write("\n"); + writer.indent(4); + if (!param.getNames().isEmpty()) { + writer.write(Joiner.on(',').join(param.getParameter().names())); + } + if (getParameterArity(param) > 0) { + String[] argumentNames = ExtendedCommands.parameterArgumentNames( + leafJc, param.getParameterized()); + for (int i = 0; i < getParameterArity(param); i++) { + writer.write(" "); + if (i < argumentNames.length) { + writer.write("<"); + writer.write(argumentNames[i]); + writer.write(">"); + } else { + writer.write(""); + } + } + } + if (param.getDescription() != null && !param.getDescription().isEmpty()) { + writer.write(" - "); + writer.write(param.getDescription()); + } + if (param.getDefault() != null) { + String defaultValue = null; + if (param.getParameterized().getType() == Boolean.class || + param.getParameterized().getType() == Boolean.TYPE) { + if ((Boolean)param.getDefault()) { + defaultValue = "True"; + } + } else if (List.class.isAssignableFrom(param.getParameterized().getType())) { + if (!((List)param.getDefault()).isEmpty()) { + defaultValue = param.getDefault().toString(); + } + } else { + defaultValue = param.getDefault().toString(); + } + if (defaultValue != null) { + writer.write(" (default: "); + writer.write(defaultValue); + writer.write(")"); + } + } + writer.deindent(4); + } + } + + if (leafJc.getMainParameter() != null) { + String[] argumentNames = ExtendedCommands.parameterArgumentNames(leafJc, + leafJc.getMainParameter().getParameterized()); + writer.write("\n"); + writer.indent(4); + if (argumentNames.length > 0) { + writer.write("<"); + writer.write(argumentNames[0]); + writer.write(">"); + } else { + writer.write(""); + } + + if (leafJc.getMainParameterDescription() != null) { + writer.write(" - "); + writer.write(leafJc.getMainParameterDescription()); + } + writer.deindent(4); + } + writer.deindent(2); + } + + if (!leafJc.getCommands().isEmpty()) { + writer.write("\n\nCommands:"); + writer.indent(2); + + + List> entryList = Lists.newArrayList(leafJc.getCommands().entrySet()); + Collections.sort(entryList, new Comparator>() { + @Override public int compare(Entry o1, Entry o2) { + return o1.getKey().compareTo(o2.getKey()); + } + }); + + for (Entry entry : entryList) { + String commandName = entry.getKey(); + JCommander command = entry.getValue(); + + Object arg = command.getObjects().get(0); + Parameters parametersAnno = arg.getClass().getAnnotation(Parameters.class); + if (!parametersAnno.hidden()) { + writer.write("\n"); + writer.indent(4); + writer.write(commandName); + List aliases = getCommandAliases(command); + if (!aliases.isEmpty()) { + writer.write("("); + writer.write(Joiner.on(',').join(aliases)); + writer.write(")"); + } + + String commandDesc = leafJc.getCommandDescription(commandName); + if (commandDesc != null) { + writer.write(" - "); + writer.write(commandDesc); + } + writer.deindent(4); + } + } + writer.deindent(2); + } + + String postfixDescription = getPostfixDescription(leafJc); + if (!postfixDescription.isEmpty()) { + writer.write("\n\n"); + writer.write(postfixDescription); + } + + writer.flush(); + + return stringWriter.getBuffer().toString(); + } catch (IOException ex) { + throw new RuntimeException(ex); + } + } +}