Thermostat/PluginTabCompletion

From IcedTea

Jump to: navigation, search

Thermostat Home

Contents

1 Introduction

Thermostat plugins may provide cli/shell commands, and these commands often have options, or flags, which often take arguments.

Built in to Thermostat shell is tab completion for command names, including those provided by plugins. Also built in to the shell is tab completion for all options or flags and subcommands for each of these commands, so long as these options, flags, and subcommands properly declared (in the plugin's thermostat-plugin.xml).

That is to say, 'foo-command subcommand --option' could potentially be tab-completed in Thermostat shell by typing 'foo<TAB>s<TAB><TAB>o<TAB>', for example. The progression would look like this:

$ 'foo<TAB>'
> 'foo-command '
$ 'foo-command s<TAB>'
> 'foo-command subcommand '
$ 'foo-command subcommand <TAB>'
> 'foo-command subcommand --'
$ 'foo-command subcommand --o<TAB>'
> 'foo-command subcommand --option '

For more information on thermostat-plugin.xml, see the Extension Tutorial.

2 Built-in Argument Tab Completions

There are also built-in completions for several options' arguments which are common to most all or all commands which come bundled with Thermostat, such as vmId, agentId, dbUrl, logLevel, and filename options.

Notably, these common completions are defined not only for bundled commands, but are available and automatically applied to all commands, even those provided by third-party plugins. In order to take advantage of these "automagic" completions, you simply need to list the relevant option in your plugin's thermostat-plugin.xml. See the "automatic completion" code sample for more detail.

3 Defining Custom Completions

The following sections will explain how you can add tab completions to your commands.

3.1 Related Classes and Interfaces

Below is a list of classes and interfaces that you can make use of to implement tab completion for your plugin. The Javadoc comments on each should provide some guidance.

Most likely, the classes you will find yourself using are CompleterService, CompletionFinder, CompletionFinderTabCompleter, CompletionInfo, and CliCommandOption.

CompleterService is the interface which defines an OSGi service which provides tab completions for a command, or set of commands. This is your entry point to the Thermostat tab completion API. To start, create a concrete implementation class which implements CompleterService. Implement the required methods, which define which command(s) your tab completions will be added to, and define the completions supplied for each option. Note that the options are not command-specific - if your plugin provides foo-command with options -a and -b, and bar-command with options -a and -c, all of which require tab completions, then you should register an ACompleterService which services both foo-command and bar-command, as well as BCompleterService and CCompleterService which service only 'foo-command -b' and 'bar-command -c', respectively. Once you have implemented your CompleterService(s), register it as a CompleterService implementation in your bundle's Activator, or use OSGi Declarative Services annotations. The service will be detected by Thermostat when your bundle is activated and your completions will be applied and available in the shell.

There is an important gotcha when registering CompleterServices: the mechanism for detecting these completer services only checks the completions they provide once, at first detection. For this reason it is mandatory for CompleterServices to do their very best to return correct results from their getCommands and getOptionCompleters implementations, even if there are dependencies which have not yet appeared. You should design your completer service so that any missing dependency issues are delegated to ex. your CompletionFinder, so that your CompleterService can offer a CliCommandOption/TabCompleter pairing to the Thermostat tab completion mechanism under all circumstances.

In other words, even if there are missing service dependencies at runtime, your CompleterService should not return an empty or partially-populated getOptionCompleters map, or else your tab completions may not work or only work unreliably and intermittently.

If, for some reason, it does not seem possible to design your CompleterService this way, then the recommended path is to only register your CompleterService once the required dependencies become available. One case where this may happen is if your CompleterService requires the FileNameTabCompleter service. See the filename completion example.

CompleterService

AbstractCompleterService

CliCommandOption

CompletionFinderTabCompleter

TabCompleter

CompletionFinder

AbstractCompletionFinder

CompletionInfo

FileNameTabCompleter

DirectoryContentsCompletionFinder

3.2 Code Samples

3.2.1 Automatic vmId/agentId/dbUrl/logLevel completion

Here is a mostly-complete example of a thermostat-plugin.xml which would be eligible for automatic vmId completion.

<plugin xmlns="http://icedtea.classpath.org/thermostat/plugins/v1.0"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://icedtea.classpath.org/thermostat/plugins/v1.0 thermostat-plugin.xsd">
  <commands>
    <command>
      <name>foo-command</name>
      <summary>Thermostat tab completion example</summary>
      <description>A snippet from an example thermostat-plugin.xml</description>
      <options>
        <option>
          <long>vmId</long>
          <short>v</short>
          <argument>vm</argument>
          <required>false</required>
          <description>the VM to foo</description>
        </option>
      </options>
      <environments>
        <environment>cli</environment>
        <environment>shell</environment>
      </environments>
      <bundles>
        ... snip ...
      </bundles>
    </command>
  </commands>
  <extensions>
    <extension>
      <name>gui</name>
      <bundles>
        ... snip ...
      </bundles>
    </extension>
  </extensions>
</plugin>

The important tags are the <long> and <short> tags. The <argument> and <description> are allowed to vary, and of course the <required> tag is plugin context dependent.

If you include this option snippet in your thermostat-plugin.xml, then your command will automatically receive tab completions for your -v/--vmId option. Likewise, you will automatically receive tab completions if you include a similar <option></option> for -a/--agentId, etc.

Of course, there are situations where these automatic completions do not suffice - for example, if you need completions for a vmId argument, but do not wish to have the option itself called "--vmId", or if your command has a custom command-specific "--foo" option which also requires tab completions. For solutions to these problems, please see the code examples below.

3.2.2 Filename completion, with a differing option name

Notice in this example that there is a required dependency - the FileNameTabCompleter - and that the Declarative Services annotations used with the CompleterService implicitly specify that dependency is required at activation time. This is an unfortunate corner case where the CompleterService registration must be delayed until an external dependency becomes available. However, since the FileNameTabCompleter is provided by Thermostat, it should be available very quickly once the shell opens, so the delay is expected to be very short and imperceptible to users.

The end result here is that "foo-command --config <TAB>" will provide file/path completions similar to what you would expect from 'cd' within a UNIX shell.

thermostat-plugin.xml:

<plugin xmlns="http://icedtea.classpath.org/thermostat/plugins/v1.0"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://icedtea.classpath.org/thermostat/plugins/v1.0 thermostat-plugin.xsd">
  <commands>
    <command>
      <name>foo-command</name>
      <summary>Thermostat tab completion example</summary>
      <description>A snippet from an example thermostat-plugin.xml</description>
      <options>
        <option>
          <long>config</long>
          <short>c</short>
          <argument>configFile</argument>
          <required>false</required>
          <description>config file path</description>
        </option>
      </options>
      <environments>
        <environment>cli</environment>
        <environment>shell</environment>
      </environments>
      <bundles>
        ... snip ...
      </bundles>
    </command>
  </commands>
  <extensions>
    <extension>
      <name>gui</name>
      <bundles>
        ... snip ...
      </bundles>
    </extension>
  </extensions>
</plugin>

FooCommandCompleterService.java:

import com.redhat.thermostat.common.cli.CompleterService;
import com.redhat.thermostat.common.cli.CliCommandOption;
import com.redhat.thermostat.common.cli.FileNameTabCompleter;
import com.redhat.thermostat.common.cli.TabCompleter;

import java.util.Collections;
import java.util.Map;
import java.util.Set;

import org.apache.felix.scr.annotations.Component;
import org.apache.felix.scr.annotations.Reference;
import org.apache.felix.scr.annotations.Service;

@Component
@Service
public class FooCommandCompleterService implements CompleterService {
    
    // "c" and "config" here must correspond to the option short/long in thermostat-plugin.xml
    static final CliCommandOption CONFIG_FILE_OPTION = new CliCommandOption("c", "config", true, "config file path", false);

    @Reference
    private FileNameTabCompleter fileNameTabCompleter;

    @Override
    public Set<String> getCommands() {
        return Collections.singleton("foo-command");
    }

    @Override
    public Map<CliCommandOption, ? extends TabCompleter> getOptionCompleters() {
        return Collections.singletonMap(CONFIG_FILE_OPTION, fileNameTabCompleter);
    }

    @Override
    public Map<String, Map<CliCommandOption, ? extends TabCompleter>> getSubcommandCompleters() {
        return Collections.emptyMap();
    }
    
    public void bindFileNameTabCompleter(FileNameTabCompleter fileNameTabCompleter) {
         this.fileNameTabCompleter = fileNameTabCompleter;
    }

    public void unbindFileNameTabCompleter(FileNameTabCompleter fileNameTabCompleter) {
         this.fileNameTabCompleter = null;
    }
    
}

3.2.3 Directory contents completion

This example will assume your thermostat-plugin.xml contains a command option of -c/--config, which is expected to a be a file which resides within paths.getSystemPluginConfigurationDirectory().getAbsolutePath() + "/foo-command/" , where paths is a CommonPaths instance. The FooCommand will, however, expect a full file path - but the tab completions will only provide files within this directory, and which have the file extension ".cfg", for illustration purposes.

FooCommandCompleterService.java:

import com.redhat.thermostat.common.cli.CompleterService;
import com.redhat.thermostat.common.cli.CliCommandOption;
import com.redhat.thermostat.common.cli.CompletionFinderTabCompleter;
import com.redhat.thermostat.common.cli.TabCompleter;
import org.foo.FooCommand;

import java.util.Collections;
import java.util.Map;
import java.util.Set;

import org.apache.felix.scr.annotations.Component;
import org.apache.felix.scr.annotations.Reference;
import org.apache.felix.scr.annotations.Service;

@Component
@Service
public class FooCommandCompleterService implements CompleterService {

    static final CliCommandOption CONFIG_OPTION = new CliCommandOption("c", "config", true, "the config file to use", true);

    @Reference
    private FooConfigFinder fooConfigFinder;

    @Override
    public Set<String> getCommands() {
        return Collections.singleton(FooCommand.COMMAND_NAME); // "foo-command"
    }

    @Override
    public Map<CliCommandOption, ? extends TabCompleter> getOptionCompleters() {
        return Collections.singletonMap(CONFIG_OPTION, new CompletionFinderTabCompleter(fooConfigFinder));
    }

    @Override
    public Map<String, Map<CliCommandOption, ? extends TabCompleter>> getSubcommandCompleters() {
        return Collections.emptyMap();
    }

    public void bindFooConfigFinder(FooConfigFinder fooConfigFinder) {
        this.fooConfigFinder = fooConfigFinder;
    }

    public void unbindFooConfigFinder(FooConfigFinder fooConfigFinder) {
        this.fooConfigFinder = null;
    }

}

FooConfigFinder.java:

import com.redhat.thermostat.common.cli.CompletionFinder;

interface FooConfigFinder extends CompletionFinder {
}

FooConfigFinderImpl.java:

import com.redhat.thermostat.common.cli.CompletionInfo;
import com.redhat.thermostat.common.cli.DirectoryContentsCompletionFinder;
import com.redhat.thermostat.shared.config.CommonPaths;

import java.io.File;
import java.io.FileFilter;
import java.util.Collections;
import java.util.List;

import org.apache.felix.scr.annotations.Component;
import org.apache.felix.scr.annotations.Service;

@Component
@Service
public class FooConfigFinderImpl implements FooConfigFinder {

    @Reference
    private CommonPaths commonPaths;

    private DirectoryContentsCompletionFinder directoryFinder;

    @Override
    public List<CompletionInfo> findCompletions() {
        if (commonPaths == null) {
            directoryFinder = null;
            return Collections.emptyList();
        }
        if (directoryFinder == null) {
            File configDirectory = new File(getConfigFileDirectory(commonPaths));
            directoryFinder = new DirectoryContentsCompletionFinder(configDirectory);
            directoryFinder.setFileFilter(new FooConfigFilter());
            directoryFinder.setCompletionMode(DirectoryContentsCompletionFinder.CompletionMode.CANONICAL_PATH);
        }
        return directoryFinder.findCompletions();
    }

    public void bindCommonPaths(CommonPaths commonPaths) {
        this.commonPaths = commonPaths;
    }

    public void unbindCommonPaths(CommonPaths commonPaths) {
        this.commonPaths = null;
    }

    static String getConfigFileDirectory(CommonPaths paths) {
        return paths.getSystemPluginConfigurationDirectory().getAbsolutePath() + "/foo-command/";
    }

    static class FooConfigFilter implements FileFilter {
        @Override
        public boolean accept(File file) {
            return file.isFile() && file.getName().endsWith(".cfg");
        }
    }

}

3.2.4 Custom command-specific option completion

This example omits the thermostat-plugin.xml, which is required but already covered in previous examples. FooDaoImpl is also omitted for brevity.

FooCommandCompleterService.java:

import com.redhat.thermostat.common.cli.CompleterService;
import com.redhat.thermostat.common.cli.CliCommandOption;
import com.redhat.thermostat.common.cli.CompletionFinderTabCompleter;
import com.redhat.thermostat.common.cli.TabCompleter;

import java.util.Collections;
import java.util.Map;
import java.util.Set;

import org.apache.felix.scr.annotations.Component;
import org.apache.felix.scr.annotations.Reference;
import org.apache.felix.scr.annotations.Service;

@Component
@Service
public class FooCommandCompleterService implements CompleterService {

    static final CliCommandOption BAR_OPTION = new CliCommandOption("b", "bar", true, "the foo to bar", true);

    @Reference
    private FooCompletionFinder fooCompletionFinder;

    @Override
    public Set<String> getCommands() {
        return Collections.singleton(FooCommand.COMMAND_NAME); // "foo-command"
    }

    @Override
    public Map<CliCommandOption, ? extends TabCompleter> getOptionCompleters() {
        return Collections.singletonMap(BAR_OPTION, new CompletionFinderTabCompleter(fooCompletionFinder));
    }

    @Override
    public Map<String, Map<CliCommandOption, ? extends TabCompleter>> getSubcommandCompleters() {
        return Collections.emptyMap();
    }

    public void bindFooCompletionFinder(FooCompletionFinder fooCompletionFinder) {
        this.fooCompletionFinder = fooCompletionFinder;
    }

    public void unbindFooCompletionFinder(FooCompletionFinder fooCompletionFinder) {
        this.fooCompletionFinder = null;
    }

}

FooCompletionFinder.java:

import com.redhat.thermostat.common.cli.CompletionFinder;

interface FooCompletionFinder extends CompletionFinder {
}

FooCompletionFinderImpl.java:

import com.redhat.thermostat.common.cli.CompletionInfo;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

import org.foo.Foo;
import org.foo.FooDao;

import org.apache.felix.scr.annotations.Component;
import org.apache.felix.scr.annotations.Reference;
import org.apache.felix.scr.annotations.Service;

@Component
@Service
public class FooCompletionFinderImpl implements FooCompletionFinder {

    @Reference
    private FooDao fooDao;

    @Override
    public List<CompletionInfo> findCompletions() {
        if (fooDao == null) {
            return Collections.emptyList();
        }
        List<CompletionInfo> result = new ArrayList<>();
        for (Foo foo : fooDao.getAllFoos()) {
            result.add(new CompletionInfo(Integer.toString(foo.getValue())));
        }
        return result;
    }

    public void bindFooDao(FooDao fooDao) {
        this.fooDao = fooDao;
    }

    public void unbindFooDao(FooDao fooDao) {
        this.fooDao = null;
    }
}

FooDao.java:

import java.util.List;

public interface FooDao {
    List<Foo> getAllFoos();
}

Foo.java:

public class Foo {
    private int value;

    public void setValue(int value) {
        this.value = value;
    }

    public int getValue() {
        return value;
    }
}

3.2.5 Subcommand completion

The result here is that typing "foo-command <TAB>" results in completions for "bar", "baz", "start", and "stop", which are the example subcommands in this scenario.

The FooCompletionFinderImpl is omitted in this example. See the previous example if you are unsure of how the FooCompletionFinder field works.

FooCommandCompleterService.java:

import com.redhat.thermostat.common.cli.CompleterService;
import com.redhat.thermostat.common.cli.CliCommandOption;
import com.redhat.thermostat.common.cli.CompletionFinderTabCompleter;
import com.redhat.thermostat.common.cli.TabCompleter;

import java.util.Arrays;
import java.util.Collections;
import java.util.Map;
import java.util.Set;

import org.apache.felix.scr.annotations.Component;
import org.apache.felix.scr.annotations.Reference;
import org.apache.felix.scr.annotations.Service;

import org.foo.Foo;
import org.foo.FooDao;

@Component
@Service
public class FooCommandCompleterService implements CompleterService {

    // better practice would be to expose this in FooCommand, but it's here for this example
    static final List<String> SUBCOMMANDS = Collections.unmodifiableList(Arrays.asList(
        "bar", "baz", "start", "stop"
    ));

    static final CliCommandOption FOO_OPTION = new CliCommandOption("f", "foo", true, "the foo to bar", true);

    @Reference
    private FooCompletionFinder fooCompletionFinder;

    @Override
    public Set<String> getCommands() {
        return Collections.singleton(FooCommand.COMMAND_NAME); // "foo-command"
    }

    @Override
    public Map<CliCommandOption, ? extends TabCompleter> getOptionCompleters() {
        return Collections.<CliCommandOption, ? extends TabCompleter>emptyMap();
    }

    @Override
    public Map<String, Map<CliCommandOption, ? extends TabCompleter>> getSubcommandCompleters() {
        Map<String, Map<CliCommandOption, ? extends TabCompleter>> map = new HashMap<>();
        for (String subcommand : SUBCOMMANDS) {
            map.put(subcommand, Collections.singletonMap(FOO_OPTION, fooCompletionFinder));
        }
        return map;
    }

}

4 Known Issues

Below are known issues with Thermostat's tab completion API.

4.1 Bundle Activation

Plugin-provided CompleterServices do not become available until the OSGi bundles containing them are activated. This is expected, normal operation of OSGi. However, it means that at first, commands in the shell do not always have tab completions available. Completions for agentId, vmId, dbUrl, logLevel, and filename are offered immediately because those CompleterServices are registered right away when the shell itself is activated. Plugin-provided CompleterServices must currently first be made available by running a command in the bundle which provides the CompleterService, after which point the completion will be detected and added by the Thermostat tab completion API.

If you have a plugin which provides "foo-command", which has an option -a, and a CompleterService which provides completions for option -a, this means that on the first run within shell, "foo-command -a<TAB>" will simply insert a space, rather than providing completions for -a. The suggested workaround is to first simply run "foo-command<ENTER>", which will run your command with no arguments and thus activate your plugin bundle. If everything is wired up properly then your CompleterService will become available and from this point on, "foo-command -a<TAB>" will provide tab completions as expected.

Personal tools