diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/util/headless/AnalyzeHeadless.java b/Ghidra/Features/Base/src/main/java/ghidra/app/util/headless/AnalyzeHeadless.java
index b60925091e..5cd2db6daf 100644
--- a/Ghidra/Features/Base/src/main/java/ghidra/app/util/headless/AnalyzeHeadless.java
+++ b/Ghidra/Features/Base/src/main/java/ghidra/app/util/headless/AnalyzeHeadless.java
@@ -17,9 +17,9 @@ package ghidra.app.util.headless;
import java.io.File;
import java.io.IOException;
-import java.net.MalformedURLException;
-import java.net.URL;
+import java.net.*;
import java.util.*;
+import java.util.stream.Collectors;
import generic.stl.Pair;
import ghidra.*;
@@ -37,7 +37,76 @@ import ghidra.util.exception.InvalidInputException;
*/
public class AnalyzeHeadless implements GhidraLaunchable {
+ /**
+ * Headless command line arguments.
+ *
+ * NOTE: Please update 'analyzeHeadlessREADME.html' if changing command line parameters
+ */
+ private enum Arg {
+ //@formatter:off
+ IMPORT("-import", true, "[|]+"),
+ PROCESS("-process", true, "[]"),
+ PRE_SCRIPT("-prescript", true, ""),
+ POST_SCRIPT("-postscript", true, ""),
+ SCRIPT_PATH("-scriptPath", true, "\"[;...]\""),
+ PROPERTIES_PATH("-propertiesPath", true, "\"[;...]\""),
+ SCRIPT_LOG("-scriptlog", true, ""),
+ LOG("-log", true, ""),
+ OVERWRITE("-overwrite", false),
+ RECURSIVE("-recursive", false),
+ READ_ONLY("-readOnly", false),
+ DELETE_PROJECT("-deleteproject", false),
+ NO_ANALYSIS("-noanalysis", false),
+ PROCESSOR("-processor", true, ""),
+ CSPEC("-cspec", true, ""),
+ ANALYSIS_TIMEOUT_PER_FILE("-analysisTimeoutPerFile", true, ""),
+ KEYSTORE("-keystore", true, ""),
+ CONNECT("-connect", false, "[]"),
+ PASSWORD("-p", false),
+ COMMIT("-commit", false, "[\"\"]]"),
+ OK_TO_DELETE("-okToDelete", false),
+ MAX_CPU("-max-cpu", true, ""),
+ LIBRARY_SEARCH_PATHS("-librarySearchPaths", true, "[;...]"),
+ LOADER("-loader", true, ""),
+ LOADER_ARGS(Loader.COMMAND_LINE_ARG_PREFIX + "-", true, "") {
+ @Override
+ public boolean matches(String arg) {
+ return arg.startsWith(Loader.COMMAND_LINE_ARG_PREFIX + "-");
+ }
+ };
+ //@formatter:on
+
+ private String name;
+ private boolean requiresSubArgs;
+ private String subArgFormat;
+
+ private Arg(String name, boolean requiresSubArgs, String subArgFormat) {
+ this.name = name;
+ this.requiresSubArgs = requiresSubArgs;
+ this.subArgFormat = subArgFormat;
+ }
+
+ private Arg(String name, boolean requiresSubArgs) {
+ this(name, requiresSubArgs, "");
+ }
+
+ public String usage() {
+ return "%s%s%s".formatted(name, subArgFormat.isEmpty() ? "" : " ", subArgFormat);
+ }
+
+ public boolean matches(String arg) {
+ return arg.equalsIgnoreCase(name);
+ }
+
+ @Override
+ public String toString() {
+ return name;
+ }
+ }
+
private static final int EXIT_CODE_ERROR = 1;
+ private static final Set ALL_ARG_NAMES =
+ Arrays.stream(Arg.values()).map(a -> a.name).collect(Collectors.toSet());
/**
* The entry point of 'analyzeHeadless.bat'. Parses the command line arguments to the script
@@ -64,7 +133,7 @@ public class AnalyzeHeadless implements GhidraLaunchable {
if (args[0].startsWith("ghidra:")) {
optionStartIndex = 1;
try {
- ghidraURL = new URL(args[0]);
+ ghidraURL = new URI(args[0]).toURL();
}
catch (MalformedURLException e) {
System.err.println("Invalid Ghidra URL: " + args[0]);
@@ -98,10 +167,10 @@ public class AnalyzeHeadless implements GhidraLaunchable {
File logFile = null;
File scriptLogFile = null;
for (int argi = optionStartIndex; argi < args.length; argi++) {
- if (checkArgument("-log", args, argi)) {
+ if (checkArgument(Arg.LOG, args, argi)) {
logFile = new File(args[++argi]);
}
- else if (checkArgument("-scriptlog", args, argi)) {
+ else if (checkArgument(Arg.SCRIPT_LOG, args, argi)) {
scriptLogFile = new File(args[++argi]);
}
}
@@ -158,7 +227,7 @@ public class AnalyzeHeadless implements GhidraLaunchable {
String languageId = null;
String compilerSpecId = null;
String keystorePath = null;
- String serverUID = null;
+ String userId = null;
boolean allowPasswordPrompt = false;
List> preScripts = new LinkedList<>();
List> postScripts = new LinkedList<>();
@@ -166,57 +235,57 @@ public class AnalyzeHeadless implements GhidraLaunchable {
for (int argi = startIndex; argi < args.length; argi++) {
String arg = args[argi];
- if (checkArgument("-log", args, argi)) {
+ if (checkArgument(Arg.LOG, args, argi)) {
// Already processed
argi++;
}
- else if (checkArgument("-scriptlog", args, argi)) {
+ else if (checkArgument(Arg.SCRIPT_LOG, args, argi)) {
// Already processed
argi++;
}
- else if (arg.equalsIgnoreCase("-overwrite")) {
+ else if (checkArgument(Arg.OVERWRITE, args, argi)) {
options.enableOverwriteOnConflict(true);
}
- else if (arg.equalsIgnoreCase("-noanalysis")) {
+ else if (checkArgument(Arg.NO_ANALYSIS, args, argi)) {
options.enableAnalysis(false);
}
- else if (arg.equalsIgnoreCase("-deleteproject")) {
+ else if (checkArgument(Arg.DELETE_PROJECT, args, argi)) {
options.setDeleteCreatedProjectOnClose(true);
}
- else if (checkArgument("-loader", args, argi)) {
+ else if (checkArgument(Arg.LOADER, args, argi)) {
loaderName = args[++argi];
}
- else if (arg.startsWith(Loader.COMMAND_LINE_ARG_PREFIX)) {
- if (args[argi + 1].startsWith("-")) {
+ else if (checkArgument(Arg.LOADER_ARGS, args, argi)) {
+ if (ALL_ARG_NAMES.contains(args[argi + 1])) {
throw new InvalidInputException(args[argi] + " expects value to follow.");
}
loaderArgs.add(new Pair<>(arg, args[++argi]));
}
- else if (checkArgument("-processor", args, argi)) {
+ else if (checkArgument(Arg.PROCESSOR, args, argi)) {
languageId = args[++argi];
}
- else if (checkArgument("-cspec", args, argi)) {
+ else if (checkArgument(Arg.CSPEC, args, argi)) {
compilerSpecId = args[++argi];
}
- else if (checkArgument("-prescript", args, argi)) {
+ else if (checkArgument(Arg.PRE_SCRIPT, args, argi)) {
String scriptName = args[++argi];
- String[] scriptArgs = getSubArguments(args, argi);
+ String[] scriptArgs = getSubArguments(args, argi, ALL_ARG_NAMES);
argi += scriptArgs.length;
preScripts.add(new Pair<>(scriptName, scriptArgs));
}
- else if (checkArgument("-postscript", args, argi)) {
+ else if (checkArgument(Arg.POST_SCRIPT, args, argi)) {
String scriptName = args[++argi];
- String[] scriptArgs = getSubArguments(args, argi);
+ String[] scriptArgs = getSubArguments(args, argi, ALL_ARG_NAMES);
argi += scriptArgs.length;
postScripts.add(new Pair<>(scriptName, scriptArgs));
}
- else if (checkArgument("-scriptPath", args, argi)) {
+ else if (checkArgument(Arg.SCRIPT_PATH, args, argi)) {
options.setScriptDirectories(args[++argi]);
}
- else if (checkArgument("-propertiesPath", args, argi)) {
+ else if (checkArgument(Arg.PROPERTIES_PATH, args, argi)) {
options.setPropertiesFileDirectories(args[++argi]);
}
- else if (checkArgument("-import", args, argi)) {
+ else if (checkArgument(Arg.IMPORT, args, argi)) {
File inputFile = null;
try {
inputFile = new File(args[++argi]);
@@ -242,7 +311,7 @@ public class AnalyzeHeadless implements GhidraLaunchable {
nextArg = args[++argi];
// Check if next argument is a parameter
- if (nextArg.charAt(0) == '-') {
+ if (ALL_ARG_NAMES.contains(nextArg)) {
argi--;
break;
}
@@ -258,29 +327,29 @@ public class AnalyzeHeadless implements GhidraLaunchable {
filesToImport.add(otherFile);
}
}
- else if ("-connect".equals(args[argi])) {
+ else if (checkArgument(Arg.CONNECT, args, argi)) {
if ((argi + 1) < args.length) {
arg = args[argi + 1];
- if (!arg.startsWith("-")) {
+ if (!ALL_ARG_NAMES.contains(arg)) {
// serverUID is optional argument after -connect
- serverUID = arg;
+ userId = arg;
++argi;
}
}
}
- else if ("-commit".equals(args[argi])) {
+ else if (checkArgument(Arg.COMMIT, args, argi)) {
String comment = null;
if ((argi + 1) < args.length) {
arg = args[argi + 1];
- if (!arg.startsWith("-")) {
- // comment is optional argument after -commit
+ if (!ALL_ARG_NAMES.contains(arg)) {
+ // commit is optional argument after -commit
comment = arg;
++argi;
}
}
options.setCommitFiles(true, comment);
}
- else if (checkArgument("-keystore", args, argi)) {
+ else if (checkArgument(Arg.KEYSTORE, args, argi)) {
keystorePath = args[++argi];
File keystore = new File(keystorePath);
if (!keystore.isFile()) {
@@ -288,13 +357,13 @@ public class AnalyzeHeadless implements GhidraLaunchable {
keystore.getAbsolutePath() + " is not a valid keystore file.");
}
}
- else if (arg.equalsIgnoreCase("-p")) {
+ else if (checkArgument(Arg.PASSWORD, args, argi)) {
allowPasswordPrompt = true;
}
- else if ("-analysisTimeoutPerFile".equalsIgnoreCase(args[argi])) {
+ else if (checkArgument(Arg.ANALYSIS_TIMEOUT_PER_FILE, args, argi)) {
options.setPerFileAnalysisTimeout(args[++argi]);
}
- else if ("-process".equals(args[argi])) {
+ else if (checkArgument(Arg.PROCESS, args, argi)) {
if (options.runScriptsNoImport) {
throw new InvalidInputException(
"The -process option may only be specified once.");
@@ -302,7 +371,7 @@ public class AnalyzeHeadless implements GhidraLaunchable {
String processBinary = null;
if ((argi + 1) < args.length) {
arg = args[argi + 1];
- if (!arg.startsWith("-")) {
+ if (!ALL_ARG_NAMES.contains(arg)) {
// processBinary is optional argument after -process
processBinary = arg;
++argi;
@@ -310,11 +379,11 @@ public class AnalyzeHeadless implements GhidraLaunchable {
}
options.setRunScriptsNoImport(true, processBinary);
}
- else if ("-recursive".equals(args[argi])) {
+ else if (checkArgument(Arg.RECURSIVE, args, argi)) {
Integer depth = null;
if ((argi + 1) < args.length) {
arg = args[argi + 1];
- if (!arg.startsWith("-")) {
+ if (!ALL_ARG_NAMES.contains(arg)) {
// depth is optional argument after -recursive
try {
depth = Integer.parseInt(arg);
@@ -327,10 +396,10 @@ public class AnalyzeHeadless implements GhidraLaunchable {
}
options.enableRecursiveProcessing(true, depth);
}
- else if ("-readOnly".equalsIgnoreCase(args[argi])) {
+ else if (checkArgument(Arg.READ_ONLY, args, argi)) {
options.enableReadOnlyProcessing(true);
}
- else if (checkArgument("-max-cpu", args, argi)) {
+ else if (checkArgument(Arg.MAX_CPU, args, argi)) {
String cpuVal = args[++argi];
try {
options.setMaxCpu(Integer.parseInt(cpuVal));
@@ -339,12 +408,15 @@ public class AnalyzeHeadless implements GhidraLaunchable {
throw new InvalidInputException("Invalid value for max-cpu: " + cpuVal);
}
}
- else if ("-okToDelete".equalsIgnoreCase(args[argi])) {
+ else if (checkArgument(Arg.OK_TO_DELETE, args, argi)) {
options.setOkToDelete(true);
}
- else if (checkArgument("-librarySearchPaths", args, argi)) {
+ else if (checkArgument(Arg.LIBRARY_SEARCH_PATHS, args, argi)) {
LibrarySearchPathManager.setLibraryPaths(args[++argi].split(";"));
}
+ else if (ALL_ARG_NAMES.contains(args[argi])) {
+ throw new AssertionError("Valid option was not processed: " + args[argi]);
+ }
else {
throw new InvalidInputException("Bad argument: " + arg);
}
@@ -362,7 +434,7 @@ public class AnalyzeHeadless implements GhidraLaunchable {
// Set up optional Ghidra Server authenticator
try {
- options.setClientCredentials(serverUID, keystorePath, allowPasswordPrompt);
+ options.setClientCredentials(userId, keystorePath, allowPasswordPrompt);
}
catch (IOException e) {
throw new InvalidInputException(
@@ -438,47 +510,48 @@ public class AnalyzeHeadless implements GhidraLaunchable {
* @param execCmd the command used to run the headless analyzer from the calling method.
*/
public static void usage(String execCmd) {
- System.out.println("Headless Analyzer Usage: " + execCmd);
- System.out.println(" [/]");
- System.out.println(
- " | ghidra://[:]/[/]");
- System.out.println(
- " [[-import [|]+] | [-process []]]");
- System.out.println(" [-preScript ]");
- System.out.println(" [-postScript ]");
- System.out.println(" [-scriptPath \"[;...]\"]");
- System.out.println(" [-propertiesPath \"[;...]\"]");
- System.out.println(" [-scriptlog ]");
- System.out.println(" [-log ]");
- System.out.println(" [-overwrite]");
- System.out.println(" [-recursive]");
- System.out.println(" [-readOnly]");
- System.out.println(" [-deleteProject]");
- System.out.println(" [-noanalysis]");
- System.out.println(" [-processor ]");
- System.out.println(" [-cspec ]");
- System.out.println(" [-analysisTimeoutPerFile ]");
- System.out.println(" [-keystore ]");
- System.out.println(" [-connect ]");
- System.out.println(" [-p]");
- System.out.println(" [-commit [\"\"]]");
- System.out.println(" [-okToDelete]");
- System.out.println(" [-max-cpu ]");
- System.out.println(" [-loader ]");
- // ** NOTE: please update 'analyzeHeadlessREADME.html' if changing command line parameters **
+ StringBuilder sb = new StringBuilder();
+ final String INDENT = " ";
+
+ sb.append("Headless Analyzer Usage: %s\n".formatted(execCmd));
+ sb.append(INDENT + " [/]\n");
+ sb.append(INDENT + " | ghidra://[:]/[/]\n");
+ for (Arg arg : Arg.values()) {
+ switch (arg) {
+ case IMPORT -> {
+ // Can't use both IMPORT and PROCESS, so must handle the usage a little
+ // differently
+ sb.append(
+ INDENT + "[[%s] | [%s]]\n".formatted(arg.usage(), Arg.PROCESS.usage()));
+ }
+ case PROCESS -> {
+ // Handled above by IMPORT
+ }
+ case LOADER_ARGS -> {
+ // Loader args are a little different because we don't know the full
+ // argument name ahead of time...just what it starts with
+ sb.append(INDENT + "[%s %s]\n"
+ .formatted(Arg.LOADER_ARGS.name, Arg.LOADER_ARGS.subArgFormat));
+ }
+ default -> {
+ sb.append(INDENT + "[%s]\n".formatted(arg.usage()));
+ }
+ }
+ }
if (Platform.CURRENT_PLATFORM.getOperatingSystem() != OperatingSystem.WINDOWS) {
- System.out.println();
- System.out.println(
+ sb.append("\n");
+ sb.append(
" - All uses of $GHIDRA_HOME or $USER_HOME in script path must be" +
- " preceded by '\\'");
+ " preceded by '\\'\n");
}
- System.out.println();
- System.out.println(
+ sb.append("\n");
+ sb.append(
"Please refer to 'analyzeHeadlessREADME.html' for detailed usage examples " +
- "and notes.");
+ "and notes.\n");
- System.out.println();
+ sb.append("\n");
+ System.out.println(sb);
System.exit(EXIT_CODE_ERROR);
}
@@ -486,23 +559,22 @@ public class AnalyzeHeadless implements GhidraLaunchable {
usage("analyzeHeadless");
}
- private String[] getSubArguments(String[] args, int argi) {
- List subArgs = new LinkedList<>();
+ private String[] getSubArguments(String[] args, int argi, Set argNames) {
+ List subArgs = new ArrayList<>();
int i = argi + 1;
- while (i < args.length && !args[i].startsWith("-")) {
+ while (i < args.length && !argNames.contains(args[i])) {
subArgs.add(args[i++]);
}
- return subArgs.toArray(new String[0]);
+ return subArgs.toArray(new String[subArgs.size()]);
}
- private boolean checkArgument(String optionName, String[] args, int argi)
+ private boolean checkArgument(Arg arg, String[] args, int argi)
throws InvalidInputException {
- // everything after this requires an argument
- if (!optionName.equalsIgnoreCase(args[argi])) {
+ if (!arg.matches(args[argi])) {
return false;
}
- if (argi + 1 == args.length) {
- throw new InvalidInputException(optionName + " requires an argument");
+ if (arg.requiresSubArgs && argi + 1 == args.length) {
+ throw new InvalidInputException(args[argi] + " requires an argument");
}
return true;
}
diff --git a/Ghidra/RuntimeScripts/Common/support/analyzeHeadlessREADME.html b/Ghidra/RuntimeScripts/Common/support/analyzeHeadlessREADME.html
index d4f7376435..963a5f2161 100644
--- a/Ghidra/RuntimeScripts/Common/support/analyzeHeadlessREADME.html
+++ b/Ghidra/RuntimeScripts/Common/support/analyzeHeadlessREADME.html
@@ -132,6 +132,7 @@ The Headless Analyzer uses the command-line parameters discussed below. See -max-cpu <max cpu cores to use>]
[-librarySearchPaths <path1>[;<path2>...]]
[-loader <desired loader name>]
+ [-loader-<loader argument name> <loader argument value>]