Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions .github/workflows/dejagnu.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
name: DejaGnu Tests

on: [ push, pull_request ]

jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Install Fish shell and DejaGnu
run: |
sudo apt-add-repository -y ppa:fish-shell/release-3
sudo apt-get update
sudo apt-get install -y fish dejagnu tcllib
- name: Check versions
run: |
fish --version
runtest --version
- name: Checkout
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v2
- name: Run DejaGnu fish tests
continue-on-error: true
run: |
cd src/test/dejagnu.fishtests
./runCompletion
- name: Run DejaGnu bash tests
continue-on-error: true
run: |
cd src/test/dejagnu.tests
./runCompletion
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,7 @@ picocli.iml
/src/test/dejagnu.tests/testrun.log
/src/test/dejagnu.tests/testrun.sum
/src/test/dejagnu.tests/tmp/
/src/test/dejagnu.fishtests/log/completion.sum
/src/test/dejagnu.fishtests/log/completion.log
/picocli-tests-java567/gradle/wrapper/dists/**/*.lck
/picocli-tests-java567/gradle/wrapper/dists/**/*.ok
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,13 @@
{ "name" : "spec" }
]
},
{
"name" : "picocli.AutoComplete$GenerateCompletion$Shell",
"allDeclaredConstructors" : true,
"allPublicConstructors" : true,
"allDeclaredMethods" : true,
"allPublicMethods" : true
},
{
"name" : "picocli.CommandLine$AutoHelpMixin",
"allDeclaredConstructors" : true,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,17 @@
"allDeclaredMethods" : true,
"allPublicMethods" : true,
"fields" : [
{ "name" : "shell" },
{ "name" : "spec" }
]
},
{
"name" : "picocli.AutoComplete$GenerateCompletion$Shell",
"allDeclaredConstructors" : true,
"allPublicConstructors" : true,
"allDeclaredMethods" : true,
"allPublicMethods" : true
},
{
"name" : "picocli.CommandLine$AutoHelpMixin",
"allDeclaredConstructors" : true,
Expand Down
160 changes: 155 additions & 5 deletions src/main/java/picocli/AutoComplete.java
Original file line number Diff line number Diff line change
Expand Up @@ -219,22 +219,49 @@ private boolean checkExists(final File file) {
@Command(name = "generate-completion", version = "generate-completion " + CommandLine.VERSION,
mixinStandardHelpOptions = true,
description = {
"Generate bash/zsh completion script for ${ROOT-COMMAND-NAME:-the root command of this command}.",
"Generate bash/zsh or fish completion script for ${ROOT-COMMAND-NAME:-the root command of this command}.",
"Run the following command to give `${ROOT-COMMAND-NAME:-$PARENTCOMMAND}` TAB completion in the current shell:",
"",
" source <(${PARENT-COMMAND-FULL-NAME:-$PARENTCOMMAND} ${COMMAND-NAME})",
" bash/zsh: source <(${PARENT-COMMAND-FULL-NAME:-$PARENTCOMMAND} ${COMMAND-NAME})",
"",
" fish: eval (${PARENT-COMMAND-FULL-NAME:-$PARENTCOMMAND} ${COMMAND-NAME} --shell fish)",
""},
optionListHeading = "Options:%n",
helpCommand = true
)
public static class GenerateCompletion implements Runnable {

enum Shell {
bash {
@Override
String generate(CommandSpec spec) {
return AutoComplete.bash(
spec.root().name(),
spec.root().commandLine());
}
},
fish {
@Override
String generate(CommandSpec spec) {
return AutoComplete.fish(
spec.root().name(),
spec.root().commandLine());
}
};

abstract String generate(CommandSpec spec);
}

@Spec CommandLine.Model.CommandSpec spec;

@Option(names = {"-s", "--shell"}, description = "Specify the shell to generate the completion script for. " +
"When omitted, the default shell is bash. " +
"Valid values are: bash, fish", defaultValue = "bash")
Shell shell;

public void run() {
String script = AutoComplete.bash(
spec.root().name(),
spec.root().commandLine());
String script = shell.generate(spec);

// not PrintWriter.println: scripts with Windows line separators fail in strange ways!
spec.commandLine().getOut().print(script);
spec.commandLine().getOut().print('\n');
Expand Down Expand Up @@ -540,6 +567,129 @@ public static String bash(String scriptName, CommandLine commandLine) {
return result.toString();
}


public static String fish(String scriptName, CommandLine commandLine) {
if (scriptName == null) { throw new NullPointerException("scriptName"); }
if (commandLine == null) { throw new NullPointerException("commandLine"); }
List<CommandDescriptor> hierarchy = createHierarchy(scriptName, commandLine);
StringBuilder result = new StringBuilder();

String parentFunction = "";
List<CommandDescriptor> currentLevel = new ArrayList<CommandDescriptor>();
List<String> currentLevelCommands = new ArrayList<String>();

CommandDescriptor rootDescriptor = null;
for (CommandDescriptor descriptor : hierarchy) {
if (descriptor.parentFunctionName.equals("")) {
rootDescriptor = descriptor;
parentFunction = descriptor.functionName;
continue;
}
if (!descriptor.parentFunctionName.equals(parentFunction)) {
processLevel(scriptName, result, currentLevel, currentLevelCommands, parentFunction, rootDescriptor);
rootDescriptor = null;

currentLevel.clear();
currentLevelCommands.clear();
parentFunction = descriptor.parentFunctionName;
}

currentLevel.add(descriptor);
currentLevelCommands.add(descriptor.commandName);
}
processLevel(scriptName, result, currentLevel, currentLevelCommands, parentFunction, rootDescriptor);


return result.toString();
}

private static void processLevel(String scriptName, StringBuilder result, List<CommandDescriptor> currentLevel,
List<String> currentLevelCommands, String levelName,
CommandDescriptor rootDescriptor) {
if (levelName.equals("")) {
levelName = "root";
}

// fish doesn't like dashes in variable names
levelName = levelName.replaceAll("-", "_");

result.append("\n# ").append(levelName).append(" completion\n");
result.append("set -l ").append(levelName);
if (!currentLevelCommands.isEmpty()) {
result.append(" ").append(join(" ", currentLevelCommands));
}
result.append("\n");
if (rootDescriptor != null) {
String condition = " --condition \"not __fish_seen_subcommand_from $" + levelName + "\"";
for (OptionSpec optionSpec : rootDescriptor.commandLine.getCommandSpec().options()) {
completeFishOption(scriptName, optionSpec, condition, result);
}
}
for (CommandDescriptor commandDescriptor : currentLevel) {

result.append("complete -c ").append(scriptName);
result.append(" --no-files"); // do not show files
result.append(" --condition \"not __fish_seen_subcommand_from $").append(levelName).append("\"");
if (!commandDescriptor.parentWithoutTopLevelCommand.equals("")) {
result.append(" --condition '__fish_seen_subcommand_from ").append(
commandDescriptor.parentWithoutTopLevelCommand).append("'");
}

result.append(" --arguments ").append(commandDescriptor.commandName);

String[] descriptions = commandDescriptor.commandLine.getCommandSpec().usageMessage().description();
String description = descriptions.length > 0 ? descriptions[0] : "";
result.append(" -d '").append(sanitizeFishDescription(description)).append("'\n");

String condition = getFishCondition(commandDescriptor);
for (OptionSpec optionSpec : commandDescriptor.commandLine.getCommandSpec().options()) {
completeFishOption(scriptName, optionSpec, condition, result);
}
}
}

private static String getFishCondition(CommandDescriptor commandDescriptor) {
StringBuilder condition = new StringBuilder();
condition.append(" --condition \"__fish_seen_subcommand_from ").append(commandDescriptor.commandName).append("\"");
if (!commandDescriptor.parentWithoutTopLevelCommand.equals("")) {
condition.append(" --condition '__fish_seen_subcommand_from ").append(
commandDescriptor.parentWithoutTopLevelCommand).append("'");
}
return condition.toString();
}

private static void completeFishOption(String scriptName, OptionSpec optionSpec, String conditions, StringBuilder result) {
result.append("complete -c ").append(scriptName);
result.append(conditions);
result.append(" --long-option ").append(optionSpec.longestName().replace("--", ""));

if (!optionSpec.shortestName().equals(optionSpec.longestName())) {
result.append(" --short-option ").append(optionSpec.shortestName().replace("-", ""));
}

if (optionSpec.completionCandidates() != null) {
result.append(" --no-files --arguments '").append(join(" ", extract(optionSpec.completionCandidates()))).append("' ");
}

String optionDescription = sanitizeFishDescription(optionSpec.description().length > 0 ? optionSpec.description()[0] : "");
result.append(" -d '").append(optionDescription).append("'\n");
}

private static String sanitizeFishDescription(String description) {
return description.replace("'", "\\'");
}

private static String join(String delimeter, List<String> list) {
StringBuilder result = new StringBuilder();
for (int i = 0; i < list.size(); i++) {
if (i > 0) {
result.append(delimeter);
}
result.append(list.get(i));
}
return result.toString();
}

private static List<CommandDescriptor> createHierarchy(String scriptName, CommandLine commandLine) {
List<CommandDescriptor> result = new ArrayList<CommandDescriptor>();
result.add(new CommandDescriptor("_picocli_" + scriptName, "", "", scriptName, commandLine));
Expand Down
4 changes: 4 additions & 0 deletions src/test/dejagnu.fishtests/README.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Run
```
runtest --outdir log --tool completion
```
23 changes: 23 additions & 0 deletions src/test/dejagnu.fishtests/completion/basicExample.exp
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
set timeout 1

# Setup completion and fake command
send "source ../resources/basic.fish\r"
expect -re "(.+>)"

send "function basicExample; echo 'do'; end\r"
expect -re "(.+>)"

set cmd "basicExample -"
set test "Tab should show options for '$cmd'"
set candidates "-t -u --timeout --timeUnit --timeUnit="
run_completion_test $cmd $test $candidates

set cmd "basicExample --"
set test "Tab should show options for '$cmd'"
set candidates "--timeout --timeUnit --timeUnit="
run_completion_test $cmd $test $candidates

set cmd "basicExample --timeUnit="
set test "Tab should show time unit enum values for '$cmd'"
set candidates ".*timeUnit=DAYS.*timeUnit=MICROSECONDS.*timeUnit=MINUTES.*timeUnit=SECONDS.*timeUnit=HOURS.*timeUnit=MILLISECONDS.*timeUnit=NANOSECONDS.*"
run_completion_test $cmd $test $candidates
24 changes: 24 additions & 0 deletions src/test/dejagnu.fishtests/completion/picocompletion-demo.exp
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
exp_internal 1
set timeout 1

# Setup completion and fake command
send "source ../resources/picocompletion-demo_completion.fish\r"
expect -re "(.+>)"

send "function picocompletion-demo; echo 'do'; end\r"
expect -re "(.+>)"

set cmd "picocompletion-demo "
set test "Tab should show sub1 and sub2 for '${cmd}'"
# for some reason, bash completion doesn't show sub1-alias and sub2-alias
set candidates "sub1.*sub1-alias.*sub2.*sub2-alias.*"
run_completion_test $cmd $test $candidates

# now we show files in this test
# set cmd "picocompletion-demo sub1 "
# set test "Tab should not show completions for '${cmd}'"

set cmd "picocompletion-demo sub1 -"
set test "Tab should show sub1 options for '${cmd}'"
set candidates "--candidates.*--candidates=.*--num.*--str.*"
run_completion_test $cmd $test $candidates
8 changes: 8 additions & 0 deletions src/test/dejagnu.fishtests/lib/completion.exp
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
exp_spawn fish --no-config
expect -re "(.+>)"

# Set terminal size to my notebook's resolution
send "stty rows 53 cols 190\r"
expect -re "(.+>)"

source $::srcdir/lib/library.exp
11 changes: 11 additions & 0 deletions src/test/dejagnu.fishtests/lib/library.exp
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
proc run_completion_test {cmd test candidates} {
exp_internal 1
send "${cmd}\t"
expect {
-re "(\n${candidates}\u001b)" { pass $test }
timeout { fail $test }
}
exp_internal 0
send "\x03"
expect ">"
}
3 changes: 3 additions & 0 deletions src/test/dejagnu.fishtests/runCompletion
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
#!/bin/bash
mkdir -p log
runtest --outdir log --tool completion
Empty file modified src/test/dejagnu.tests/runCompletion
100644 → 100755
Empty file.
14 changes: 11 additions & 3 deletions src/test/java/picocli/AutoCompleteDejaGnuTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -67,11 +67,19 @@ public void tryRunDejaGnuCompletionTests() throws Exception {

// ignores test if dejagnu not installed
org.junit.Assume.assumeTrue("dejagnu must be installed to run this test", isDejaGnuInstalled());
runDejaGnuCompletionTests();
runDejaGnuCompletionTests("src/test/dejagnu.tests");
}

private void runDejaGnuCompletionTests() throws Exception {
final File testDir = new File("src/test/dejagnu.tests");
@Test
public void tryRunFishDejaGnuCompletionTests() throws Exception {
// ignores test if dejagnu not installed
org.junit.Assume.assumeTrue("dejagnu must be installed to run this test", isDejaGnuInstalled());
runDejaGnuCompletionTests("src/test/dejagnu.fishtests");
}


private void runDejaGnuCompletionTests(String pathname) throws Exception {
final File testDir = new File(pathname);
assertTrue(testDir.getAbsolutePath() + " should exist", testDir.exists());
File runCompletionScript = new File(testDir, "runCompletion");
assertTrue(runCompletionScript.getAbsolutePath() + " should exist", runCompletionScript.exists());
Expand Down
Loading
Loading