Skip to content
This repository has been archived by the owner on Nov 9, 2017. It is now read-only.

Commit

Permalink
add bash completion auto generator
Browse files Browse the repository at this point in the history
  • Loading branch information
Patrick Huang committed Feb 21, 2013
1 parent 967c839 commit 0a6ec41
Show file tree
Hide file tree
Showing 2 changed files with 242 additions and 0 deletions.
31 changes: 31 additions & 0 deletions zanata-cli/pom.xml
Expand Up @@ -81,6 +81,37 @@
</plugins>
</build>

<profiles>
<profile>
<id>bash</id>
<build>
<plugins>
<plugin>
<!-- See the script etc/zanataj -->
<groupId>org.codehaus.mojo</groupId>
<artifactId>exec-maven-plugin</artifactId>
<version>1.2.1</version>
<executions>
<execution>
<phase>test</phase>
<goals>
<goal>java</goal>
</goals>
</execution>
</executions>
<configuration>
<mainClass>org.zanata.client.BashCompletionGenerator</mainClass>
<arguments>
<argument>${project.build.directory}/zanata-cli-completion</argument>
</arguments>
<classpathScope>test</classpathScope>
</configuration>
</plugin>
</plugins>
</build>
</profile>
</profiles>


<dependencies>
<dependency>
Expand Down
@@ -0,0 +1,211 @@
package org.zanata.client;

import java.io.File;
import java.io.IOException;
import java.lang.reflect.AccessibleObject;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.kohsuke.args4j.Option;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.zanata.client.commands.BasicOptions;
import com.google.common.base.Charsets;
import com.google.common.base.Function;
import com.google.common.base.Joiner;
import com.google.common.base.Predicate;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import com.google.common.io.Files;

/**
* @author Patrick Huang <a
* href="mailto:pahuang@redhat.com">pahuang@redhat.com</a>
*/
public class BashCompletionGenerator
{
private static final Logger log = LoggerFactory.getLogger(BashCompletionGenerator.class);

private static final Joiner joiner = Joiner.on(" ");

private List<String> baseCommands;
private List<Option> genericOptions;
private Map<String, List<Option>> commandOptions = Maps.newHashMap();
private Set<Option> allOptions = Sets.newHashSet();

public static void main(String[] args) throws IOException
{
ZanataClient client = new ZanataClient();
BashCompletionGenerator generator = new BashCompletionGenerator();

LinkedHashMap<String, BasicOptions> commands = client.getOptionsMap();
generator.baseCommands = ImmutableList.copyOf(Iterables.transform(commands.values(), new Function<BasicOptions, String>()
{
@Override
public String apply(BasicOptions input)
{
return input.getCommandName();
}
}));

generator.genericOptions = getOptions(ZanataClient.class);
generator.allOptions.addAll(generator.genericOptions);

for (BasicOptions command : commands.values())
{
List<Option> options = getOptions(command.getClass());
// do we still want generic options to appear?
// options.removeAll(generator.genericOptions);
generator.commandOptions.put(command.getCommandName(), options);
generator.allOptions.addAll(options);
}

File to = new File(".", "zanata-cli-completion");
if (args != null && args.length == 1)
{
to = new File(args[0]);
}
log.info("writing bash completion file to {}", to);
generateFile(client, generator, to);
}

private static List<Option> getOptions(Class<?> bean)
{
ImmutableList.Builder<Option> allOptions = ImmutableList.builder();
// recursively process all the methods/fields.
for (Class c = bean; c != null; c = c.getSuperclass())
{
ImmutableList.Builder<AccessibleObject> builder = ImmutableList.builder();
List<AccessibleObject> fieldAndMethods = builder.add(c.getDeclaredFields()).add(c.getDeclaredMethods()).build();
for (AccessibleObject accessibleObject : fieldAndMethods)
{
Option option = accessibleObject.getAnnotation(Option.class);
if (option != null)
{
allOptions.add(option);
}
}
}
return allOptions.build();
}

private static void generateFile(ZanataClient client, BashCompletionGenerator generator, File to) throws IOException
{
// if we can use groovy here then here doc will be really handy...
String commands = joiner.join(generator.baseCommands);
String genericOptions = optionsToString(generator.genericOptions);

List<String> lines = Lists.newArrayList();
lines.add("# ");
lines.add("# Completion for " + client.getCommandDescription());
lines.add("# Generated by " + BashCompletionGenerator.class.getSimpleName());
lines.add("# ");

lines.add("_zanata()");
lines.add("{");
lines.add(" local cur prev opts base cmds");
lines.add(" COMPREPLY=()");
lines.add(" cur=\"${COMP_WORDS[COMP_CWORD]}\"");
lines.add(" prev=\"${COMP_WORDS[COMP_CWORD-1]}\"");
lines.add(" base=\"${COMP_WORDS[1]}\"");
// lines.add(" opts=\"" + commands + " " + genericOptions + "\"");
lines.add(" cmds=\"" + commands + "\"");

// basic commands as first argument
lines.add(" if [[ ${#COMP_WORDS[@]} == 2 ]] ; then");
lines.add(" COMPREPLY=( $(compgen -W \"${cmds} --help\" -- ${cur}) )");
lines.add(" return 0");
lines.add(" fi");

// special treatment for help
lines.add(" if [[ ${COMP_WORDS[1]} == '--help' ]] ; then");
lines.add(" COMPREPLY=( $(compgen -W \"${cmds}\" -- ${cur}) )");
lines.add(" return 0");
lines.add(" fi");


// for each special case
lines.add(" case \"${prev}\" in ");
// case for file type option
for (Option option : findByMetaVar(generator, "file"))
{
lines.add(String.format(" %s)", option.name()));
lines.add(" COMPREPLY=( $(compgen -df ${cur}) )");
lines.add(" return 0");
lines.add(" ;;");
}
// case for directory type option
for (Option option : findByMetaVar(generator, "dir"))
{
lines.add(String.format(" %s)", option.name()));
lines.add(" COMPREPLY=( $(compgen -f ${cur}) )");
lines.add(" return 0");
lines.add(" ;;");
}
// case for url
for (Option option : findByMetaVar(generator, "url"))
{
lines.add(String.format(" %s)", option.name()));
lines.add(" COMPREPLY=( $(compgen -A hostname ${cur}) )");
lines.add(" return 0");
lines.add(" ;;");
}
lines.add(" esac");

// for each command
// TODO should eliminate prev appeared options reappearing
lines.add(" case \"${base}\" in ");
// case for individual commands
for (Map.Entry<String, List<Option>> entry : generator.commandOptions.entrySet())
{
String localVar = entry.getKey() + "_opts";
lines.add(String.format(" %s)", entry.getKey()));
lines.add(String.format(" local %s=\"%s\"", localVar, optionsToString(entry.getValue())));
lines.add(String.format(" COMPREPLY=( $(compgen -W \"${%s}\" -- ${cur}) )", localVar));
lines.add(" return 0");
lines.add(" ;;");
}
lines.add(" esac");

findByMetaVar(generator, "file");
lines.add("}");
lines.add("complete -F _zanata " + client.getCommandName());

Joiner lineJoiner = Joiner.on(System.getProperty("line.separator"));
Files.write(lineJoiner.join(lines), to, Charsets.UTF_8);
}

private static String optionsToString(Iterable<Option> options)
{
return joiner.join(Iterables.transform(options, OptionToName.FUNCTION));
}

private static Iterable<Option> findByMetaVar(BashCompletionGenerator generator, final String expectedMetaVar)
{
return Iterables.filter(generator.allOptions, new Predicate<Option>()
{
@Override
public boolean apply(Option input)
{
// THIS IS NOT QUITE RELIABLE. but hey :)
return input.metaVar().toLowerCase().contains(expectedMetaVar);
}
});
}

static enum OptionToName implements Function<Option, String>
{
FUNCTION;

@Override
public String apply(Option input)
{
return input.name();
}
}
}

0 comments on commit 0a6ec41

Please sign in to comment.