org.tomitribe.crest.BashCompletion Maven / Gradle / Ivy
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.tomitribe.crest;
import org.tomitribe.crest.api.Command;
import org.tomitribe.crest.api.Option;
import org.tomitribe.crest.cmds.Cmd;
import org.tomitribe.crest.cmds.CmdGroup;
import org.tomitribe.crest.cmds.CmdMethod;
import org.tomitribe.crest.cmds.OverloadedCmdMethod;
import org.tomitribe.crest.cmds.processors.Commands;
import org.tomitribe.crest.cmds.processors.OptionParam;
import org.tomitribe.crest.environments.Environment;
import org.tomitribe.util.IO;
import org.tomitribe.util.Join;
import org.tomitribe.util.PrintString;
import java.io.File;
import java.io.IOException;
import java.io.PrintStream;
import java.net.URI;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
public class BashCompletion {
private static final String COMPLETER = "_completer";
final PrintString out = new PrintString();
private final Main main;
private String mainCommand;
public BashCompletion(final Main main) {
this.main = main;
}
@Command(COMPLETER)
public String _completer(@Option("f") boolean toFile) {
return _completer(toFile, guessName());
}
@Command(COMPLETER)
public String _completer(@Option("f") boolean toFile, final String name) {
this.mainCommand = name;
out.println("#!/bin/bash\n");
utilities();
group(2, "_" + mainCommand, this.main.commands.values());
out.println("\ncomplete -F _" + mainCommand + " " + mainCommand);
if (toFile) {
return asFile();
} else {
return out.toString();
}
}
private String asFile() {
try {
final File file = File.createTempFile(String.format(".%s-completion-", mainCommand), ".sh");
IO.copy(out.toByteArray(), file);
return file.getAbsolutePath();
} catch (IOException e) {
throw new RuntimeException("Failed to generate completion script for " + mainCommand, e);
}
}
private String getMainCommandName(String... args) {
// Specifying it explicitly wins
if (args.length == 1) return asFilename(args[0]);
return guessName();
}
private String guessName() {
{ // Next look for a system property 'cmd'
final String name = System.getProperty("cmd");
if (name != null) return asFilename(name);
}
{ // Next look for a system property 'cmd'
final String name = System.getenv("CMD");
if (name != null) return asFilename(name);
}
final Environment env = Environment.ENVIRONMENT_THREAD_LOCAL.get();
final PrintStream error = env.getError();
error.println("Specify the bash executable name. Acceptable methods in order of priority:\n" +
" 1. passed as an argument to this command (e.g. _completion wombat)\n" +
" 2. set via a -Dcmd system property (e.g. java -Dcmd=wombat, e.g. java -Dcmd=/some/path/wombat)\n" +
" 3. set as a $CMD environment variable (e.g. export CMD=$0, e.g. export CMD=wombat, e.g. export CMD=/some/path/wombat)");
throw new IllegalStateException("The bash executable name was not found");
}
private String asFilename(String name) {
final File file = new File(name);
return file.getName();
}
public static String generate(final Main main, final String... args) {
final BashCompletion bashCompletion = new BashCompletion(main);
final Map cmds = Commands.get(bashCompletion);
return (String) cmds.get(COMPLETER).exec(null, args);
}
private void cmd(final int depth, final String group, Cmd cmd) {
if (cmd instanceof CmdGroup) {
cmdGroup((CmdGroup) cmd, group, depth);
} else if (cmd instanceof OverloadedCmdMethod) {
overloadedCmdMethod((OverloadedCmdMethod) cmd, group);
} else if (cmd instanceof CmdMethod) {
cmdMethod((CmdMethod) cmd, group);
} else {
throw new IllegalStateException("Unknown cmd type: " + cmd.getClass().getName());
}
}
private void cmdGroup(final CmdGroup cmdGroup, final String group, final int depth) {
final String functionName = getFunctionName(group, cmdGroup);
group(depth, functionName, cmdGroup.getCommands());
}
private void overloadedCmdMethod(final OverloadedCmdMethod overloadedCmdMethod, final String group) {
final CmdMethod cmdMethod = overloadedCmdMethod.getMethods().iterator().next();
cmdMethod(cmdMethod, group);
}
private void cmdMethod(final CmdMethod cmdMethod, final String group) {
final String functionName = getFunctionName(group, cmdMethod);
out.println("\nfunction " + functionName + "() {");
final CmdMethod.Spec spec = cmdMethod.getSpec();
if (hasFlags(spec)) {
proposeFlags(spec);
} else {
out.println(" _propose_files");
}
out.println("}");
}
private void proposeFlags(final CmdMethod.Spec spec) {
out.println("" +
" local cur=${COMP_WORDS[COMP_CWORD]}\n" +
"\n" +
" case \"$cur\" in");
final Collection options = spec.getOptions().values();
for (final OptionParam param : options) {
final List values = guessValues(param);
if (values.size() > 0) {
final List strings = values.stream()
.map(this::quote)
.collect(Collectors.toList());
out.printf(" %s*) _propose_flag_values %s ;;\n", flag(param.getName()), Join.join(" ", strings));
} else {
out.printf(" %s*) _propose_flag_file_values ;;\n", flag(param.getName()));
}
}
{
final List flags = options.stream()
.map(OptionParam::getName)
.map(this::flag)
.map(this::quote)
.collect(Collectors.toList());
out.printf(" -*) _propose_flags %s;;\n", Join.join(" ", flags));
}
out.println("" +
" *) _propose_files ;;\n" +
" esac\n");
}
private static boolean hasFlags(final CmdMethod.Spec spec) {
return spec != null && spec.getOptions() != null && spec.getOptions().size() != 0;
}
private List guessValues(final OptionParam param) {
final List values = getDefaults(param);
final Class> type = param.getType();
if (type.isPrimitive()) {
values.remove(0);
}
if (Boolean.class.isAssignableFrom(type) || Boolean.TYPE.isAssignableFrom(type)) {
values.add("true");
values.add("false");
} else if (Enum.class.isAssignableFrom(type)) {
final Class> enumClass = (Class>) type;
final Enum>[] constants = enumClass.getEnumConstants();
for (final Enum> constant : constants) {
values.add(constant.name());
}
} else if (Pattern.class.isAssignableFrom(type) && values.size() == 0) {
values.add("");
} else if (isNonFile(type) && values.size() == 0) {
values.add(String.format("<%s>", type.getSimpleName()));
}
return values.stream()
.filter(Objects::nonNull)
.distinct()
.collect(Collectors.toList());
}
private static List getDefaults(final OptionParam param) {
final ArrayList values = new ArrayList<>();
values.add(param.getDefaultValue());
values.addAll(param.getDefaultValues());
return values.stream()
.filter(Objects::nonNull)
.distinct()
.collect(Collectors.toList());
}
/**
* By default we will list files as the suggestion for parameter or option
*
* There a few scenarios this is pointless. In these situations we will
* simply suggest the type itself so the user has some indication of a good value.
*/
private boolean isNonFile(final Class> type) {
final Class>[] types = {
URI.class,
URL.class,
Byte.class,
Byte.TYPE,
Character.class,
Character.TYPE,
Short.class,
Short.TYPE,
Integer.class,
Integer.TYPE,
Long.class,
Long.TYPE,
Float.class,
Float.TYPE,
Double.class,
Double.TYPE
};
for (final Class> aClass : types) {
if (aClass.isAssignableFrom(type)) {
return true;
}
}
return false;
}
private String flag(String s) {
if (s.length() == 1) {
return String.format("-%s=", s);
} else {
return String.format("--%s=", s);
}
}
private String quote(T t) {
return "\"" + t + "\"";
}
/**
* This should really be recursive as we have command groups
*/
private String getFunctionName(final String group, Cmd cmd) {
final String safeCharacters = cmd.getName().replaceAll("[^a-zA-Z0-9]+", "");
return group + "_" + safeCharacters;
}
private void proposeFiles() {
out.println("\n" +
"function _propose_files() {\n" +
" local cur=${COMP_WORDS[COMP_CWORD]}\n" +
" COMPREPLY=($(compgen -f \"$cur\"))\n" +
"}\n"
);
}
private void proposeFlags() {
out.println("\n" +
"function _propose_flags() {\n" +
" local FLAGS=\"$@\"\n" +
" local cur=${COMP_WORDS[COMP_CWORD]}\n" +
"\n" +
" # minus flags we've used\n" +
" for ((i = 0; i < ${#COMP_WORDS[*]} - 1; i++)); do\n" +
" n=\"${COMP_WORDS[$i]}\"\n" +
" [[ \"$n\" == -* ]] && {\n" +
" n=\"${n/=*/=}\"\n" +
" FLAGS=(\"${FLAGS[@]/$n/}\")\n" +
" }\n" +
" done\n" +
"\n" +
" COMPREPLY=($(compgen -W \"${FLAGS[*]}\" -- \"$cur\"))\n" +
"}\n"
);
}
private void proposeFlagValues() {
out.println("\n" +
"function _propose_flag_values() {\n" +
" local VALUES=\"$@\"\n" +
" local cur=${COMP_WORDS[COMP_CWORD]}\n" +
"\n" +
" cur=\"$(echo \"$cur\" | perl -pe 's/[^=]+=//')\"\n" +
" COMPREPLY=($(compgen -W \"${VALUES[*]}\" \"$cur\"))\n" +
"}\n"
);
}
private void proposeFlagValuesFiles() {
out.println("\n" +
"function _propose_flag_file_values() {\n" +
" local cur=${COMP_WORDS[COMP_CWORD]}\n" +
"\n" +
" cur=\"$(echo \"$cur\" | perl -pe 's/[^=]+=//')\"\n" +
" COMPREPLY=($(compgen -f \"$cur\"))\n" +
"}\n"
);
}
private void utilities() {
proposeFiles();
proposeFlags();
proposeFlagValues();
proposeFlagValuesFiles();
}
private void group(final int depth, final String functionName, final Collection commands) {
final int depthPlusOne = depth + 1;
final int depthMinusOne = depth - 1;
final List names = commands.stream()
.map(Cmd::getName)
.map(s -> " " + s)
.collect(Collectors.toList());
out.println("function " + functionName + "() {\n" +
" local cur=${COMP_WORDS[COMP_CWORD]}\n" +
" local args_length=${#COMP_WORDS[@]}\n" +
"\n" +
" local COMMANDS=(\n" +
Join.join("\n", names) +
"\n )\n" +
"\n" +
" # List the commands\n" +
" [ $args_length -lt " + depthPlusOne + " ] && {\n" +
" COMPREPLY=($(compgen -W \"${COMMANDS[*]}\" \"$cur\"))\n" +
" return\n" +
" }\n" +
"\n" +
" # Command chosen. Delegate to its completion function\n" +
"\n" +
" # Verify the command is one we know and execute the\n" +
" # function that performs its completion\n" +
" local CMD=${COMP_WORDS[" + depthMinusOne + "]}\n" +
" for n in \"${COMMANDS[@]}\"; do\n" +
" [ \"$CMD\" = \"$n\" ] && {\n" +
" CMD=\"$(echo \"$CMD\" | perl -pe 's,[^a-zA-Z0-9],,g')\"\n" +
" " + functionName + "_$CMD\n" +
" return\n" +
" }\n" +
" done\n" +
"\n" +
" COMPREPLY=()\n" +
"}\n");
for (final Cmd cmd : commands) {
cmd(depthPlusOne, functionName, cmd);
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy