From a7d7a6656693211b0f63c7cfbd91ba1ef3190ee8 Mon Sep 17 00:00:00 2001 From: Kai Wang Date: Mon, 16 Mar 2026 15:12:49 +0800 Subject: [PATCH 1/3] Add zsh and bash completion scripts for mat-cli --- README.md | 58 ++ .../build.properties | 4 +- .../rootfiles/completion/bash/mat-cli | 412 +++++++++++++ .../rootfiles/completion/zsh/_mat-cli | 407 +++++++++++++ .../mat/cli/internal/CliArgumentParser.java | 38 +- .../mat/cli/internal/CliArguments.java | 5 + .../eclipse/mat/cli/internal/CliCommand.java | 13 +- .../mat/cli/internal/CliCommandCatalog.java | 539 ++++++++++------- .../mat/cli/internal/CliCommandExecutor.java | 4 + .../org/eclipse/mat/cli/internal/CliHelp.java | 1 + .../internal/CompletionScriptGenerator.java | 549 ++++++++++++++++++ .../mat/cli/internal/CompletionValueType.java | 20 + .../src/org/eclipse/mat/tests/AllTests.java | 1 + .../mat/tests/cli/CliArgumentParserTest.java | 67 +++ .../mat/tests/cli/CliCommandExecutorTest.java | 32 + .../org/eclipse/mat/tests/cli/CliTests.java | 2 +- .../cli/CompletionScriptGeneratorTest.java | 97 ++++ 17 files changed, 2036 insertions(+), 213 deletions(-) create mode 100644 features/org.eclipse.mat.cli.feature/rootfiles/completion/bash/mat-cli create mode 100644 features/org.eclipse.mat.cli.feature/rootfiles/completion/zsh/_mat-cli create mode 100644 plugins/org.eclipse.mat.cli/src/org/eclipse/mat/cli/internal/CompletionScriptGenerator.java create mode 100644 plugins/org.eclipse.mat.cli/src/org/eclipse/mat/cli/internal/CompletionValueType.java create mode 100644 plugins/org.eclipse.mat.tests/src/org/eclipse/mat/tests/cli/CompletionScriptGeneratorTest.java diff --git a/README.md b/README.md index 19aad06b..278e3eb3 100644 --- a/README.md +++ b/README.md @@ -46,6 +46,63 @@ npx skills add https://github.com/Demogorgon314/mat-cli --skill mat-cli-heapdump 2. Unzip it. 3. Run `./mat-cli --help` from the extracted directory. +### Shell Completion + +`mat-cli` ships first-party `bash` and `zsh` completion scripts in the release archive: + +- `completion/bash/mat-cli` +- `completion/zsh/_mat-cli` + +You can also generate the same scripts at runtime: + +```bash +./mat-cli completion bash +./mat-cli completion zsh +``` + +Temporary activation in the current shell: + +```bash +source <(./mat-cli completion bash) +source <(./mat-cli completion zsh) +``` + +Persistent `bash` installation: + +```bash +mkdir -p ~/.local/share/bash-completion/completions +cp ./completion/bash/mat-cli ~/.local/share/bash-completion/completions/mat-cli +``` + +Or generate it directly: + +```bash +mkdir -p ~/.local/share/bash-completion/completions +./mat-cli completion bash > ~/.local/share/bash-completion/completions/mat-cli +``` + +Persistent `zsh` installation: + +```bash +mkdir -p ~/.zfunc +cp ./completion/zsh/_mat-cli ~/.zfunc/_mat-cli +``` + +Or generate it directly: + +```bash +mkdir -p ~/.zfunc +./mat-cli completion zsh > ~/.zfunc/_mat-cli +``` + +If your `zsh` setup does not already load `~/.zfunc`, add this once to your shell startup file before `compinit`: + +```zsh +fpath=(~/.zfunc $fpath) +autoload -Uz compinit +compinit +``` + ## Requirements - Java 17 or newer to run the standalone release @@ -68,6 +125,7 @@ Useful discovery commands: - `mat-cli schema --format json` - `mat-cli list-queries --format json` - `mat-cli describe-query --format json` +- `mat-cli completion ` ## Example Workflows diff --git a/features/org.eclipse.mat.cli.feature/build.properties b/features/org.eclipse.mat.cli.feature/build.properties index 4c7257d1..88e1bc48 100644 --- a/features/org.eclipse.mat.cli.feature/build.properties +++ b/features/org.eclipse.mat.cli.feature/build.properties @@ -2,7 +2,9 @@ bin.includes = feature.xml,\ build.properties root=file:rootfiles/notice.html,\ - file:rootfiles/.eclipseproduct + file:rootfiles/.eclipseproduct,\ + file:rootfiles/completion/bash/mat-cli,\ + file:rootfiles/completion/zsh/_mat-cli root.win32.win32.x86_64=file:rootfiles/win32/mat-cli.bat diff --git a/features/org.eclipse.mat.cli.feature/rootfiles/completion/bash/mat-cli b/features/org.eclipse.mat.cli.feature/rootfiles/completion/bash/mat-cli new file mode 100644 index 00000000..ea899350 --- /dev/null +++ b/features/org.eclipse.mat.cli.feature/rootfiles/completion/bash/mat-cli @@ -0,0 +1,412 @@ +# bash completion for mat-cli +_mat_cli_is_command() { + case "$1" in + summary|threads|histogram|instances|inspect-object|top-consumers|path2gc|oql|query|describe|schema|list-queries|describe-query|completion) + return 0 + ;; + esac + return 1 +} + +_mat_cli_kind_expects_value() { + [ -n "$1" ] && [ "$1" != "none" ] +} + +_mat_cli_option_kind_global() { + case "$1" in + --help) + printf '%s\n' 'none' + ;; + --verbose) + printf '%s\n' 'none' + ;; + --format) + printf '%s\n' 'enum:text json' + ;; + esac +} + +_mat_cli_option_kind_for_command() { + case "$1:$2" in + summary:--format) + printf '%s\n' 'enum:text json' + ;; + threads:--limit) + printf '%s\n' 'free-text' + ;; + threads:--format) + printf '%s\n' 'enum:text json' + ;; + histogram:--limit) + printf '%s\n' 'free-text' + ;; + histogram:--format) + printf '%s\n' 'enum:text json' + ;; + instances:--class) + printf '%s\n' 'free-text' + ;; + instances:--class-regex) + printf '%s\n' 'free-text' + ;; + instances:--class-contains) + printf '%s\n' 'free-text' + ;; + instances:--include-subclasses) + printf '%s\n' 'none' + ;; + instances:--limit) + printf '%s\n' 'free-text' + ;; + instances:--format) + printf '%s\n' 'enum:text json' + ;; + inspect-object:--object) + printf '%s\n' 'free-text' + ;; + inspect-object:--select-fields) + printf '%s\n' 'free-text' + ;; + inspect-object:--field-paths) + printf '%s\n' 'free-text' + ;; + inspect-object:--show-nulls) + printf '%s\n' 'none' + ;; + inspect-object:--limit) + printf '%s\n' 'free-text' + ;; + inspect-object:--depth) + printf '%s\n' 'free-text' + ;; + inspect-object:--format) + printf '%s\n' 'enum:text json' + ;; + top-consumers:--limit) + printf '%s\n' 'free-text' + ;; + top-consumers:--depth) + printf '%s\n' 'free-text' + ;; + top-consumers:--format) + printf '%s\n' 'enum:text json' + ;; + path2gc:--object) + printf '%s\n' 'free-text' + ;; + path2gc:--limit) + printf '%s\n' 'free-text' + ;; + path2gc:--depth) + printf '%s\n' 'free-text' + ;; + path2gc:--format) + printf '%s\n' 'enum:text json' + ;; + oql:--query) + printf '%s\n' 'free-text' + ;; + oql:--query-file) + printf '%s\n' 'file' + ;; + oql:--query-stdin) + printf '%s\n' 'none' + ;; + oql:--limit) + printf '%s\n' 'free-text' + ;; + oql:--depth) + printf '%s\n' 'free-text' + ;; + oql:--format) + printf '%s\n' 'enum:text json' + ;; + query:--command) + printf '%s\n' 'free-text' + ;; + query:--command-file) + printf '%s\n' 'file' + ;; + query:--command-stdin) + printf '%s\n' 'none' + ;; + query:--limit) + printf '%s\n' 'free-text' + ;; + query:--depth) + printf '%s\n' 'free-text' + ;; + query:--format) + printf '%s\n' 'enum:text json' + ;; + describe:--format) + printf '%s\n' 'enum:text json' + ;; + schema:--format) + printf '%s\n' 'enum:text json' + ;; + list-queries:--format) + printf '%s\n' 'enum:text json' + ;; + describe-query:--format) + printf '%s\n' 'enum:text json' + ;; + completion:--format) + printf '%s\n' 'enum:text json' + ;; + esac +} + +_mat_cli_option_kind() { + local kind + kind="$(_mat_cli_option_kind_for_command "$1" "$2")" + if [ -n "$kind" ]; then + printf '%s\n' "$kind" + return 0 + fi + _mat_cli_option_kind_global "$2" +} + +_mat_cli_options_for_command() { + case "$1" in + summary) + printf '%s\n' '--format --help --verbose' + ;; + threads) + printf '%s\n' '--limit --format --help --verbose' + ;; + histogram) + printf '%s\n' '--limit --format --help --verbose' + ;; + instances) + printf '%s\n' '--class --class-regex --class-contains --include-subclasses --limit --format --help --verbose' + ;; + inspect-object) + printf '%s\n' '--object --select-fields --field-paths --show-nulls --limit --depth --format --help --verbose' + ;; + top-consumers) + printf '%s\n' '--limit --depth --format --help --verbose' + ;; + path2gc) + printf '%s\n' '--object --limit --depth --format --help --verbose' + ;; + oql) + printf '%s\n' '--query --query-file --query-stdin --limit --depth --format --help --verbose' + ;; + query) + printf '%s\n' '--command --command-file --command-stdin --limit --depth --format --help --verbose' + ;; + describe) + printf '%s\n' '--format --help --verbose' + ;; + schema) + printf '%s\n' '--format --help --verbose' + ;; + list-queries) + printf '%s\n' '--format --help --verbose' + ;; + describe-query) + printf '%s\n' '--format --help --verbose' + ;; + completion) + printf '%s\n' '--format --help --verbose' + ;; + esac +} + +_mat_cli_positional_kind() { + case "$1:$2" in + summary:0) + printf '%s\n' 'file' + ;; + threads:0) + printf '%s\n' 'file' + ;; + histogram:0) + printf '%s\n' 'file' + ;; + instances:0) + printf '%s\n' 'file' + ;; + inspect-object:0) + printf '%s\n' 'file' + ;; + top-consumers:0) + printf '%s\n' 'file' + ;; + path2gc:0) + printf '%s\n' 'file' + ;; + oql:0) + printf '%s\n' 'file' + ;; + query:0) + printf '%s\n' 'file' + ;; + describe:0) + printf '%s\n' 'enum:summary threads histogram instances inspect-object top-consumers path2gc oql query describe schema list-queries describe-query completion' + ;; + schema:0) + printf '%s\n' 'enum:summary threads histogram instances inspect-object top-consumers path2gc oql query describe schema list-queries describe-query completion' + ;; + describe-query:0) + printf '%s\n' 'free-text' + ;; + completion:0) + printf '%s\n' 'enum:bash zsh' + ;; + esac +} + +_mat_cli_set_word_replies() { + local prefix="$1" + local current="$2" + local values="$3" + local i + COMPREPLY=( $(compgen -W "$values" -- "$current") ) + if [ -n "$prefix" ]; then + for ((i=0; i<${#COMPREPLY[@]}; i++)); do + COMPREPLY[i]="$prefix${COMPREPLY[i]}" + done + fi +} + +_mat_cli_set_file_replies() { + local prefix="$1" + local current="$2" + local line + COMPREPLY=() + while IFS= read -r line; do + if [ -n "$prefix" ]; then + COMPREPLY+=("$prefix$line") + else + COMPREPLY+=("$line") + fi + done < <(compgen -f -- "$current") + if type compopt >/dev/null 2>&1; then + compopt -o filenames 2>/dev/null + fi +} + +_mat_cli_complete_kind() { + local kind="$1" + local current="$2" + local prefix="$3" + case "$kind" in + enum:*) + _mat_cli_set_word_replies "$prefix" "$current" "${kind#enum:}" + ;; + file) + _mat_cli_set_file_replies "$prefix" "$current" + ;; + *) + COMPREPLY=() + ;; + esac +} + +_mat_cli_find_command_index() { + local i word pending_kind="" + for ((i=1; i/dev/null 2>&1 + _files -P "$prefix" + else + _files + fi + ;; + *) + return 1 + ;; + esac +} + +_mat_cli_find_command() { + local i word pending_kind="" + reply=() + for ((i=2; i/dev/null 2>&1 + _mat_cli_complete_kind "$kind" "$inline_prefix" + return 0 + fi + fi + kind="$(_mat_cli_option_kind_global "$prev")" + if _mat_cli_kind_expects_value "$kind"; then + _mat_cli_complete_kind "$kind" "" + return 0 + fi + _mat_cli_complete_words "" "summary" "threads" "histogram" "instances" "inspect-object" "top-consumers" "path2gc" "oql" "query" "describe" "schema" "list-queries" "describe-query" "completion" "--help" "--verbose" "--format" + return 0 + fi + + cmd="$reply[1]" + cmd_index="$reply[2]" + if [[ "$cur" == --*=* ]]; then + inline_option="${cur%%=*}" + inline_prefix="$inline_option=" + kind="$(_mat_cli_option_kind "$cmd" "$inline_option")" + if [[ "$kind" == enum:* || "$kind" == file ]]; then + compset -P "$inline_prefix" >/dev/null 2>&1 + _mat_cli_complete_kind "$kind" "$inline_prefix" + return 0 + fi + fi + kind="$(_mat_cli_option_kind "$cmd" "$prev")" + if _mat_cli_kind_expects_value "$kind"; then + _mat_cli_complete_kind "$kind" "" + return 0 + fi + + position_index=0 + pending_kind="" + for ((i=cmd_index+1; i/dev/null diff --git a/plugins/org.eclipse.mat.cli/src/org/eclipse/mat/cli/internal/CliArgumentParser.java b/plugins/org.eclipse.mat.cli/src/org/eclipse/mat/cli/internal/CliArgumentParser.java index d7fac7f9..cd1f1e0b 100644 --- a/plugins/org.eclipse.mat.cli/src/org/eclipse/mat/cli/internal/CliArgumentParser.java +++ b/plugins/org.eclipse.mat.cli/src/org/eclipse/mat/cli/internal/CliArgumentParser.java @@ -218,9 +218,9 @@ else if (command.requiresSubjectCommand() && subjectCommand == null) subjectName = arg; subjectCommand = CliCommand.parse(arg); } - else if (command.requiresQueryIdentifier() && subjectName == null) + else if (command.requiresSubjectName() && subjectName == null) { - subjectName = arg; + subjectName = normalizeSubjectName(command, arg); } else if (command.requiresSnapshot() && heapFile == null) { @@ -306,8 +306,14 @@ private void validate(CliArguments arguments) throws CliException if (arguments.getCommand().requiresSubjectCommand() && arguments.getSubjectCommand() == null) throw CliException.usage(arguments.getCommand().getToken() + " requires a command name"); //$NON-NLS-1$ - if (arguments.getCommand().requiresQueryIdentifier() && isEmpty(arguments.getSubjectName())) - throw CliException.usage(arguments.getCommand().getToken() + " requires a query identifier"); //$NON-NLS-1$ + if (arguments.getCommand().requiresSubjectName() && isEmpty(arguments.getSubjectName())) + { + if (arguments.getCommand() == CliCommand.COMPLETION) + throw CliException.usage("completion requires a shell name (bash or zsh)"); //$NON-NLS-1$ + if (arguments.getCommand() == CliCommand.DESCRIBE_QUERY) + throw CliException.usage("describe-query requires a query identifier"); //$NON-NLS-1$ + throw CliException.usage(arguments.getCommand().getToken() + " requires a subject name"); //$NON-NLS-1$ + } if (arguments.getCommand().requiresSnapshot() && arguments.getHeapFile() == null) throw CliException.usage("Missing heap dump path"); //$NON-NLS-1$ @@ -345,6 +351,9 @@ private void validate(CliArguments arguments) throws CliException if (isEmpty(arguments.getQueryCommand())) throw CliException.usage("query requires --command"); //$NON-NLS-1$ break; + case COMPLETION: + validateCompletionShell(arguments.getCompletionShell()); + break; case DESCRIBE: case SCHEMA: default: @@ -357,6 +366,16 @@ private boolean isEmpty(String value) return value == null || value.length() == 0; } + private void validateCompletionShell(String shell) throws CliException + { + for (String supportedShell : CliCommandCatalog.supportedCompletionShells()) + { + if (supportedShell.equals(shell)) + return; + } + throw CliException.usage("Unsupported completion shell: " + shell + " (expected bash or zsh)"); //$NON-NLS-1$ //$NON-NLS-2$ + } + private void validateSelectFields(List selectFields) throws CliException { for (String selectField : selectFields) @@ -597,9 +616,9 @@ else if (command.requiresSubjectCommand() && subjectCommand == null) subjectCommand = null; } } - else if (command.requiresQueryIdentifier() && subjectName == null) + else if (command.requiresSubjectName() && subjectName == null) { - subjectName = arg; + subjectName = normalizeSubjectName(command, arg); } else if (command.requiresSnapshot() && heapFile == null) { @@ -693,4 +712,11 @@ private CliException removedOption(String option) { return CliException.usage(option + " has been removed. Use --format json."); //$NON-NLS-1$ } + + private String normalizeSubjectName(CliCommand command, String value) + { + if (command == CliCommand.COMPLETION && value != null) + return value.toLowerCase(java.util.Locale.ENGLISH); + return value; + } } diff --git a/plugins/org.eclipse.mat.cli/src/org/eclipse/mat/cli/internal/CliArguments.java b/plugins/org.eclipse.mat.cli/src/org/eclipse/mat/cli/internal/CliArguments.java index 1c58732e..6e5a6cc8 100644 --- a/plugins/org.eclipse.mat.cli/src/org/eclipse/mat/cli/internal/CliArguments.java +++ b/plugins/org.eclipse.mat.cli/src/org/eclipse/mat/cli/internal/CliArguments.java @@ -93,6 +93,11 @@ public String getSubjectName() return subjectName; } + public String getCompletionShell() + { + return command == CliCommand.COMPLETION ? subjectName : null; + } + public File getHeapFile() { return heapFile; diff --git a/plugins/org.eclipse.mat.cli/src/org/eclipse/mat/cli/internal/CliCommand.java b/plugins/org.eclipse.mat.cli/src/org/eclipse/mat/cli/internal/CliCommand.java index 6348784e..da63ae11 100644 --- a/plugins/org.eclipse.mat.cli/src/org/eclipse/mat/cli/internal/CliCommand.java +++ b/plugins/org.eclipse.mat.cli/src/org/eclipse/mat/cli/internal/CliCommand.java @@ -23,22 +23,23 @@ public enum CliCommand DESCRIBE("describe", false, true, false, false), //$NON-NLS-1$ SCHEMA("schema", false, true, false, false), //$NON-NLS-1$ LIST_QUERIES("list-queries", false, false, true, false), //$NON-NLS-1$ - DESCRIBE_QUERY("describe-query", false, false, true, true); //$NON-NLS-1$ + DESCRIBE_QUERY("describe-query", false, false, true, true), //$NON-NLS-1$ + COMPLETION("completion", false, false, false, true); //$NON-NLS-1$ private final String token; private final boolean requiresSnapshot; private final boolean requiresSubjectCommand; private final boolean requiresQueryRegistry; - private final boolean requiresQueryIdentifier; + private final boolean requiresSubjectName; private CliCommand(String token, boolean requiresSnapshot, boolean requiresSubjectCommand, - boolean requiresQueryRegistry, boolean requiresQueryIdentifier) + boolean requiresQueryRegistry, boolean requiresSubjectName) { this.token = token; this.requiresSnapshot = requiresSnapshot; this.requiresSubjectCommand = requiresSubjectCommand; this.requiresQueryRegistry = requiresQueryRegistry; - this.requiresQueryIdentifier = requiresQueryIdentifier; + this.requiresSubjectName = requiresSubjectName; } public String getToken() @@ -61,9 +62,9 @@ public boolean requiresQueryRegistry() return requiresQueryRegistry; } - public boolean requiresQueryIdentifier() + public boolean requiresSubjectName() { - return requiresQueryIdentifier; + return requiresSubjectName; } public boolean requiresRuntimeServices() diff --git a/plugins/org.eclipse.mat.cli/src/org/eclipse/mat/cli/internal/CliCommandCatalog.java b/plugins/org.eclipse.mat.cli/src/org/eclipse/mat/cli/internal/CliCommandCatalog.java index b7857c0e..c324906d 100644 --- a/plugins/org.eclipse.mat.cli/src/org/eclipse/mat/cli/internal/CliCommandCatalog.java +++ b/plugins/org.eclipse.mat.cli/src/org/eclipse/mat/cli/internal/CliCommandCatalog.java @@ -9,6 +9,7 @@ *******************************************************************************/ package org.eclipse.mat.cli.internal; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.EnumMap; @@ -18,19 +19,54 @@ public final class CliCommandCatalog { + public static final class PositionalDefinition + { + private final String name; + private final CompletionValueType completionValueType; + private final List completionCandidates; + + private PositionalDefinition(String name, CompletionValueType completionValueType, + List completionCandidates) + { + this.name = name; + this.completionValueType = completionValueType; + this.completionCandidates = immutableCopy(completionCandidates); + } + + public String getName() + { + return name; + } + + public CompletionValueType getCompletionValueType() + { + return completionValueType; + } + + public List getCompletionCandidates() + { + return completionCandidates; + } + } + public static final class OptionDefinition { private final String name; private final String valueHint; private final boolean required; private final String description; + private final CompletionValueType completionValueType; + private final List completionCandidates; - private OptionDefinition(String name, String valueHint, boolean required, String description) + private OptionDefinition(String name, String valueHint, boolean required, String description, + CompletionValueType completionValueType, List completionCandidates) { this.name = name; this.valueHint = valueHint; this.required = required; this.description = description; + this.completionValueType = completionValueType; + this.completionCandidates = immutableCopy(completionCandidates); } public String getName() @@ -52,6 +88,16 @@ public String getDescription() { return description; } + + public CompletionValueType getCompletionValueType() + { + return completionValueType; + } + + public List getCompletionCandidates() + { + return completionCandidates; + } } public static final class OutputDefinition @@ -89,7 +135,7 @@ public static final class CommandDefinition private final String summary; private final String usage; private final boolean requiresSnapshot; - private final List positionalArguments; + private final List positionalArguments; private final List options; private final List outputs; private final List suggestedNextCommands; @@ -97,20 +143,20 @@ public static final class CommandDefinition private final String agentPayloadDescription; private CommandDefinition(CliCommand command, String summary, String usage, boolean requiresSnapshot, - List positionalArguments, List options, List outputs, - List suggestedNextCommands, String agentPayloadDescription, - List agentPayloadFields) + List positionalArguments, List options, + List outputs, List suggestedNextCommands, + String agentPayloadDescription, List agentPayloadFields) { this.command = command; this.summary = summary; this.usage = usage; this.requiresSnapshot = requiresSnapshot; - this.positionalArguments = positionalArguments; - this.options = options; - this.outputs = outputs; - this.suggestedNextCommands = suggestedNextCommands; + this.positionalArguments = immutableCopy(positionalArguments); + this.options = immutableCopy(options); + this.outputs = immutableCopy(outputs); + this.suggestedNextCommands = immutableCopy(suggestedNextCommands); this.agentPayloadDescription = agentPayloadDescription; - this.agentPayloadFields = agentPayloadFields; + this.agentPayloadFields = immutableCopy(agentPayloadFields); } public CliCommand getCommand() @@ -134,6 +180,16 @@ public boolean requiresSnapshot() } public List getPositionalArguments() + { + List names = new ArrayList(positionalArguments.size()); + for (PositionalDefinition positionalArgument : positionalArguments) + { + names.add(positionalArgument.getName()); + } + return Collections.unmodifiableList(names); + } + + public List getPositionalDefinitions() { return positionalArguments; } @@ -164,27 +220,33 @@ public String getAgentPayloadDescription() } } + private static final List FORMAT_VALUES = immutableCopy(Arrays.asList("text", "json")); //$NON-NLS-1$ //$NON-NLS-2$ + private static final List COMPLETION_SHELLS = immutableCopy(Arrays.asList("bash", "zsh")); //$NON-NLS-1$ //$NON-NLS-2$ + private static final List COMMAND_TOKENS = buildCommandTokens(); + private static final List GLOBAL_OPTIONS = immutableCopy(Arrays.asList( + flagOption("--help", "Show general or command-specific help."), //$NON-NLS-1$ //$NON-NLS-2$ + flagOption("--verbose", "Print detailed diagnostics on failure."), //$NON-NLS-1$ //$NON-NLS-2$ + enumOption("--format", "text|json", false, "Select text or JSON output.", FORMAT_VALUES))); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ private static final List FORMAT_OPTION = Collections.singletonList( - option("--format", "text|json", false, "Select text or JSON output.")); + enumOption("--format", "text|json", false, "Select text or JSON output.", FORMAT_VALUES)); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ private static final List FORMAT_AND_LIMIT_OPTIONS = Arrays.asList( - option("--limit", "N", false, "Limit rows or children per level."), - option("--format", "text|json", false, "Select text or JSON output.")); + freeTextOption("--limit", "N", false, "Limit rows or children per level."), //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ + enumOption("--format", "text|json", false, "Select text or JSON output.", FORMAT_VALUES)); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ private static final List FORMAT_LIMIT_CLASS_OPTIONS = Arrays.asList( - option("--class", "", false, - "Exactly match one fully qualified class name. Cannot be combined with --class-regex or --class-contains."), - option("--class-regex", "", false, - "Match class names with a full Java regex. Use .*String.* for contains matching. Cannot be combined with --class or --class-contains."), - option("--class-contains", "", false, - "Match class names containing plain text. Cannot be combined with --class or --class-regex."), - option("--include-subclasses", null, false, "Include subclasses of each matched class."), - option("--limit", "N", false, "Limit rows returned."), - option("--format", "text|json", false, "Select text or JSON output.")); + freeTextOption("--class", "", false, //$NON-NLS-1$ //$NON-NLS-2$ + "Exactly match one fully qualified class name. Cannot be combined with --class-regex or --class-contains."), //$NON-NLS-1$ + freeTextOption("--class-regex", "", false, //$NON-NLS-1$ //$NON-NLS-2$ + "Match class names with a full Java regex. Use .*String.* for contains matching. Cannot be combined with --class or --class-contains."), //$NON-NLS-1$ + freeTextOption("--class-contains", "", false, //$NON-NLS-1$ //$NON-NLS-2$ + "Match class names containing plain text. Cannot be combined with --class or --class-regex."), //$NON-NLS-1$ + flagOption("--include-subclasses", "Include subclasses of each matched class."), //$NON-NLS-1$ //$NON-NLS-2$ + freeTextOption("--limit", "N", false, "Limit rows returned."), //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ + enumOption("--format", "text|json", false, "Select text or JSON output.", FORMAT_VALUES)); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ private static final List FORMAT_LIMIT_AND_DEPTH_OPTIONS = Arrays.asList( - option("--limit", "N", false, "Limit rows, sections, or children per level."), - option("--depth", "N", false, "Limit nested tree or section depth."), - option("--format", "text|json", false, "Select text or JSON output.")); + freeTextOption("--limit", "N", false, "Limit rows, sections, or children per level."), //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ + freeTextOption("--depth", "N", false, "Limit nested tree or section depth."), //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ + enumOption("--format", "text|json", false, "Select text or JSON output.", FORMAT_VALUES)); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ private static final Map QUERY_LIMIT_OVERRIDES = queryLimitOverrides(); - private static final Map DEFINITIONS = definitions(); private CliCommandCatalog() @@ -200,200 +262,262 @@ public static Integer queryDefaultLimit(String queryIdentifier) return queryIdentifier == null ? null : QUERY_LIMIT_OVERRIDES.get(queryIdentifier); } + public static List globalOptions() + { + return GLOBAL_OPTIONS; + } + + public static List supportedCompletionShells() + { + return COMPLETION_SHELLS; + } + + public static List commandTokens() + { + return COMMAND_TOKENS; + } + + public static List completionOptions(CliCommand command) + { + CommandDefinition definition = lookup(command); + java.util.LinkedHashMap options = new java.util.LinkedHashMap(); + if (definition != null) + { + for (OptionDefinition option : definition.getOptions()) + { + options.put(option.getName(), option); + } + } + for (OptionDefinition globalOption : GLOBAL_OPTIONS) + { + if (!"--format".equals(globalOption.getName())) //$NON-NLS-1$ + options.put(globalOption.getName(), globalOption); + } + return immutableCopy(new ArrayList(options.values())); + } + private static Map definitions() { EnumMap definitions = new EnumMap(CliCommand.class); + List heapArgument = Collections.singletonList(filePositional("heap")); //$NON-NLS-1$ + List commandArgument = Collections.singletonList(enumPositional("command", COMMAND_TOKENS)); //$NON-NLS-1$ + List queryIdArgument = Collections.singletonList(freeTextPositional("query-id")); //$NON-NLS-1$ + List completionShellArgument = Collections.singletonList( + enumPositional("bash|zsh", COMPLETION_SHELLS)); //$NON-NLS-1$ + definitions.put(CliCommand.SUMMARY, new CommandDefinition(CliCommand.SUMMARY, - "Read basic heap metadata such as object counts and used heap.", - "mat-cli summary [--format text|json]", true, Collections.singletonList("heap"), FORMAT_OPTION, - Arrays.asList(output("text", "summary", "Human-readable heap summary."), - output("json", "summary", "Stable summary JSON envelope.")), - Arrays.asList("histogram ", "top-consumers ", - "query --command \"thread_overview\""), - "summary object with snapshot-wide counters and heap metadata.", - Arrays.asList("summary.path", "summary.heapFormat", "summary.numberOfObjects", - "summary.numberOfClasses", "summary.usedHeapSize"))); + "Read basic heap metadata such as object counts and used heap.", //$NON-NLS-1$ + "mat-cli summary [--format text|json]", true, heapArgument, FORMAT_OPTION, //$NON-NLS-1$ + Arrays.asList(output("text", "summary", "Human-readable heap summary."), //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ + output("json", "summary", "Stable summary JSON envelope.")), //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ + Arrays.asList("histogram ", "top-consumers ", //$NON-NLS-1$ //$NON-NLS-2$ + "query --command \"thread_overview\""), //$NON-NLS-1$ + "summary object with snapshot-wide counters and heap metadata.", //$NON-NLS-1$ + Arrays.asList("summary.path", "summary.heapFormat", "summary.numberOfObjects", //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ + "summary.numberOfClasses", "summary.usedHeapSize"))); //$NON-NLS-1$ //$NON-NLS-2$ definitions.put(CliCommand.THREADS, new CommandDefinition(CliCommand.THREADS, - "Show a best-effort thread report from the heap dump, including state, retained heap, and stack traces when available.", - "mat-cli threads [--limit N] [--format text|json]", true, - Collections.singletonList("heap"), - Arrays.asList(option("--limit", "N", false, - "Limit threads returned. Defaults to all threads."), - option("--format", "text|json", false, "Select text or JSON output.")), - Arrays.asList(output("text", "threads", "Thread report with overview plus per-thread stack sections."), - output("json", "threads", "Stable thread report JSON.")), - Arrays.asList("histogram ", "top-consumers "), - "thread report payload with a summary, best-effort notice, and one entry per returned thread.", - Arrays.asList("notice", "summary.totalThreads", "summary.returnedThreads", - "summary.stackAvailableThreads", "summary.stateAvailableThreads", "threads[]", - "threads[].name", "threads[].technicalName", "threads[].objectAddress", - "threads[].state", "threads[].retainedBytes", "threads[].stackAvailable", - "threads[].stackUnavailableReason", "threads[].stackFrames[]"))); + "Show a best-effort thread report from the heap dump, including state, retained heap, and stack traces when available.", //$NON-NLS-1$ + "mat-cli threads [--limit N] [--format text|json]", true, heapArgument, //$NON-NLS-1$ + Arrays.asList(freeTextOption("--limit", "N", false, "Limit threads returned. Defaults to all threads."), //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ + enumOption("--format", "text|json", false, "Select text or JSON output.", FORMAT_VALUES)), //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ + Arrays.asList(output("text", "threads", "Thread report with overview plus per-thread stack sections."), //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ + output("json", "threads", "Stable thread report JSON.")), //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ + Arrays.asList("histogram ", "top-consumers "), //$NON-NLS-1$ //$NON-NLS-2$ + "thread report payload with a summary, best-effort notice, and one entry per returned thread.", //$NON-NLS-1$ + Arrays.asList("notice", "summary.totalThreads", "summary.returnedThreads", //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ + "summary.stackAvailableThreads", "summary.stateAvailableThreads", "threads[]", //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ + "threads[].name", "threads[].technicalName", "threads[].objectAddress", //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ + "threads[].state", "threads[].retainedBytes", "threads[].stackAvailable", //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ + "threads[].stackUnavailableReason", "threads[].stackFrames[]"))); //$NON-NLS-1$ //$NON-NLS-2$ definitions.put(CliCommand.HISTOGRAM, new CommandDefinition(CliCommand.HISTOGRAM, - "Group objects by class and report shallow heap plus approximate retained heap.", - "mat-cli histogram [--limit N] [--format text|json]", true, - Collections.singletonList("heap"), FORMAT_AND_LIMIT_OPTIONS, - Arrays.asList(output("text", "table", "Text table with formatted columns."), - output("json", "table", "Stable keyed table JSON.")), - Arrays.asList("top-consumers ", - "query --command \"histogram\""), - "table payload with stable column ids, normalized byte values, optional row addresses, and per-cell metadata for approximate retained sizes.", - Arrays.asList("items[]", "items[]._address when no address column", - "items[]._meta.retained_heap.kind when approximate"))); + "Group objects by class and report shallow heap plus approximate retained heap.", //$NON-NLS-1$ + "mat-cli histogram [--limit N] [--format text|json]", true, heapArgument, //$NON-NLS-1$ + FORMAT_AND_LIMIT_OPTIONS, + Arrays.asList(output("text", "table", "Text table with formatted columns."), //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ + output("json", "table", "Stable keyed table JSON.")), //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ + Arrays.asList("top-consumers ", "query --command \"histogram\""), //$NON-NLS-1$ //$NON-NLS-2$ + "table payload with stable column ids, normalized byte values, optional row addresses, and per-cell metadata for approximate retained sizes.", //$NON-NLS-1$ + Arrays.asList("items[]", "items[]._address when no address column", //$NON-NLS-1$ //$NON-NLS-2$ + "items[]._meta.retained_heap.kind when approximate"))); //$NON-NLS-1$ definitions.put(CliCommand.INSTANCES, new CommandDefinition(CliCommand.INSTANCES, - "List live objects for one class so you can pick a concrete instance to inspect.", - "mat-cli instances [--class | --class-regex | --class-contains ] [--include-subclasses] [--limit N] [--format text|json]", - true, Collections.singletonList("heap"), FORMAT_LIMIT_CLASS_OPTIONS, - Arrays.asList(output("text", "table", "Text table of matching objects."), - output("json", "table", "Stable keyed table JSON for matching objects.")), - Arrays.asList("inspect-object --object 0x...", - "path2gc --object 0x..."), - "table payload listing matching objects with addresses, previews, and heap sizes.", - Arrays.asList("items[]", "items[].object_address", "items[].class_name", - "items[].preview"))); + "List live objects for one class so you can pick a concrete instance to inspect.", //$NON-NLS-1$ + "mat-cli instances [--class | --class-regex | --class-contains ] [--include-subclasses] [--limit N] [--format text|json]", //$NON-NLS-1$ + true, heapArgument, FORMAT_LIMIT_CLASS_OPTIONS, + Arrays.asList(output("text", "table", "Text table of matching objects."), //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ + output("json", "table", "Stable keyed table JSON for matching objects.")), //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ + Arrays.asList("inspect-object --object 0x...", "path2gc --object 0x..."), //$NON-NLS-1$ //$NON-NLS-2$ + "table payload listing matching objects with addresses, previews, and heap sizes.", //$NON-NLS-1$ + Arrays.asList("items[]", "items[].object_address", "items[].class_name", "items[].preview"))); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$ definitions.put(CliCommand.INSPECT_OBJECT, new CommandDefinition(CliCommand.INSPECT_OBJECT, - "Inspect one object like MAT's object inspector, or jump directly to one or more field paths for targeted state checks.", - "mat-cli inspect-object --object 0x... [--select-fields FIELD | --field-paths PATH] [--show-nulls] [--limit N] [--depth N] [--format text|json]", - true, Collections.singletonList("heap"), Arrays.asList(option("--object", "0x...", true, - "Object address to inspect."), - option("--select-fields", "FIELD", false, - "Inspect one or more direct fields from the root object. May be repeated. Cannot be combined with --field-paths."), - option("--field-paths", "PATH", false, - "Inspect one or more dotted field paths such as cleaner.offsetMap. May be repeated. Cannot be combined with --select-fields."), - option("--show-nulls", null, false, - "Show nested null fields and array slots in text output."), - option("--limit", "N", false, "Limit children per level."), - option("--depth", "N", false, - "Limit nested object expansion depth. Defaults to 3 for inspect-object when omitted."), - option("--format", "text|json", false, "Select text or JSON output.")), - Arrays.asList(output("text", "tree", "Indented object-inspector tree."), - output("json", "tree", "Stable keyed object-inspector tree JSON.")), - Arrays.asList("path2gc --object 0x...", - "oql --query \"SELECT * FROM OBJECTS 0x...\""), - "tree payload with field and element nodes, stable paths, value kinds, concrete values, targeted field-path roots, preview metadata, sparse child arrays, and optional row addresses.", - Arrays.asList("items[]", "items[].path", "items[].valueKind", - "items[].hasChildren", "items[].kind", "items[].name", - "items[].value", "items[].object_address", "items[]._children[] when returned", - "items[]._childrenTruncated when true", "items[]._address when no address column", - "items[]._meta.value.kind when previewed", - "items[]._meta.value.length when previewed", - "items[]._meta.value.encoding when byte[] previewed"))); + "Inspect one object like MAT's object inspector, or jump directly to one or more field paths for targeted state checks.", //$NON-NLS-1$ + "mat-cli inspect-object --object 0x... [--select-fields FIELD | --field-paths PATH] [--show-nulls] [--limit N] [--depth N] [--format text|json]", //$NON-NLS-1$ + true, heapArgument, + Arrays.asList(freeTextOption("--object", "0x...", true, "Object address to inspect."), //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ + freeTextOption("--select-fields", "FIELD", false, //$NON-NLS-1$ //$NON-NLS-2$ + "Inspect one or more direct fields from the root object. May be repeated. Cannot be combined with --field-paths."), //$NON-NLS-1$ + freeTextOption("--field-paths", "PATH", false, //$NON-NLS-1$ //$NON-NLS-2$ + "Inspect one or more dotted field paths such as cleaner.offsetMap. May be repeated. Cannot be combined with --select-fields."), //$NON-NLS-1$ + flagOption("--show-nulls", "Show nested null fields and array slots in text output."), //$NON-NLS-1$ //$NON-NLS-2$ + freeTextOption("--limit", "N", false, "Limit children per level."), //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ + freeTextOption("--depth", "N", false, //$NON-NLS-1$ //$NON-NLS-2$ + "Limit nested object expansion depth. Defaults to 3 for inspect-object when omitted."), //$NON-NLS-1$ + enumOption("--format", "text|json", false, "Select text or JSON output.", FORMAT_VALUES)), //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ + Arrays.asList(output("text", "tree", "Indented object-inspector tree."), //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ + output("json", "tree", "Stable keyed object-inspector tree JSON.")), //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ + Arrays.asList("path2gc --object 0x...", "oql --query \"SELECT * FROM OBJECTS 0x...\""), //$NON-NLS-1$ //$NON-NLS-2$ + "tree payload with field and element nodes, stable paths, value kinds, concrete values, targeted field-path roots, preview metadata, sparse child arrays, and optional row addresses.", //$NON-NLS-1$ + Arrays.asList("items[]", "items[].path", "items[].valueKind", "items[].hasChildren", //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$ + "items[].kind", "items[].name", "items[].value", "items[].object_address", //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$ + "items[]._children[] when returned", "items[]._childrenTruncated when true", //$NON-NLS-1$ //$NON-NLS-2$ + "items[]._address when no address column", //$NON-NLS-1$ + "items[]._meta.value.kind when previewed", //$NON-NLS-1$ + "items[]._meta.value.length when previewed", //$NON-NLS-1$ + "items[]._meta.value.encoding when byte[] previewed"))); //$NON-NLS-1$ definitions.put(CliCommand.TOP_CONSUMERS, new CommandDefinition(CliCommand.TOP_CONSUMERS, - "Show the largest dominators grouped the same way as MAT top consumers.", - "mat-cli top-consumers [--limit N] [--depth N] [--format text|json]", true, - Collections.singletonList("heap"), FORMAT_LIMIT_AND_DEPTH_OPTIONS, - Arrays.asList(output("text", "top-consumers", "Text report matching MAT top consumers."), - output("json", "top-consumers", - "Stable aggregated JSON without display-only duplicates.")), - Arrays.asList("histogram ", - "path2gc --object 0x..."), - "aggregated payload with biggestObjects, classes, classLoaders, and packages.", - Arrays.asList("totalRetainedHeap", "biggestObjects[]", - "biggestObjects[].objectAddress", "biggestObjectsTruncated", - "classes[]", "classes[].objectAddress", "classesTruncated", - "classLoaders[]", "classLoaders[].objectAddress", - "classLoadersTruncated", "packages", "packagesTruncated"))); + "Show the largest dominators grouped the same way as MAT top consumers.", //$NON-NLS-1$ + "mat-cli top-consumers [--limit N] [--depth N] [--format text|json]", true, //$NON-NLS-1$ + heapArgument, FORMAT_LIMIT_AND_DEPTH_OPTIONS, + Arrays.asList(output("text", "top-consumers", "Text report matching MAT top consumers."), //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ + output("json", "top-consumers", //$NON-NLS-1$ //$NON-NLS-2$ + "Stable aggregated JSON without display-only duplicates.")), //$NON-NLS-1$ + Arrays.asList("histogram ", "path2gc --object 0x..."), //$NON-NLS-1$ //$NON-NLS-2$ + "aggregated payload with biggestObjects, classes, classLoaders, and packages.", //$NON-NLS-1$ + Arrays.asList("totalRetainedHeap", "biggestObjects[]", "biggestObjects[].objectAddress", //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ + "biggestObjectsTruncated", "classes[]", "classes[].objectAddress", //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ + "classesTruncated", "classLoaders[]", "classLoaders[].objectAddress", //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ + "classLoadersTruncated", "packages", "packagesTruncated"))); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ definitions.put(CliCommand.PATH2GC, new CommandDefinition(CliCommand.PATH2GC, - "Find paths from an object to GC roots using MAT's native query.", - "mat-cli path2gc --object 0x... [--limit N] [--depth N] [--format text|json]", true, - Collections.singletonList("heap"), Arrays.asList(option("--object", "0x...", true, - "Object address to resolve from the snapshot."), - option("--limit", "N", false, "Limit children per level."), - option("--depth", "N", false, "Limit nested tree depth."), - option("--format", "text|json", false, "Select text or JSON output.")), - Arrays.asList(output("text", "tree", "Indented tree view."), - output("json", "tree", "Stable keyed tree JSON.")), - Arrays.asList("inspect-object --object 0x...", - "oql --query \"SELECT * FROM OBJECTS 0x...\""), - "tree payload with keyed nodes, stable paths, value kinds, sparse child arrays, and optional row addresses.", - Arrays.asList("items[]", "items[].path", "items[].valueKind", - "items[].hasChildren", "items[]._children[] when returned", - "items[]._childrenTruncated when true", "items[]._address when no address column"))); + "Find paths from an object to GC roots using MAT's native query.", //$NON-NLS-1$ + "mat-cli path2gc --object 0x... [--limit N] [--depth N] [--format text|json]", true, //$NON-NLS-1$ + heapArgument, + Arrays.asList(freeTextOption("--object", "0x...", true, "Object address to resolve from the snapshot."), //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ + freeTextOption("--limit", "N", false, "Limit children per level."), //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ + freeTextOption("--depth", "N", false, "Limit nested tree depth."), //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ + enumOption("--format", "text|json", false, "Select text or JSON output.", FORMAT_VALUES)), //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ + Arrays.asList(output("text", "tree", "Indented tree view."), //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ + output("json", "tree", "Stable keyed tree JSON.")), //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ + Arrays.asList("inspect-object --object 0x...", "oql --query \"SELECT * FROM OBJECTS 0x...\""), //$NON-NLS-1$ //$NON-NLS-2$ + "tree payload with keyed nodes, stable paths, value kinds, sparse child arrays, and optional row addresses.", //$NON-NLS-1$ + Arrays.asList("items[]", "items[].path", "items[].valueKind", "items[].hasChildren", //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$ + "items[]._children[] when returned", "items[]._childrenTruncated when true", //$NON-NLS-1$ //$NON-NLS-2$ + "items[]._address when no address column"))); //$NON-NLS-1$ definitions.put(CliCommand.OQL, new CommandDefinition(CliCommand.OQL, - "Run a MAT OQL query directly against the snapshot.", - "mat-cli oql --query \"...\" [--limit N] [--depth N] [--format text|json]", true, - Collections.singletonList("heap"), Arrays.asList(option("--query", "\"...\"", true, - "OQL query string."), - option("--query-file", "PATH", false, - "Read OQL text from a UTF-8 file to avoid shell quoting issues such as inner classes with '$'."), - option("--query-stdin", null, false, "Read OQL text from stdin."), - option("--limit", "N", false, "Limit rows, sections, or children per level."), - option("--depth", "N", false, "Limit nested tree or section depth."), - option("--format", "text|json", false, "Select text or JSON output.")), - Arrays.asList(output("text", "text|table|tree|pie", "Depends on the OQL result."), - output("json", "text|table|tree|pie", - "Stable JSON envelope around the resolved result kind.")), - Arrays.asList("histogram ", - "query --command \"histogram\""), - "same payload contract as the resolved result kind returned by the OQL query.", - Arrays.asList("resultKind", "items[] when table/tree", - "items[].path/valueKind/hasChildren when tree", - "slices[] when pie", "content when text"))); + "Run a MAT OQL query directly against the snapshot.", //$NON-NLS-1$ + "mat-cli oql --query \"...\" [--limit N] [--depth N] [--format text|json]", true, //$NON-NLS-1$ + heapArgument, + Arrays.asList(freeTextOption("--query", "\"...\"", true, "OQL query string."), //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ + fileOption("--query-file", "PATH", false, //$NON-NLS-1$ //$NON-NLS-2$ + "Read OQL text from a UTF-8 file to avoid shell quoting issues such as inner classes with '$'."), //$NON-NLS-1$ + flagOption("--query-stdin", "Read OQL text from stdin."), //$NON-NLS-1$ //$NON-NLS-2$ + freeTextOption("--limit", "N", false, "Limit rows, sections, or children per level."), //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ + freeTextOption("--depth", "N", false, "Limit nested tree or section depth."), //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ + enumOption("--format", "text|json", false, "Select text or JSON output.", FORMAT_VALUES)), //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ + Arrays.asList(output("text", "text|table|tree|pie", "Depends on the OQL result."), //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ + output("json", "text|table|tree|pie", //$NON-NLS-1$ //$NON-NLS-2$ + "Stable JSON envelope around the resolved result kind.")), //$NON-NLS-1$ + Arrays.asList("histogram ", "query --command \"histogram\""), //$NON-NLS-1$ //$NON-NLS-2$ + "same payload contract as the resolved result kind returned by the OQL query.", //$NON-NLS-1$ + Arrays.asList("resultKind", "items[] when table/tree", "items[].path/valueKind/hasChildren when tree", //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ + "slices[] when pie", "content when text"))); //$NON-NLS-1$ //$NON-NLS-2$ definitions.put(CliCommand.QUERY, new CommandDefinition(CliCommand.QUERY, - "Run a MAT query command string through SnapshotQuery.parse(...).", - "mat-cli query --command \"...\" [--limit N] [--depth N] [--format text|json]", true, - Collections.singletonList("heap"), Arrays.asList(option("--command", "\"...\"", true, - "MAT query command string."), - option("--command-file", "PATH", false, - "Read MAT query text from a UTF-8 file to avoid shell quoting issues such as inner classes with '$'."), - option("--command-stdin", null, false, "Read MAT query text from stdin."), - option("--limit", "N", false, "Limit rows, sections, or children per level."), - option("--depth", "N", false, "Limit nested tree or section depth."), - option("--format", "text|json", false, "Select text or JSON output.")), - Arrays.asList(output("text", "text|table|tree|section|pie|top-consumers", + "Run a MAT query command string through SnapshotQuery.parse(...).", //$NON-NLS-1$ + "mat-cli query --command \"...\" [--limit N] [--depth N] [--format text|json]", true, //$NON-NLS-1$ + heapArgument, + Arrays.asList(freeTextOption("--command", "\"...\"", true, "MAT query command string."), //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ + fileOption("--command-file", "PATH", false, //$NON-NLS-1$ //$NON-NLS-2$ + "Read MAT query text from a UTF-8 file to avoid shell quoting issues such as inner classes with '$'."), //$NON-NLS-1$ + flagOption("--command-stdin", "Read MAT query text from stdin."), //$NON-NLS-1$ //$NON-NLS-2$ + freeTextOption("--limit", "N", false, "Limit rows, sections, or children per level."), //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ + freeTextOption("--depth", "N", false, "Limit nested tree or section depth."), //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ + enumOption("--format", "text|json", false, "Select text or JSON output.", FORMAT_VALUES)), //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ + Arrays.asList(output("text", "text|table|tree|section|pie|top-consumers", //$NON-NLS-1$ //$NON-NLS-2$ "Depends on the resolved query result."), - output("json", "text|table|tree|section|pie|top-consumers", - "Stable JSON envelope around the resolved result kind.")), - Arrays.asList("schema histogram", - "describe top-consumers"), - "same payload contract as the resolved result kind returned by the parsed query.", - Arrays.asList("resultKind", "items[] when table/tree", - "items[].path/valueKind/hasChildren when tree", - "slices[] when pie", "sections[] when section"))); + output("json", "text|table|tree|section|pie|top-consumers", //$NON-NLS-1$ //$NON-NLS-2$ + "Stable JSON envelope around the resolved result kind.")), //$NON-NLS-1$ + Arrays.asList("schema histogram", "describe top-consumers"), //$NON-NLS-1$ //$NON-NLS-2$ + "same payload contract as the resolved result kind returned by the parsed query.", //$NON-NLS-1$ + Arrays.asList("resultKind", "items[] when table/tree", "items[].path/valueKind/hasChildren when tree", //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ + "slices[] when pie", "sections[] when section"))); //$NON-NLS-1$ //$NON-NLS-2$ definitions.put(CliCommand.DESCRIBE, new CommandDefinition(CliCommand.DESCRIBE, - "Describe a CLI command, its options, and the result kinds it can return.", - "mat-cli describe [--format text|json]", false, - Collections.singletonList("command"), FORMAT_OPTION, - Arrays.asList(output("text", "describe", "Human-readable command description."), - output("json", "describe", "Stable command metadata JSON.")), - Arrays.asList("schema "), "metadata payload for one command definition.", - Arrays.asList("name", "summary", "usage", "requiresSnapshot", "options[]", "outputs[]", - "suggestedNextCommands via top-level envelope"))); + "Describe a CLI command, its options, and the result kinds it can return.", //$NON-NLS-1$ + "mat-cli describe [--format text|json]", false, commandArgument, FORMAT_OPTION, //$NON-NLS-1$ + Arrays.asList(output("text", "describe", "Human-readable command description."), //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ + output("json", "describe", "Stable command metadata JSON.")), //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ + Arrays.asList("schema "), "metadata payload for one command definition.", //$NON-NLS-1$ //$NON-NLS-2$ + Arrays.asList("name", "summary", "usage", "requiresSnapshot", "options[]", "outputs[]", //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$ //$NON-NLS-5$ //$NON-NLS-6$ + "suggestedNextCommands via top-level envelope"))); //$NON-NLS-1$ definitions.put(CliCommand.SCHEMA, new CommandDefinition(CliCommand.SCHEMA, - "Describe the stable JSON contract for a CLI command.", - "mat-cli schema [--format text|json]", false, - Collections.singletonList("command"), FORMAT_OPTION, - Arrays.asList(output("text", "schema", "Readable contract summary."), - output("json", "schema", "Stable contract metadata JSON.")), - Arrays.asList("describe "), "JSON envelope and payload contract summary.", - Arrays.asList("jsonEnvelope", "payloadKind", "payloadFields[]", "outputs[]"))); + "Describe the stable JSON contract for a CLI command.", //$NON-NLS-1$ + "mat-cli schema [--format text|json]", false, commandArgument, FORMAT_OPTION, //$NON-NLS-1$ + Arrays.asList(output("text", "schema", "Readable contract summary."), //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ + output("json", "schema", "Stable contract metadata JSON.")), //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ + Arrays.asList("describe "), "JSON envelope and payload contract summary.", //$NON-NLS-1$ //$NON-NLS-2$ + Arrays.asList("jsonEnvelope", "payloadKind", "payloadFields[]", "outputs[]"))); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$ definitions.put(CliCommand.LIST_QUERIES, new CommandDefinition(CliCommand.LIST_QUERIES, - "List the registered MAT query commands available through SnapshotQuery/QueryRegistry.", - "mat-cli list-queries [--format text|json]", false, Collections.emptyList(), + "List the registered MAT query commands available through SnapshotQuery/QueryRegistry.", //$NON-NLS-1$ + "mat-cli list-queries [--format text|json]", false, Collections.emptyList(), //$NON-NLS-1$ FORMAT_OPTION, - Arrays.asList(output("text", "query-list", "List MAT query identifiers and summaries."), - output("json", "query-list", "Stable query registry metadata JSON.")), - Arrays.asList("describe-query histogram", - "query --command \"histogram\""), - "array of MAT query descriptors including identifiers, usage, and arguments.", - Arrays.asList("queries[]", "queries[].identifier", "queries[].usage", - "queries[].arguments[]"))); + Arrays.asList(output("text", "query-list", "List MAT query identifiers and summaries."), //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ + output("json", "query-list", "Stable query registry metadata JSON.")), //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ + Arrays.asList("describe-query histogram", "query --command \"histogram\""), //$NON-NLS-1$ //$NON-NLS-2$ + "array of MAT query descriptors including identifiers, usage, and arguments.", //$NON-NLS-1$ + Arrays.asList("queries[]", "queries[].identifier", "queries[].usage", "queries[].arguments[]"))); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$ definitions.put(CliCommand.DESCRIBE_QUERY, new CommandDefinition(CliCommand.DESCRIBE_QUERY, - "Describe one registered MAT query, including its arguments and help text.", - "mat-cli describe-query [--format text|json]", false, - Collections.singletonList("query-id"), FORMAT_OPTION, - Arrays.asList(output("text", "query-description", "Human-readable MAT query metadata."), - output("json", "query-description", "Stable MAT query metadata JSON.")), - Arrays.asList("list-queries", - "query --command \"\""), - "single MAT query descriptor with argument metadata, help, and subjects.", - Arrays.asList("query.identifier", "query.usage", "query.arguments[]", "query.subjects[]"))); - return definitions; + "Describe one registered MAT query, including its arguments and help text.", //$NON-NLS-1$ + "mat-cli describe-query [--format text|json]", false, queryIdArgument, //$NON-NLS-1$ + FORMAT_OPTION, + Arrays.asList(output("text", "query-description", "Human-readable MAT query metadata."), //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ + output("json", "query-description", "Stable MAT query metadata JSON.")), //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ + Arrays.asList("list-queries", "query --command \"\""), //$NON-NLS-1$ //$NON-NLS-2$ + "single MAT query descriptor with argument metadata, help, and subjects.", //$NON-NLS-1$ + Arrays.asList("query.identifier", "query.usage", "query.arguments[]", "query.subjects[]"))); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$ + definitions.put(CliCommand.COMPLETION, new CommandDefinition(CliCommand.COMPLETION, + "Generate a bash or zsh shell completion script for mat-cli.", //$NON-NLS-1$ + "mat-cli completion [--format text|json]", false, completionShellArgument, //$NON-NLS-1$ + FORMAT_OPTION, + Arrays.asList(output("text", "text", "Shell completion script."), //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ + output("json", "text", "Shell completion script in the standard text envelope.")), //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ + Arrays.asList("completion bash", "completion zsh"), //$NON-NLS-1$ //$NON-NLS-2$ + "plain-text shell completion script.", //$NON-NLS-1$ + Arrays.asList("text"))); //$NON-NLS-1$ + return Collections.unmodifiableMap(definitions); } - private static OptionDefinition option(String name, String valueHint, boolean required, String description) + private static OptionDefinition enumOption(String name, String valueHint, boolean required, String description, + List completionCandidates) { - return new OptionDefinition(name, valueHint, required, description); + return new OptionDefinition(name, valueHint, required, description, CompletionValueType.ENUM, + completionCandidates); + } + + private static OptionDefinition freeTextOption(String name, String valueHint, boolean required, String description) + { + return new OptionDefinition(name, valueHint, required, description, CompletionValueType.FREE_TEXT, null); + } + + private static OptionDefinition fileOption(String name, String valueHint, boolean required, String description) + { + return new OptionDefinition(name, valueHint, required, description, CompletionValueType.FILE, null); + } + + private static OptionDefinition flagOption(String name, String description) + { + return new OptionDefinition(name, null, false, description, CompletionValueType.NONE, null); + } + + private static PositionalDefinition enumPositional(String name, List completionCandidates) + { + return new PositionalDefinition(name, CompletionValueType.ENUM, completionCandidates); + } + + private static PositionalDefinition freeTextPositional(String name) + { + return new PositionalDefinition(name, CompletionValueType.FREE_TEXT, null); + } + + private static PositionalDefinition filePositional(String name) + { + return new PositionalDefinition(name, CompletionValueType.FILE, null); } private static Map queryLimitOverrides() @@ -404,8 +528,25 @@ private static Map queryLimitOverrides() return Collections.unmodifiableMap(limits); } + private static List buildCommandTokens() + { + List tokens = new ArrayList(CliCommand.values().length); + for (CliCommand command : CliCommand.values()) + { + tokens.add(command.getToken()); + } + return Collections.unmodifiableList(tokens); + } + private static OutputDefinition output(String format, String resultKind, String description) { return new OutputDefinition(format, resultKind, description); } + + private static List immutableCopy(List values) + { + if (values == null || values.isEmpty()) + return Collections.emptyList(); + return Collections.unmodifiableList(new ArrayList(values)); + } } diff --git a/plugins/org.eclipse.mat.cli/src/org/eclipse/mat/cli/internal/CliCommandExecutor.java b/plugins/org.eclipse.mat.cli/src/org/eclipse/mat/cli/internal/CliCommandExecutor.java index 57526610..b7a383e1 100644 --- a/plugins/org.eclipse.mat.cli/src/org/eclipse/mat/cli/internal/CliCommandExecutor.java +++ b/plugins/org.eclipse.mat.cli/src/org/eclipse/mat/cli/internal/CliCommandExecutor.java @@ -62,6 +62,9 @@ public CliExecution execute(CliArguments arguments) throws Exception return CliExecution.result(new QueryMetadataCollector().listQueries()); case DESCRIBE_QUERY: return CliExecution.result(new QueryMetadataCollector().describeQuery(arguments.getSubjectName())); + case COMPLETION: + return CliExecution.result(new TextResult( + new CompletionScriptGenerator().generate(arguments.getCompletionShell()))); default: throw CliException.execution("Command requires a snapshot session: " + arguments.getCommand().getToken(), //$NON-NLS-1$ null); @@ -220,6 +223,7 @@ private CliExecution execute(CliArguments arguments, ISnapshot snapshot, IProgre case SCHEMA: case LIST_QUERIES: case DESCRIBE_QUERY: + case COMPLETION: return execute(arguments); default: throw CliException.usage("Unsupported command: " + arguments.getCommand().getToken()); //$NON-NLS-1$ diff --git a/plugins/org.eclipse.mat.cli/src/org/eclipse/mat/cli/internal/CliHelp.java b/plugins/org.eclipse.mat.cli/src/org/eclipse/mat/cli/internal/CliHelp.java index 24a809ea..3a03800c 100644 --- a/plugins/org.eclipse.mat.cli/src/org/eclipse/mat/cli/internal/CliHelp.java +++ b/plugins/org.eclipse.mat.cli/src/org/eclipse/mat/cli/internal/CliHelp.java @@ -31,6 +31,7 @@ public static String generalHelp() help.append(" mat-cli schema [options]\n"); //$NON-NLS-1$ help.append(" mat-cli list-queries [options]\n"); //$NON-NLS-1$ help.append(" mat-cli describe-query [options]\n"); //$NON-NLS-1$ + help.append(" mat-cli completion [options]\n"); //$NON-NLS-1$ help.append(" mat-cli --help\n"); //$NON-NLS-1$ help.append(" mat-cli --help\n\n"); //$NON-NLS-1$ help.append("Commands:\n"); //$NON-NLS-1$ diff --git a/plugins/org.eclipse.mat.cli/src/org/eclipse/mat/cli/internal/CompletionScriptGenerator.java b/plugins/org.eclipse.mat.cli/src/org/eclipse/mat/cli/internal/CompletionScriptGenerator.java new file mode 100644 index 00000000..ab100921 --- /dev/null +++ b/plugins/org.eclipse.mat.cli/src/org/eclipse/mat/cli/internal/CompletionScriptGenerator.java @@ -0,0 +1,549 @@ +/******************************************************************************* + * Copyright (c) 2026 Eclipse Memory Analyzer Project. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + *******************************************************************************/ +package org.eclipse.mat.cli.internal; + +import java.util.ArrayList; +import java.util.List; + +public final class CompletionScriptGenerator +{ + public String generate(String shell) throws CliException + { + if ("bash".equals(shell)) //$NON-NLS-1$ + return generateBash(); + if ("zsh".equals(shell)) //$NON-NLS-1$ + return generateZsh(); + throw CliException.usage("Unsupported completion shell: " + shell + " (expected bash or zsh)"); //$NON-NLS-1$ //$NON-NLS-2$ + } + + private String generateBash() + { + StringBuilder builder = new StringBuilder(16384); + builder.append("# bash completion for mat-cli\n"); //$NON-NLS-1$ + builder.append("_mat_cli_is_command() {\n"); //$NON-NLS-1$ + builder.append(" case \"$1\" in\n"); //$NON-NLS-1$ + builder.append(" ").append(joinWithPipe(CliCommandCatalog.commandTokens())).append(")\n"); //$NON-NLS-1$ + builder.append(" return 0\n"); //$NON-NLS-1$ + builder.append(" ;;\n"); //$NON-NLS-1$ + builder.append(" esac\n"); //$NON-NLS-1$ + builder.append(" return 1\n"); //$NON-NLS-1$ + builder.append("}\n\n"); //$NON-NLS-1$ + + appendKindHelpers(builder, false); + appendOptionKindFunctions(builder, false); + appendOptionsForCommandFunction(builder, false); + appendPositionalKindFunction(builder, false); + + builder.append("_mat_cli_set_word_replies() {\n"); //$NON-NLS-1$ + builder.append(" local prefix=\"$1\"\n"); //$NON-NLS-1$ + builder.append(" local current=\"$2\"\n"); //$NON-NLS-1$ + builder.append(" local values=\"$3\"\n"); //$NON-NLS-1$ + builder.append(" local i\n"); //$NON-NLS-1$ + builder.append(" COMPREPLY=( $(compgen -W \"$values\" -- \"$current\") )\n"); //$NON-NLS-1$ + builder.append(" if [ -n \"$prefix\" ]; then\n"); //$NON-NLS-1$ + builder.append(" for ((i=0; i<${#COMPREPLY[@]}; i++)); do\n"); //$NON-NLS-1$ + builder.append(" COMPREPLY[i]=\"$prefix${COMPREPLY[i]}\"\n"); //$NON-NLS-1$ + builder.append(" done\n"); //$NON-NLS-1$ + builder.append(" fi\n"); //$NON-NLS-1$ + builder.append("}\n\n"); //$NON-NLS-1$ + + builder.append("_mat_cli_set_file_replies() {\n"); //$NON-NLS-1$ + builder.append(" local prefix=\"$1\"\n"); //$NON-NLS-1$ + builder.append(" local current=\"$2\"\n"); //$NON-NLS-1$ + builder.append(" local line\n"); //$NON-NLS-1$ + builder.append(" COMPREPLY=()\n"); //$NON-NLS-1$ + builder.append(" while IFS= read -r line; do\n"); //$NON-NLS-1$ + builder.append(" if [ -n \"$prefix\" ]; then\n"); //$NON-NLS-1$ + builder.append(" COMPREPLY+=(\"$prefix$line\")\n"); //$NON-NLS-1$ + builder.append(" else\n"); //$NON-NLS-1$ + builder.append(" COMPREPLY+=(\"$line\")\n"); //$NON-NLS-1$ + builder.append(" fi\n"); //$NON-NLS-1$ + builder.append(" done < <(compgen -f -- \"$current\")\n"); //$NON-NLS-1$ + builder.append(" if type compopt >/dev/null 2>&1; then\n"); //$NON-NLS-1$ + builder.append(" compopt -o filenames 2>/dev/null\n"); //$NON-NLS-1$ + builder.append(" fi\n"); //$NON-NLS-1$ + builder.append("}\n\n"); //$NON-NLS-1$ + + builder.append("_mat_cli_complete_kind() {\n"); //$NON-NLS-1$ + builder.append(" local kind=\"$1\"\n"); //$NON-NLS-1$ + builder.append(" local current=\"$2\"\n"); //$NON-NLS-1$ + builder.append(" local prefix=\"$3\"\n"); //$NON-NLS-1$ + builder.append(" case \"$kind\" in\n"); //$NON-NLS-1$ + builder.append(" enum:*)\n"); //$NON-NLS-1$ + builder.append(" _mat_cli_set_word_replies \"$prefix\" \"$current\" \"${kind#enum:}\"\n"); //$NON-NLS-1$ + builder.append(" ;;\n"); //$NON-NLS-1$ + builder.append(" file)\n"); //$NON-NLS-1$ + builder.append(" _mat_cli_set_file_replies \"$prefix\" \"$current\"\n"); //$NON-NLS-1$ + builder.append(" ;;\n"); //$NON-NLS-1$ + builder.append(" *)\n"); //$NON-NLS-1$ + builder.append(" COMPREPLY=()\n"); //$NON-NLS-1$ + builder.append(" ;;\n"); //$NON-NLS-1$ + builder.append(" esac\n"); //$NON-NLS-1$ + builder.append("}\n\n"); //$NON-NLS-1$ + + builder.append("_mat_cli_find_command_index() {\n"); //$NON-NLS-1$ + builder.append(" local i word pending_kind=\"\"\n"); //$NON-NLS-1$ + builder.append(" for ((i=1; i/dev/null 2>&1\n"); //$NON-NLS-1$ + builder.append(" _files -P \"$prefix\"\n"); //$NON-NLS-1$ + builder.append(" else\n"); //$NON-NLS-1$ + builder.append(" _files\n"); //$NON-NLS-1$ + builder.append(" fi\n"); //$NON-NLS-1$ + builder.append(" ;;\n"); //$NON-NLS-1$ + builder.append(" *)\n"); //$NON-NLS-1$ + builder.append(" return 1\n"); //$NON-NLS-1$ + builder.append(" ;;\n"); //$NON-NLS-1$ + builder.append(" esac\n"); //$NON-NLS-1$ + builder.append("}\n\n"); //$NON-NLS-1$ + + builder.append("_mat_cli_find_command() {\n"); //$NON-NLS-1$ + builder.append(" local i word pending_kind=\"\"\n"); //$NON-NLS-1$ + builder.append(" reply=()\n"); //$NON-NLS-1$ + builder.append(" for ((i=2; i/dev/null 2>&1\n"); //$NON-NLS-1$ + builder.append(" _mat_cli_complete_kind \"$kind\" \"$inline_prefix\"\n"); //$NON-NLS-1$ + builder.append(" return 0\n"); //$NON-NLS-1$ + builder.append(" fi\n"); //$NON-NLS-1$ + builder.append(" fi\n"); //$NON-NLS-1$ + builder.append(" kind=\"$(_mat_cli_option_kind_global \"$prev\")\"\n"); //$NON-NLS-1$ + builder.append(" if _mat_cli_kind_expects_value \"$kind\"; then\n"); //$NON-NLS-1$ + builder.append(" _mat_cli_complete_kind \"$kind\" \"\"\n"); //$NON-NLS-1$ + builder.append(" return 0\n"); //$NON-NLS-1$ + builder.append(" fi\n"); //$NON-NLS-1$ + builder.append(" _mat_cli_complete_words \"\" ").append(quotedWords(topLevelWords())).append('\n'); + builder.append(" return 0\n"); //$NON-NLS-1$ + builder.append(" fi\n\n"); //$NON-NLS-1$ + + builder.append(" cmd=\"$reply[1]\"\n"); //$NON-NLS-1$ + builder.append(" cmd_index=\"$reply[2]\"\n"); //$NON-NLS-1$ + builder.append(" if [[ \"$cur\" == --*=* ]]; then\n"); //$NON-NLS-1$ + builder.append(" inline_option=\"${cur%%=*}\"\n"); //$NON-NLS-1$ + builder.append(" inline_prefix=\"$inline_option=\"\n"); //$NON-NLS-1$ + builder.append(" kind=\"$(_mat_cli_option_kind \"$cmd\" \"$inline_option\")\"\n"); //$NON-NLS-1$ + builder.append(" if [[ \"$kind\" == enum:* || \"$kind\" == file ]]; then\n"); //$NON-NLS-1$ + builder.append(" compset -P \"$inline_prefix\" >/dev/null 2>&1\n"); //$NON-NLS-1$ + builder.append(" _mat_cli_complete_kind \"$kind\" \"$inline_prefix\"\n"); //$NON-NLS-1$ + builder.append(" return 0\n"); //$NON-NLS-1$ + builder.append(" fi\n"); //$NON-NLS-1$ + builder.append(" fi\n"); //$NON-NLS-1$ + builder.append(" kind=\"$(_mat_cli_option_kind \"$cmd\" \"$prev\")\"\n"); //$NON-NLS-1$ + builder.append(" if _mat_cli_kind_expects_value \"$kind\"; then\n"); //$NON-NLS-1$ + builder.append(" _mat_cli_complete_kind \"$kind\" \"\"\n"); //$NON-NLS-1$ + builder.append(" return 0\n"); //$NON-NLS-1$ + builder.append(" fi\n\n"); //$NON-NLS-1$ + + builder.append(" position_index=0\n"); //$NON-NLS-1$ + builder.append(" pending_kind=\"\"\n"); //$NON-NLS-1$ + builder.append(" for ((i=cmd_index+1; i/dev/null\n"); //$NON-NLS-1$ + return builder.toString(); + } + + private void appendKindHelpers(StringBuilder builder, boolean zsh) + { + builder.append("_mat_cli_kind_expects_value() {\n"); //$NON-NLS-1$ + if (zsh) + builder.append(" [[ -n \"$1\" && \"$1\" != none ]]\n"); //$NON-NLS-1$ + else + builder.append(" [ -n \"$1\" ] && [ \"$1\" != \"none\" ]\n"); //$NON-NLS-1$ + builder.append("}\n\n"); //$NON-NLS-1$ + } + + private void appendOptionKindFunctions(StringBuilder builder, boolean zsh) + { + appendOptionKindFunction(builder, "_mat_cli_option_kind_global", CliCommandCatalog.globalOptions()); //$NON-NLS-1$ + + builder.append("_mat_cli_option_kind_for_command() {\n"); //$NON-NLS-1$ + builder.append(" case \"$1:$2\" in\n"); //$NON-NLS-1$ + for (CliCommand command : CliCommand.values()) + { + CliCommandCatalog.CommandDefinition definition = CliCommandCatalog.lookup(command); + if (definition == null) + continue; + for (CliCommandCatalog.OptionDefinition option : definition.getOptions()) + { + builder.append(" ").append(command.getToken()).append(':').append(option.getName()).append(")\n"); //$NON-NLS-1$ + builder.append(" printf '%s\\n' '").append(completionKind(option)).append("'\n"); //$NON-NLS-1$ //$NON-NLS-2$ + builder.append(" ;;\n"); //$NON-NLS-1$ + } + } + builder.append(" esac\n"); //$NON-NLS-1$ + builder.append("}\n\n"); //$NON-NLS-1$ + + builder.append("_mat_cli_option_kind() {\n"); //$NON-NLS-1$ + builder.append(" local kind\n"); //$NON-NLS-1$ + builder.append(" kind=\"$(_mat_cli_option_kind_for_command \"$1\" \"$2\")\"\n"); //$NON-NLS-1$ + if (zsh) + { + builder.append(" if [[ -n \"$kind\" ]]; then\n"); //$NON-NLS-1$ + builder.append(" print -r -- \"$kind\"\n"); //$NON-NLS-1$ + } + else + { + builder.append(" if [ -n \"$kind\" ]; then\n"); //$NON-NLS-1$ + builder.append(" printf '%s\\n' \"$kind\"\n"); //$NON-NLS-1$ + } + builder.append(" return 0\n"); //$NON-NLS-1$ + builder.append(" fi\n"); //$NON-NLS-1$ + builder.append(" _mat_cli_option_kind_global \"$2\"\n"); //$NON-NLS-1$ + builder.append("}\n\n"); //$NON-NLS-1$ + } + + private void appendOptionKindFunction(StringBuilder builder, String functionName, + List options) + { + builder.append(functionName).append("() {\n"); //$NON-NLS-1$ + builder.append(" case \"$1\" in\n"); //$NON-NLS-1$ + for (CliCommandCatalog.OptionDefinition option : options) + { + builder.append(" ").append(option.getName()).append(")\n"); //$NON-NLS-1$ + builder.append(" printf '%s\\n' '").append(completionKind(option)).append("'\n"); //$NON-NLS-1$ //$NON-NLS-2$ + builder.append(" ;;\n"); //$NON-NLS-1$ + } + builder.append(" esac\n"); //$NON-NLS-1$ + builder.append("}\n\n"); //$NON-NLS-1$ + } + + private void appendOptionsForCommandFunction(StringBuilder builder, boolean zsh) + { + builder.append("_mat_cli_options_for_command() {\n"); //$NON-NLS-1$ + builder.append(" case \"$1\" in\n"); //$NON-NLS-1$ + for (CliCommand command : CliCommand.values()) + { + List options = new ArrayList(); + for (CliCommandCatalog.OptionDefinition option : CliCommandCatalog.completionOptions(command)) + { + options.add(option.getName()); + } + builder.append(" ").append(command.getToken()).append(")\n"); //$NON-NLS-1$ + if (zsh) + builder.append(" print -r -- \"").append(joinWords(options)).append("\"\n"); //$NON-NLS-1$ //$NON-NLS-2$ + else + builder.append(" printf '%s\\n' '").append(joinWords(options)).append("'\n"); //$NON-NLS-1$ //$NON-NLS-2$ + builder.append(" ;;\n"); //$NON-NLS-1$ + } + builder.append(" esac\n"); //$NON-NLS-1$ + builder.append("}\n\n"); //$NON-NLS-1$ + } + + private void appendPositionalKindFunction(StringBuilder builder, boolean zsh) + { + builder.append("_mat_cli_positional_kind() {\n"); //$NON-NLS-1$ + builder.append(" case \"$1:$2\" in\n"); //$NON-NLS-1$ + for (CliCommand command : CliCommand.values()) + { + CliCommandCatalog.CommandDefinition definition = CliCommandCatalog.lookup(command); + if (definition == null) + continue; + List positionals = definition.getPositionalDefinitions(); + for (int ii = 0; ii < positionals.size(); ii++) + { + builder.append(" ").append(command.getToken()).append(':').append(ii).append(")\n"); //$NON-NLS-1$ + if (zsh) + builder.append(" print -r -- '").append(completionKind(positionals.get(ii))).append("'\n"); //$NON-NLS-1$ //$NON-NLS-2$ + else + builder.append(" printf '%s\\n' '").append(completionKind(positionals.get(ii))).append("'\n"); //$NON-NLS-1$ //$NON-NLS-2$ + builder.append(" ;;\n"); //$NON-NLS-1$ + } + } + builder.append(" esac\n"); //$NON-NLS-1$ + builder.append("}\n\n"); //$NON-NLS-1$ + } + + private List topLevelWords() + { + List words = new ArrayList(); + words.addAll(CliCommandCatalog.commandTokens()); + for (CliCommandCatalog.OptionDefinition option : CliCommandCatalog.globalOptions()) + { + words.add(option.getName()); + } + return words; + } + + private String completionKind(CliCommandCatalog.OptionDefinition option) + { + return completionKind(option.getCompletionValueType(), option.getCompletionCandidates()); + } + + private String completionKind(CliCommandCatalog.PositionalDefinition positional) + { + return completionKind(positional.getCompletionValueType(), positional.getCompletionCandidates()); + } + + private String completionKind(CompletionValueType type, List candidates) + { + switch (type) + { + case ENUM: + return "enum:" + joinWords(candidates); //$NON-NLS-1$ + case FILE: + return "file"; //$NON-NLS-1$ + case FREE_TEXT: + return "free-text"; //$NON-NLS-1$ + case NONE: + default: + return "none"; //$NON-NLS-1$ + } + } + + private String joinWithPipe(List values) + { + StringBuilder builder = new StringBuilder(); + for (int ii = 0; ii < values.size(); ii++) + { + if (ii > 0) + builder.append('|'); + builder.append(values.get(ii)); + } + return builder.toString(); + } + + private String joinWords(List values) + { + StringBuilder builder = new StringBuilder(); + for (int ii = 0; ii < values.size(); ii++) + { + if (ii > 0) + builder.append(' '); + builder.append(values.get(ii)); + } + return builder.toString(); + } + + private String quotedWords(List values) + { + StringBuilder builder = new StringBuilder(); + for (String value : values) + { + if (builder.length() > 0) + builder.append(' '); + builder.append('"').append(value).append('"'); + } + return builder.toString(); + } +} diff --git a/plugins/org.eclipse.mat.cli/src/org/eclipse/mat/cli/internal/CompletionValueType.java b/plugins/org.eclipse.mat.cli/src/org/eclipse/mat/cli/internal/CompletionValueType.java new file mode 100644 index 00000000..26c75905 --- /dev/null +++ b/plugins/org.eclipse.mat.cli/src/org/eclipse/mat/cli/internal/CompletionValueType.java @@ -0,0 +1,20 @@ +/******************************************************************************* + * Copyright (c) 2026 Eclipse Memory Analyzer Project. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + *******************************************************************************/ +package org.eclipse.mat.cli.internal; + +public enum CompletionValueType +{ + NONE, ENUM, FILE, FREE_TEXT; + + public boolean expectsValue() + { + return this != NONE; + } +} diff --git a/plugins/org.eclipse.mat.tests/src/org/eclipse/mat/tests/AllTests.java b/plugins/org.eclipse.mat.tests/src/org/eclipse/mat/tests/AllTests.java index d0387369..f95b7543 100644 --- a/plugins/org.eclipse.mat.tests/src/org/eclipse/mat/tests/AllTests.java +++ b/plugins/org.eclipse.mat.tests/src/org/eclipse/mat/tests/AllTests.java @@ -49,6 +49,7 @@ org.eclipse.mat.tests.cli.CliArgumentParserTest.class, // org.eclipse.mat.tests.cli.ResultSerializerTest.class, // org.eclipse.mat.tests.cli.CliCommandExecutorTest.class, // + org.eclipse.mat.tests.cli.CompletionScriptGeneratorTest.class, // org.eclipse.mat.tests.ui.snapshot.panes.textPartitioning.TestClassNameExtractor.class, org.eclipse.mat.tests.ui.snapshot.panes.textPartitioning.TestOQLPartitionScanner.class, // org.eclipse.mat.tests.report.ParametersExpandTest.class }) diff --git a/plugins/org.eclipse.mat.tests/src/org/eclipse/mat/tests/cli/CliArgumentParserTest.java b/plugins/org.eclipse.mat.tests/src/org/eclipse/mat/tests/cli/CliArgumentParserTest.java index 8f1a586f..2855638c 100644 --- a/plugins/org.eclipse.mat.tests/src/org/eclipse/mat/tests/cli/CliArgumentParserTest.java +++ b/plugins/org.eclipse.mat.tests/src/org/eclipse/mat/tests/cli/CliArgumentParserTest.java @@ -359,6 +359,62 @@ public void parsesDescribeQueryCommandWithoutHeap() throws Exception assertEquals(CliArguments.OutputFormat.JSON, arguments.getFormat()); } + @Test + public void parsesCompletionCommandForBash() throws Exception + { + CliArgumentParser parser = new CliArgumentParser(); + CliArguments arguments = parser.parse(new String[] { "completion", "bash" }); //$NON-NLS-1$ //$NON-NLS-2$ + + assertEquals(CliCommand.COMPLETION, arguments.getCommand()); + assertEquals("bash", arguments.getCompletionShell()); //$NON-NLS-1$ + assertEquals("bash", arguments.getSubjectName()); //$NON-NLS-1$ + } + + @Test + public void parsesCompletionCommandForZshInJsonMode() throws Exception + { + CliArgumentParser parser = new CliArgumentParser(); + CliArguments arguments = parser.parse(new String[] { "completion", "zsh", "--format", "json" }); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$ + + assertEquals(CliCommand.COMPLETION, arguments.getCommand()); + assertEquals("zsh", arguments.getCompletionShell()); //$NON-NLS-1$ + assertEquals(CliArguments.OutputFormat.JSON, arguments.getFormat()); + } + + @Test + public void rejectsCompletionCommandWithoutShell() throws Exception + { + CliArgumentParser parser = new CliArgumentParser(); + + try + { + parser.parse(new String[] { "completion" }); //$NON-NLS-1$ + fail("Expected missing completion shell to be rejected"); //$NON-NLS-1$ + } + catch (CliException e) + { + assertEquals(2, e.getExitCode()); + assertTrue(e.getMessage().contains("completion requires a shell name")); //$NON-NLS-1$ + } + } + + @Test + public void rejectsUnsupportedCompletionShell() throws Exception + { + CliArgumentParser parser = new CliArgumentParser(); + + try + { + parser.parse(new String[] { "completion", "fish" }); //$NON-NLS-1$ //$NON-NLS-2$ + fail("Expected unsupported completion shell to be rejected"); //$NON-NLS-1$ + } + catch (CliException e) + { + assertEquals(2, e.getExitCode()); + assertTrue(e.getMessage().contains("Unsupported completion shell: fish")); //$NON-NLS-1$ + } + } + @Test public void parsesListQueriesCommandWithoutHeap() throws Exception { @@ -537,6 +593,7 @@ public void helpIncludesDepthAndTopConsumersLimit() assertTrue(help.contains("instances [--class | --class-regex | --class-contains ] [--include-subclasses] [--limit N] [--format text|json]")); //$NON-NLS-1$ assertTrue(help.contains("inspect-object --object 0x... [--select-fields FIELD | --field-paths PATH] [--show-nulls] [--limit N] [--depth N] [--format text|json]")); //$NON-NLS-1$ assertTrue(help.contains("top-consumers [--limit N] [--depth N] [--format text|json]")); //$NON-NLS-1$ + assertTrue(help.contains("completion [--format text|json]")); //$NON-NLS-1$ assertTrue(help.contains("--depth N Maximum tree or section depth (default: 8, inspect-object: 3)")); //$NON-NLS-1$ assertTrue(help.contains("--field-paths PATH Inspect dotted field paths such as cleaner.offsetMap; may be repeated")); //$NON-NLS-1$ assertTrue(help.contains("--select-fields FIELD Inspect direct fields from the root object; may be repeated")); //$NON-NLS-1$ @@ -569,4 +626,14 @@ public void commandHelpForDescribeShowsDescribeUsage() assertTrue(help.contains("Usage: mat-cli describe [--format text|json]")); //$NON-NLS-1$ assertTrue(help.contains("Positional arguments:")); //$NON-NLS-1$ } + + @Test + public void commandHelpForCompletionShowsShellUsage() + { + String help = CliHelp.commandHelp(CliCommand.COMPLETION); + + assertTrue(help.contains("Command: completion")); //$NON-NLS-1$ + assertTrue(help.contains("Usage: mat-cli completion [--format text|json]")); //$NON-NLS-1$ + assertTrue(help.contains("bash|zsh")); //$NON-NLS-1$ + } } diff --git a/plugins/org.eclipse.mat.tests/src/org/eclipse/mat/tests/cli/CliCommandExecutorTest.java b/plugins/org.eclipse.mat.tests/src/org/eclipse/mat/tests/cli/CliCommandExecutorTest.java index 4c5a237e..7a0b9e85 100644 --- a/plugins/org.eclipse.mat.tests/src/org/eclipse/mat/tests/cli/CliCommandExecutorTest.java +++ b/plugins/org.eclipse.mat.tests/src/org/eclipse/mat/tests/cli/CliCommandExecutorTest.java @@ -539,6 +539,38 @@ public void executesDescribeQueryCommandInJsonMode() throws Exception assertTrue(json.contains("\"query\":{\"identifier\":\"hash_entries\"")); //$NON-NLS-1$ } + @Test + public void executesCompletionCommandInTextModeForBash() throws Exception + { + String script = execute(new String[] { "completion", "bash" }); //$NON-NLS-1$ //$NON-NLS-2$ + + assertTrue(script.contains("# bash completion for mat-cli")); //$NON-NLS-1$ + assertTrue(script.contains("complete -F _mat_cli_completion mat-cli")); //$NON-NLS-1$ + assertTrue(script.contains("|completion)")); //$NON-NLS-1$ + assertTrue(script.contains("--format")); //$NON-NLS-1$ + } + + @Test + public void executesCompletionCommandInTextModeForZsh() throws Exception + { + String script = execute(new String[] { "completion", "zsh" }); //$NON-NLS-1$ //$NON-NLS-2$ + + assertTrue(script.contains("#compdef mat-cli")); //$NON-NLS-1$ + assertTrue(script.contains("compdef _mat-cli mat-cli")); //$NON-NLS-1$ + assertTrue(script.contains("|completion)")); //$NON-NLS-1$ + assertTrue(script.contains("--query-file")); //$NON-NLS-1$ + } + + @Test + public void executesCompletionCommandInJsonMode() throws Exception + { + String json = executeJson(new String[] { "completion", "bash", "--format", "json" }); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$ + + assertTrue(json.contains("\"resultKind\":\"text\"")); //$NON-NLS-1$ + assertTrue(json.contains("\"content\":\"# bash completion for mat-cli")); //$NON-NLS-1$ + assertTrue(json.contains("complete -F _mat_cli_completion mat-cli")); //$NON-NLS-1$ + } + @Test public void executesTopConsumersHtmlThroughQueryCommandAsRawSectionJson() throws Exception { diff --git a/plugins/org.eclipse.mat.tests/src/org/eclipse/mat/tests/cli/CliTests.java b/plugins/org.eclipse.mat.tests/src/org/eclipse/mat/tests/cli/CliTests.java index d1c4a0c7..8d49606f 100644 --- a/plugins/org.eclipse.mat.tests/src/org/eclipse/mat/tests/cli/CliTests.java +++ b/plugins/org.eclipse.mat.tests/src/org/eclipse/mat/tests/cli/CliTests.java @@ -18,7 +18,7 @@ @RunWith(Suite.class) @SuiteClasses( { CliArgumentParserTest.class, ResultSerializerTest.class, CliCommandExecutorTest.class, - CliApplicationErrorContextTest.class }) + CompletionScriptGeneratorTest.class, CliApplicationErrorContextTest.class }) public class CliTests { public static junit.framework.Test suite() diff --git a/plugins/org.eclipse.mat.tests/src/org/eclipse/mat/tests/cli/CompletionScriptGeneratorTest.java b/plugins/org.eclipse.mat.tests/src/org/eclipse/mat/tests/cli/CompletionScriptGeneratorTest.java new file mode 100644 index 00000000..45b6288d --- /dev/null +++ b/plugins/org.eclipse.mat.tests/src/org/eclipse/mat/tests/cli/CompletionScriptGeneratorTest.java @@ -0,0 +1,97 @@ +/******************************************************************************* + * Copyright (c) 2026 Eclipse Memory Analyzer Project. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + *******************************************************************************/ +package org.eclipse.mat.tests.cli; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; + +import org.eclipse.mat.cli.internal.CompletionScriptGenerator; +import org.junit.Test; + +public class CompletionScriptGeneratorTest +{ + @Test + public void generatesBashCompletionWithStaticCommandAndValueMetadata() throws Exception + { + String script = new CompletionScriptGenerator().generate("bash"); //$NON-NLS-1$ + + assertTrue(script.contains("# bash completion for mat-cli")); //$NON-NLS-1$ + assertTrue(script.contains("--format")); //$NON-NLS-1$ + assertTrue(script.contains("enum:text json")); //$NON-NLS-1$ + assertTrue(script.contains("summary:0")); //$NON-NLS-1$ + assertTrue(script.contains("describe:0")); //$NON-NLS-1$ + assertTrue(script.contains("completion:0")); //$NON-NLS-1$ + assertTrue(script.contains("file")); //$NON-NLS-1$ + assertTrue(script.contains("--query-file")); //$NON-NLS-1$ + assertTrue(script.contains("--command-file")); //$NON-NLS-1$ + assertTrue(script.contains("free-text")); //$NON-NLS-1$ + } + + @Test + public void generatesZshCompletionWithStaticCommandAndValueMetadata() throws Exception + { + String script = new CompletionScriptGenerator().generate("zsh"); //$NON-NLS-1$ + + assertTrue(script.contains("#compdef mat-cli")); //$NON-NLS-1$ + assertTrue(script.contains("compdef _mat-cli mat-cli")); //$NON-NLS-1$ + assertTrue(script.contains("--format")); //$NON-NLS-1$ + assertTrue(script.contains("enum:text json")); //$NON-NLS-1$ + assertTrue(script.contains("completion:0")); //$NON-NLS-1$ + assertTrue(script.contains("_files")); //$NON-NLS-1$ + } + + @Test + public void generatedScriptsMatchShippedRootfiles() throws Exception + { + CompletionScriptGenerator generator = new CompletionScriptGenerator(); + Path repositoryRoot = locateRepositoryRoot(); + + assertEquals(generator.generate("bash"), //$NON-NLS-1$ + Files.readString(repositoryRoot.resolve( + "features/org.eclipse.mat.cli.feature/rootfiles/completion/bash/mat-cli"), //$NON-NLS-1$ + StandardCharsets.UTF_8)); + assertEquals(generator.generate("zsh"), //$NON-NLS-1$ + Files.readString(repositoryRoot.resolve( + "features/org.eclipse.mat.cli.feature/rootfiles/completion/zsh/_mat-cli"), //$NON-NLS-1$ + StandardCharsets.UTF_8)); + } + + @Test + public void featureBuildPropertiesIncludeCompletionRootfiles() throws Exception + { + Path repositoryRoot = locateRepositoryRoot(); + String buildProperties = Files.readString( + repositoryRoot.resolve("features/org.eclipse.mat.cli.feature/build.properties"), //$NON-NLS-1$ + StandardCharsets.UTF_8); + + assertTrue(buildProperties.contains("file:rootfiles/completion/bash/mat-cli")); //$NON-NLS-1$ + assertTrue(buildProperties.contains("file:rootfiles/completion/zsh/_mat-cli")); //$NON-NLS-1$ + } + + private Path locateRepositoryRoot() + { + Path current = Path.of(System.getProperty("user.dir")).toAbsolutePath(); //$NON-NLS-1$ + while (current != null) + { + if (Files.isDirectory(current.resolve("features/org.eclipse.mat.cli.feature")) //$NON-NLS-1$ + && Files.isDirectory(current.resolve("plugins/org.eclipse.mat.cli"))) //$NON-NLS-1$ + return current; + current = current.getParent(); + } + + fail("Unable to locate repository root from " + System.getProperty("user.dir")); //$NON-NLS-1$ //$NON-NLS-2$ + return null; + } +} From 298521f54a542e23f918eda7f2ae72e35dc27468 Mon Sep 17 00:00:00 2001 From: Kai Wang Date: Mon, 16 Mar 2026 16:25:33 +0800 Subject: [PATCH 2/3] Implement mat-cli shell completion --- org.eclipse.mat.product/pom.xml | 7 ++++++ .../cli/CompletionScriptGeneratorTest.java | 23 +++++++++++-------- 2 files changed, 20 insertions(+), 10 deletions(-) diff --git a/org.eclipse.mat.product/pom.xml b/org.eclipse.mat.product/pom.xml index 81c05d99..cf5cd1d2 100644 --- a/org.eclipse.mat.product/pom.xml +++ b/org.eclipse.mat.product/pom.xml @@ -172,8 +172,15 @@ + + + + + diff --git a/plugins/org.eclipse.mat.tests/src/org/eclipse/mat/tests/cli/CompletionScriptGeneratorTest.java b/plugins/org.eclipse.mat.tests/src/org/eclipse/mat/tests/cli/CompletionScriptGeneratorTest.java index 45b6288d..bfa35b94 100644 --- a/plugins/org.eclipse.mat.tests/src/org/eclipse/mat/tests/cli/CompletionScriptGeneratorTest.java +++ b/plugins/org.eclipse.mat.tests/src/org/eclipse/mat/tests/cli/CompletionScriptGeneratorTest.java @@ -16,6 +16,7 @@ import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; +import java.nio.file.Paths; import org.eclipse.mat.cli.internal.CompletionScriptGenerator; import org.junit.Test; @@ -59,22 +60,19 @@ public void generatedScriptsMatchShippedRootfiles() throws Exception Path repositoryRoot = locateRepositoryRoot(); assertEquals(generator.generate("bash"), //$NON-NLS-1$ - Files.readString(repositoryRoot.resolve( - "features/org.eclipse.mat.cli.feature/rootfiles/completion/bash/mat-cli"), //$NON-NLS-1$ - StandardCharsets.UTF_8)); + readUtf8(repositoryRoot.resolve( + "features/org.eclipse.mat.cli.feature/rootfiles/completion/bash/mat-cli"))); //$NON-NLS-1$ assertEquals(generator.generate("zsh"), //$NON-NLS-1$ - Files.readString(repositoryRoot.resolve( - "features/org.eclipse.mat.cli.feature/rootfiles/completion/zsh/_mat-cli"), //$NON-NLS-1$ - StandardCharsets.UTF_8)); + readUtf8(repositoryRoot.resolve( + "features/org.eclipse.mat.cli.feature/rootfiles/completion/zsh/_mat-cli"))); //$NON-NLS-1$ } @Test public void featureBuildPropertiesIncludeCompletionRootfiles() throws Exception { Path repositoryRoot = locateRepositoryRoot(); - String buildProperties = Files.readString( - repositoryRoot.resolve("features/org.eclipse.mat.cli.feature/build.properties"), //$NON-NLS-1$ - StandardCharsets.UTF_8); + String buildProperties = readUtf8( + repositoryRoot.resolve("features/org.eclipse.mat.cli.feature/build.properties")); //$NON-NLS-1$ assertTrue(buildProperties.contains("file:rootfiles/completion/bash/mat-cli")); //$NON-NLS-1$ assertTrue(buildProperties.contains("file:rootfiles/completion/zsh/_mat-cli")); //$NON-NLS-1$ @@ -82,7 +80,7 @@ public void featureBuildPropertiesIncludeCompletionRootfiles() throws Exception private Path locateRepositoryRoot() { - Path current = Path.of(System.getProperty("user.dir")).toAbsolutePath(); //$NON-NLS-1$ + Path current = Paths.get(System.getProperty("user.dir")).toAbsolutePath(); //$NON-NLS-1$ while (current != null) { if (Files.isDirectory(current.resolve("features/org.eclipse.mat.cli.feature")) //$NON-NLS-1$ @@ -94,4 +92,9 @@ private Path locateRepositoryRoot() fail("Unable to locate repository root from " + System.getProperty("user.dir")); //$NON-NLS-1$ //$NON-NLS-2$ return null; } + + private String readUtf8(Path path) throws Exception + { + return new String(Files.readAllBytes(path), StandardCharsets.UTF_8); + } } From 1ed1bec9b3247ecaa82e2fd8ea2e9b5263542954 Mon Sep 17 00:00:00 2001 From: Kai Wang Date: Mon, 16 Mar 2026 16:49:50 +0800 Subject: [PATCH 3/3] Add mat-cli completion support --- .../internal/CompletionScriptGenerator.java | 686 +++++++++--------- 1 file changed, 351 insertions(+), 335 deletions(-) diff --git a/plugins/org.eclipse.mat.cli/src/org/eclipse/mat/cli/internal/CompletionScriptGenerator.java b/plugins/org.eclipse.mat.cli/src/org/eclipse/mat/cli/internal/CompletionScriptGenerator.java index ab100921..d8d98f10 100644 --- a/plugins/org.eclipse.mat.cli/src/org/eclipse/mat/cli/internal/CompletionScriptGenerator.java +++ b/plugins/org.eclipse.mat.cli/src/org/eclipse/mat/cli/internal/CompletionScriptGenerator.java @@ -14,6 +14,329 @@ public final class CompletionScriptGenerator { + private static final String BASH_TEMPLATE = """ +# bash completion for mat-cli +_mat_cli_is_command() { + case "$1" in +__COMMAND_CASE__ return 0 + ;; + esac + return 1 +} + +__KIND_HELPERS____OPTION_KIND_FUNCTIONS____OPTIONS_FOR_COMMAND_FUNCTION____POSITIONAL_KIND_FUNCTION___mat_cli_set_word_replies() { + local prefix="$1" + local current="$2" + local values="$3" + local i + COMPREPLY=( $(compgen -W "$values" -- "$current") ) + if [ -n "$prefix" ]; then + for ((i=0; i<${#COMPREPLY[@]}; i++)); do + COMPREPLY[i]="$prefix${COMPREPLY[i]}" + done + fi +} + +_mat_cli_set_file_replies() { + local prefix="$1" + local current="$2" + local line + COMPREPLY=() + while IFS= read -r line; do + if [ -n "$prefix" ]; then + COMPREPLY+=("$prefix$line") + else + COMPREPLY+=("$line") + fi + done < <(compgen -f -- "$current") + if type compopt >/dev/null 2>&1; then + compopt -o filenames 2>/dev/null + fi +} + +_mat_cli_complete_kind() { + local kind="$1" + local current="$2" + local prefix="$3" + case "$kind" in + enum:*) + _mat_cli_set_word_replies "$prefix" "$current" "${kind#enum:}" + ;; + file) + _mat_cli_set_file_replies "$prefix" "$current" + ;; + *) + COMPREPLY=() + ;; + esac +} + +_mat_cli_find_command_index() { + local i word pending_kind="" + for ((i=1; i/dev/null 2>&1 + _files -P "$prefix" + else + _files + fi + ;; + *) + return 1 + ;; + esac +} + +_mat_cli_find_command() { + local i word pending_kind="" + reply=() + for ((i=2; i/dev/null 2>&1 + _mat_cli_complete_kind "$kind" "$inline_prefix" + return 0 + fi + fi + kind="$(_mat_cli_option_kind_global "$prev")" + if _mat_cli_kind_expects_value "$kind"; then + _mat_cli_complete_kind "$kind" "" + return 0 + fi + _mat_cli_complete_words "" __TOP_LEVEL_WORDS__ + return 0 + fi + + cmd="$reply[1]" + cmd_index="$reply[2]" + if [[ "$cur" == --*=* ]]; then + inline_option="${cur%%=*}" + inline_prefix="$inline_option=" + kind="$(_mat_cli_option_kind "$cmd" "$inline_option")" + if [[ "$kind" == enum:* || "$kind" == file ]]; then + compset -P "$inline_prefix" >/dev/null 2>&1 + _mat_cli_complete_kind "$kind" "$inline_prefix" + return 0 + fi + fi + kind="$(_mat_cli_option_kind "$cmd" "$prev")" + if _mat_cli_kind_expects_value "$kind"; then + _mat_cli_complete_kind "$kind" "" + return 0 + fi + + position_index=0 + pending_kind="" + for ((i=cmd_index+1; i/dev/null +"""; //$NON-NLS-1$ + public String generate(String shell) throws CliException { if ("bash".equals(shell)) //$NON-NLS-1$ @@ -25,356 +348,44 @@ public String generate(String shell) throws CliException private String generateBash() { - StringBuilder builder = new StringBuilder(16384); - builder.append("# bash completion for mat-cli\n"); //$NON-NLS-1$ - builder.append("_mat_cli_is_command() {\n"); //$NON-NLS-1$ - builder.append(" case \"$1\" in\n"); //$NON-NLS-1$ - builder.append(" ").append(joinWithPipe(CliCommandCatalog.commandTokens())).append(")\n"); //$NON-NLS-1$ - builder.append(" return 0\n"); //$NON-NLS-1$ - builder.append(" ;;\n"); //$NON-NLS-1$ - builder.append(" esac\n"); //$NON-NLS-1$ - builder.append(" return 1\n"); //$NON-NLS-1$ - builder.append("}\n\n"); //$NON-NLS-1$ - - appendKindHelpers(builder, false); - appendOptionKindFunctions(builder, false); - appendOptionsForCommandFunction(builder, false); - appendPositionalKindFunction(builder, false); - - builder.append("_mat_cli_set_word_replies() {\n"); //$NON-NLS-1$ - builder.append(" local prefix=\"$1\"\n"); //$NON-NLS-1$ - builder.append(" local current=\"$2\"\n"); //$NON-NLS-1$ - builder.append(" local values=\"$3\"\n"); //$NON-NLS-1$ - builder.append(" local i\n"); //$NON-NLS-1$ - builder.append(" COMPREPLY=( $(compgen -W \"$values\" -- \"$current\") )\n"); //$NON-NLS-1$ - builder.append(" if [ -n \"$prefix\" ]; then\n"); //$NON-NLS-1$ - builder.append(" for ((i=0; i<${#COMPREPLY[@]}; i++)); do\n"); //$NON-NLS-1$ - builder.append(" COMPREPLY[i]=\"$prefix${COMPREPLY[i]}\"\n"); //$NON-NLS-1$ - builder.append(" done\n"); //$NON-NLS-1$ - builder.append(" fi\n"); //$NON-NLS-1$ - builder.append("}\n\n"); //$NON-NLS-1$ - - builder.append("_mat_cli_set_file_replies() {\n"); //$NON-NLS-1$ - builder.append(" local prefix=\"$1\"\n"); //$NON-NLS-1$ - builder.append(" local current=\"$2\"\n"); //$NON-NLS-1$ - builder.append(" local line\n"); //$NON-NLS-1$ - builder.append(" COMPREPLY=()\n"); //$NON-NLS-1$ - builder.append(" while IFS= read -r line; do\n"); //$NON-NLS-1$ - builder.append(" if [ -n \"$prefix\" ]; then\n"); //$NON-NLS-1$ - builder.append(" COMPREPLY+=(\"$prefix$line\")\n"); //$NON-NLS-1$ - builder.append(" else\n"); //$NON-NLS-1$ - builder.append(" COMPREPLY+=(\"$line\")\n"); //$NON-NLS-1$ - builder.append(" fi\n"); //$NON-NLS-1$ - builder.append(" done < <(compgen -f -- \"$current\")\n"); //$NON-NLS-1$ - builder.append(" if type compopt >/dev/null 2>&1; then\n"); //$NON-NLS-1$ - builder.append(" compopt -o filenames 2>/dev/null\n"); //$NON-NLS-1$ - builder.append(" fi\n"); //$NON-NLS-1$ - builder.append("}\n\n"); //$NON-NLS-1$ - - builder.append("_mat_cli_complete_kind() {\n"); //$NON-NLS-1$ - builder.append(" local kind=\"$1\"\n"); //$NON-NLS-1$ - builder.append(" local current=\"$2\"\n"); //$NON-NLS-1$ - builder.append(" local prefix=\"$3\"\n"); //$NON-NLS-1$ - builder.append(" case \"$kind\" in\n"); //$NON-NLS-1$ - builder.append(" enum:*)\n"); //$NON-NLS-1$ - builder.append(" _mat_cli_set_word_replies \"$prefix\" \"$current\" \"${kind#enum:}\"\n"); //$NON-NLS-1$ - builder.append(" ;;\n"); //$NON-NLS-1$ - builder.append(" file)\n"); //$NON-NLS-1$ - builder.append(" _mat_cli_set_file_replies \"$prefix\" \"$current\"\n"); //$NON-NLS-1$ - builder.append(" ;;\n"); //$NON-NLS-1$ - builder.append(" *)\n"); //$NON-NLS-1$ - builder.append(" COMPREPLY=()\n"); //$NON-NLS-1$ - builder.append(" ;;\n"); //$NON-NLS-1$ - builder.append(" esac\n"); //$NON-NLS-1$ - builder.append("}\n\n"); //$NON-NLS-1$ - - builder.append("_mat_cli_find_command_index() {\n"); //$NON-NLS-1$ - builder.append(" local i word pending_kind=\"\"\n"); //$NON-NLS-1$ - builder.append(" for ((i=1; i/dev/null 2>&1\n"); //$NON-NLS-1$ - builder.append(" _files -P \"$prefix\"\n"); //$NON-NLS-1$ - builder.append(" else\n"); //$NON-NLS-1$ - builder.append(" _files\n"); //$NON-NLS-1$ - builder.append(" fi\n"); //$NON-NLS-1$ - builder.append(" ;;\n"); //$NON-NLS-1$ - builder.append(" *)\n"); //$NON-NLS-1$ - builder.append(" return 1\n"); //$NON-NLS-1$ - builder.append(" ;;\n"); //$NON-NLS-1$ - builder.append(" esac\n"); //$NON-NLS-1$ - builder.append("}\n\n"); //$NON-NLS-1$ - - builder.append("_mat_cli_find_command() {\n"); //$NON-NLS-1$ - builder.append(" local i word pending_kind=\"\"\n"); //$NON-NLS-1$ - builder.append(" reply=()\n"); //$NON-NLS-1$ - builder.append(" for ((i=2; i/dev/null 2>&1\n"); //$NON-NLS-1$ - builder.append(" _mat_cli_complete_kind \"$kind\" \"$inline_prefix\"\n"); //$NON-NLS-1$ - builder.append(" return 0\n"); //$NON-NLS-1$ - builder.append(" fi\n"); //$NON-NLS-1$ - builder.append(" fi\n"); //$NON-NLS-1$ - builder.append(" kind=\"$(_mat_cli_option_kind_global \"$prev\")\"\n"); //$NON-NLS-1$ - builder.append(" if _mat_cli_kind_expects_value \"$kind\"; then\n"); //$NON-NLS-1$ - builder.append(" _mat_cli_complete_kind \"$kind\" \"\"\n"); //$NON-NLS-1$ - builder.append(" return 0\n"); //$NON-NLS-1$ - builder.append(" fi\n"); //$NON-NLS-1$ - builder.append(" _mat_cli_complete_words \"\" ").append(quotedWords(topLevelWords())).append('\n'); - builder.append(" return 0\n"); //$NON-NLS-1$ - builder.append(" fi\n\n"); //$NON-NLS-1$ - - builder.append(" cmd=\"$reply[1]\"\n"); //$NON-NLS-1$ - builder.append(" cmd_index=\"$reply[2]\"\n"); //$NON-NLS-1$ - builder.append(" if [[ \"$cur\" == --*=* ]]; then\n"); //$NON-NLS-1$ - builder.append(" inline_option=\"${cur%%=*}\"\n"); //$NON-NLS-1$ - builder.append(" inline_prefix=\"$inline_option=\"\n"); //$NON-NLS-1$ - builder.append(" kind=\"$(_mat_cli_option_kind \"$cmd\" \"$inline_option\")\"\n"); //$NON-NLS-1$ - builder.append(" if [[ \"$kind\" == enum:* || \"$kind\" == file ]]; then\n"); //$NON-NLS-1$ - builder.append(" compset -P \"$inline_prefix\" >/dev/null 2>&1\n"); //$NON-NLS-1$ - builder.append(" _mat_cli_complete_kind \"$kind\" \"$inline_prefix\"\n"); //$NON-NLS-1$ - builder.append(" return 0\n"); //$NON-NLS-1$ - builder.append(" fi\n"); //$NON-NLS-1$ - builder.append(" fi\n"); //$NON-NLS-1$ - builder.append(" kind=\"$(_mat_cli_option_kind \"$cmd\" \"$prev\")\"\n"); //$NON-NLS-1$ - builder.append(" if _mat_cli_kind_expects_value \"$kind\"; then\n"); //$NON-NLS-1$ - builder.append(" _mat_cli_complete_kind \"$kind\" \"\"\n"); //$NON-NLS-1$ - builder.append(" return 0\n"); //$NON-NLS-1$ - builder.append(" fi\n\n"); //$NON-NLS-1$ - - builder.append(" position_index=0\n"); //$NON-NLS-1$ - builder.append(" pending_kind=\"\"\n"); //$NON-NLS-1$ - builder.append(" for ((i=cmd_index+1; i/dev/null\n"); //$NON-NLS-1$ - return builder.toString(); + private String renderCommandCase() + { + return " " + joinWithPipe(CliCommandCatalog.commandTokens()) + ")\n"; //$NON-NLS-1$ //$NON-NLS-2$ } - private void appendKindHelpers(StringBuilder builder, boolean zsh) + private String renderKindHelpers(boolean zsh) { + StringBuilder builder = new StringBuilder(64); builder.append("_mat_cli_kind_expects_value() {\n"); //$NON-NLS-1$ if (zsh) builder.append(" [[ -n \"$1\" && \"$1\" != none ]]\n"); //$NON-NLS-1$ else builder.append(" [ -n \"$1\" ] && [ \"$1\" != \"none\" ]\n"); //$NON-NLS-1$ builder.append("}\n\n"); //$NON-NLS-1$ + return builder.toString(); } - private void appendOptionKindFunctions(StringBuilder builder, boolean zsh) + private String renderOptionKindFunctions(boolean zsh) { + StringBuilder builder = new StringBuilder(4096); appendOptionKindFunction(builder, "_mat_cli_option_kind_global", CliCommandCatalog.globalOptions()); //$NON-NLS-1$ builder.append("_mat_cli_option_kind_for_command() {\n"); //$NON-NLS-1$ @@ -411,6 +422,7 @@ private void appendOptionKindFunctions(StringBuilder builder, boolean zsh) builder.append(" fi\n"); //$NON-NLS-1$ builder.append(" _mat_cli_option_kind_global \"$2\"\n"); //$NON-NLS-1$ builder.append("}\n\n"); //$NON-NLS-1$ + return builder.toString(); } private void appendOptionKindFunction(StringBuilder builder, String functionName, @@ -428,8 +440,9 @@ private void appendOptionKindFunction(StringBuilder builder, String functionName builder.append("}\n\n"); //$NON-NLS-1$ } - private void appendOptionsForCommandFunction(StringBuilder builder, boolean zsh) + private String renderOptionsForCommandFunction(boolean zsh) { + StringBuilder builder = new StringBuilder(2048); builder.append("_mat_cli_options_for_command() {\n"); //$NON-NLS-1$ builder.append(" case \"$1\" in\n"); //$NON-NLS-1$ for (CliCommand command : CliCommand.values()) @@ -448,10 +461,12 @@ private void appendOptionsForCommandFunction(StringBuilder builder, boolean zsh) } builder.append(" esac\n"); //$NON-NLS-1$ builder.append("}\n\n"); //$NON-NLS-1$ + return builder.toString(); } - private void appendPositionalKindFunction(StringBuilder builder, boolean zsh) + private String renderPositionalKindFunction(boolean zsh) { + StringBuilder builder = new StringBuilder(2048); builder.append("_mat_cli_positional_kind() {\n"); //$NON-NLS-1$ builder.append(" case \"$1:$2\" in\n"); //$NON-NLS-1$ for (CliCommand command : CliCommand.values()) @@ -472,6 +487,7 @@ private void appendPositionalKindFunction(StringBuilder builder, boolean zsh) } builder.append(" esac\n"); //$NON-NLS-1$ builder.append("}\n\n"); //$NON-NLS-1$ + return builder.toString(); } private List topLevelWords()