You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@karaf.apache.org by gn...@apache.org on 2014/03/05 16:08:25 UTC

[03/10] [KARAF-2805] Clean console and commands model

http://git-wip-us.apache.org/repos/asf/karaf/blob/e7d23bef/shell/core/src/main/java/org/apache/karaf/shell/impl/console/ConsoleSessionImpl.java
----------------------------------------------------------------------
diff --git a/shell/core/src/main/java/org/apache/karaf/shell/impl/console/ConsoleSessionImpl.java b/shell/core/src/main/java/org/apache/karaf/shell/impl/console/ConsoleSessionImpl.java
new file mode 100644
index 0000000..7988035
--- /dev/null
+++ b/shell/core/src/main/java/org/apache/karaf/shell/impl/console/ConsoleSessionImpl.java
@@ -0,0 +1,583 @@
+/*
+ * 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.apache.karaf.shell.impl.console;
+
+import java.io.CharArrayWriter;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.InterruptedIOException;
+import java.io.PrintStream;
+import java.io.Reader;
+import java.lang.management.ManagementFactory;
+import java.util.List;
+import java.util.Map;
+import java.util.Properties;
+import java.util.concurrent.ArrayBlockingQueue;
+import java.util.concurrent.BlockingQueue;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import jline.UnsupportedTerminal;
+import jline.console.ConsoleReader;
+import jline.console.history.MemoryHistory;
+import jline.console.history.PersistentHistory;
+import org.apache.felix.service.command.CommandProcessor;
+import org.apache.felix.service.command.CommandSession;
+import org.apache.felix.service.command.Converter;
+import org.apache.felix.service.command.Function;
+import org.apache.felix.service.threadio.ThreadIO;
+import org.apache.karaf.shell.api.console.Command;
+import org.apache.karaf.shell.api.console.Completer;
+import org.apache.karaf.shell.api.console.History;
+import org.apache.karaf.shell.api.console.Registry;
+import org.apache.karaf.shell.api.console.Session;
+import org.apache.karaf.shell.api.console.SessionFactory;
+import org.apache.karaf.shell.api.console.Terminal;
+import org.apache.karaf.shell.impl.console.parsing.Parser;
+import org.apache.karaf.shell.support.ShellUtil;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class ConsoleSessionImpl implements Session {
+
+    public static final String SHELL_INIT_SCRIPT = "karaf.shell.init.script";
+    public static final String SHELL_HISTORY_MAXSIZE = "karaf.shell.history.maxSize";
+    public static final String PROMPT = "PROMPT";
+    public static final String DEFAULT_PROMPT = "\u001B[1m${USER}\u001B[0m@${APPLICATION}(${SUBSHELL})> ";
+
+    private static final Logger LOGGER = LoggerFactory.getLogger(ConsoleSessionImpl.class);
+
+    // Input stream
+    final BlockingQueue<Integer> queue = new ArrayBlockingQueue<Integer>(1024);
+    final ConsoleInputStream console = new ConsoleInputStream();
+    final Pipe pipe = new Pipe();
+    volatile boolean running;
+    volatile boolean eof;
+
+    final SessionFactory factory;
+    final ThreadIO threadIO;
+    final InputStream in;
+    final PrintStream out;
+    final PrintStream err;
+    private Runnable closeCallback;
+
+    final CommandSession session;
+    final Registry registry;
+    final Terminal terminal;
+    final History history;
+    final ConsoleReader reader;
+
+    private boolean interrupt;
+    private Thread thread;
+
+    public ConsoleSessionImpl(SessionFactory factory,
+                              CommandProcessor processor,
+                              ThreadIO threadIO,
+                              InputStream in,
+                              PrintStream out,
+                              PrintStream err,
+                              Terminal term,
+                              String encoding,
+                              Runnable closeCallback) {
+        // Arguments
+        this.factory = factory;
+        this.threadIO = threadIO;
+        this.in = in;
+        this.out = out;
+        this.err = err;
+        this.closeCallback = closeCallback;
+
+        // Terminal
+        terminal = term == null ? new JLineTerminal(new UnsupportedTerminal()) : term;
+
+
+        // Console reader
+        try {
+            reader = new ConsoleReader(null,
+                    in != null ? console : null,
+                    out,
+                    terminal instanceof JLineTerminal ? ((JLineTerminal) terminal).getTerminal() : new KarafTerminal(terminal),
+                    encoding);
+        } catch (IOException e) {
+            throw new RuntimeException("Error opening console reader", e);
+        }
+
+        // History
+        final File file = getHistoryFile();
+        try {
+            file.getParentFile().mkdirs();
+            reader.setHistory(new KarafFileHistory(file));
+        } catch (Exception e) {
+            LOGGER.error("Can not read history from file " + file + ". Using in memory history", e);
+        }
+        if (reader.getHistory() instanceof MemoryHistory) {
+            String maxSizeStr = System.getProperty(SHELL_HISTORY_MAXSIZE);
+            if (maxSizeStr != null) {
+                ((MemoryHistory) this.reader.getHistory()).setMaxSize(Integer.parseInt(maxSizeStr));
+            }
+        }
+        history = new HistoryWrapper(reader.getHistory());
+
+        // Registry
+        registry = new RegistryImpl(factory.getRegistry());
+        registry.register(factory);
+        registry.register(this);
+        registry.register(registry);
+        registry.register(terminal);
+        registry.register(history);
+
+        // Completers
+        Completer completer = new CommandsCompleter(factory);
+        reader.addCompleter(new CompleterAsCompletor(this, completer));
+        registry.register(completer);
+        registry.register(new CommandNamesCompleter());
+
+        // Session
+        session = processor.createSession(in != null ? console : null, out, err);
+        Properties sysProps = System.getProperties();
+        for (Object key : sysProps.keySet()) {
+            session.put(key.toString(), sysProps.get(key));
+        }
+        session.put(".session", this);
+        session.put(".commandSession", session);
+        session.put(".jline.reader", reader);
+        session.put(".jline.terminal", reader.getTerminal());
+        session.put(".jline.history", reader.getHistory());
+        session.put(Session.SCOPE, "shell:bundle:*");
+        session.put(Session.SUBSHELL, "");
+        session.put(Session.COMPLETION_MODE, loadCompletionMode());
+        session.put("USER", ShellUtil.getCurrentUserName());
+        session.put("APPLICATION", System.getProperty("karaf.name", "root"));
+        session.put("#LINES", new Function() {
+            public Object execute(CommandSession session, List<Object> arguments) throws Exception {
+                return Integer.toString(terminal.getHeight());
+            }
+        });
+        session.put("#COLUMNS", new Function() {
+            public Object execute(CommandSession session, List<Object> arguments) throws Exception {
+                return Integer.toString(terminal.getWidth());
+            }
+        });
+        session.put("pid", getPid());
+    }
+
+    /**
+     * Subclasses can override to use a different history file.
+     *
+     * @return
+     */
+    protected File getHistoryFile() {
+        String defaultHistoryPath = new File(System.getProperty("user.home"), ".karaf/karaf.history").toString();
+        return new File(System.getProperty("karaf.history", defaultHistoryPath));
+    }
+
+    @Override
+    public Terminal getTerminal() {
+        return terminal;
+    }
+
+    public History getHistory() {
+        return history;
+    }
+
+    @Override
+    public Registry getRegistry() {
+        return registry;
+    }
+
+    @Override
+    public SessionFactory getFactory() {
+        return factory;
+    }
+
+    public void close() {
+        if (!running) {
+            return;
+        }
+        if (reader.getHistory() instanceof PersistentHistory) {
+            try {
+                ((PersistentHistory) reader.getHistory()).flush();
+            } catch (IOException e) {
+                // ignore
+            }
+        }
+        running = false;
+        pipe.interrupt();
+        if (closeCallback != null) {
+            closeCallback.run();
+        }
+    }
+
+    public void run() {
+        try {
+            threadIO.setStreams(session.getKeyboard(), out, err);
+            thread = Thread.currentThread();
+            running = true;
+            pipe.start();
+            Properties brandingProps = Branding.loadBrandingProperties(terminal);
+            welcome(brandingProps);
+            setSessionProperties(brandingProps);
+            String scriptFileName = System.getProperty(SHELL_INIT_SCRIPT);
+            executeScript(scriptFileName);
+            while (running) {
+                try {
+                    String command = readAndParseCommand();
+                    if (command == null) {
+                        break;
+                    }
+                    //session.getConsole().println("Executing: " + line);
+                    Object result = session.execute(command);
+                    if (result != null) {
+                        session.getConsole().println(session.format(result, Converter.INSPECT));
+                    }
+                } catch (InterruptedIOException e) {
+                    //System.err.println("^C");
+                    // TODO: interrupt current thread
+                } catch (InterruptedException e) {
+                    //interrupt current thread
+                } catch (Throwable t) {
+                    ShellUtil.logException(this, t);
+                }
+            }
+            close();
+        } finally {
+            try {
+                threadIO.close();
+            } catch (Throwable t) {
+                // Ignore
+            }
+        }
+    }
+
+    @Override
+    public Object execute(CharSequence commandline) throws Exception {
+        return session.execute(commandline);
+    }
+
+    @Override
+    public Object get(String name) {
+        return session.get(name);
+    }
+
+    @Override
+    public void put(String name, Object value) {
+        session.put(name, value);
+    }
+
+    @Override
+    public InputStream getKeyboard() {
+        return session.getKeyboard();
+    }
+
+    @Override
+    public PrintStream getConsole() {
+        return session.getConsole();
+    }
+
+    @Override
+    public String resolveCommand(String name) {
+        // TODO: optimize
+        if (!name.contains(":")) {
+            String[] scopes = ((String) get(Session.SCOPE)).split(":");
+            List<Command> commands = registry.getCommands();
+            for (String scope : scopes) {
+                for (Command command : commands) {
+                    if ((Session.SCOPE_GLOBAL.equals(scope) || command.getScope().equals(scope)) && command.getName().equals(name)) {
+                        return command.getScope() + ":" + name;
+                    }
+                }
+            }
+        }
+        return name;
+    }
+
+    @Override
+    public String readLine(String prompt, Character mask) throws IOException {
+        return reader.readLine(prompt, mask);
+    }
+
+    private String loadCompletionMode() {
+        String mode;
+        try {
+            File shellCfg = new File(System.getProperty("karaf.etc"), "/org.apache.karaf.shell.cfg");
+            Properties properties = new Properties();
+            properties.load(new FileInputStream(shellCfg));
+            mode = (String) properties.get("completionMode");
+            if (mode == null) {
+                LOGGER.debug("completionMode property is not defined in etc/org.apache.karaf.shell.cfg file. Using default completion mode.");
+                mode = Session.COMPLETION_MODE_GLOBAL;
+            }
+        } catch (Exception e) {
+            LOGGER.warn("Can't read {}/org.apache.karaf.shell.cfg file. The completion is set to default.", System.getProperty("karaf.etc"));
+            mode = Session.COMPLETION_MODE_GLOBAL;
+        }
+        return mode;
+    }
+
+    private String readAndParseCommand() throws IOException {
+        String command = null;
+        boolean loop = true;
+        boolean first = true;
+        while (loop) {
+            checkInterrupt();
+            String line = reader.readLine(first ? getPrompt() : "> ");
+            if (line == null) {
+                break;
+            }
+            if (command == null) {
+                command = line;
+            } else {
+                if (command.charAt(command.length() - 1) == '\\') {
+                    command = command.substring(0, command.length() - 1) + line;
+                } else {
+                    command += "\n" + line;
+                }
+            }
+            if (reader.getHistory().size() == 0) {
+                reader.getHistory().add(command);
+            } else {
+                // jline doesn't add blank lines to the history so we don't
+                // need to replace the command in jline's console history with
+                // an indented one
+                if (command.length() > 0 && !" ".equals(command)) {
+                    reader.getHistory().replace(command);
+                }
+            }
+            if (command.length() > 0 && command.charAt(command.length() - 1) == '\\') {
+                loop = true;
+                first = false;
+            } else {
+                try {
+                    Class<?> cl = CommandSession.class.getClassLoader().loadClass("org.apache.felix.gogo.runtime.Parser");
+                    Object parser = cl.getConstructor(CharSequence.class).newInstance(command);
+                    cl.getMethod("program").invoke(parser);
+                    loop = false;
+                } catch (Exception e) {
+                    loop = true;
+                    first = false;
+                } catch (Throwable t) {
+                    // Reflection problem ? just quit
+                    loop = false;
+                }
+            }
+        }
+        return command;
+    }
+
+    private void executeScript(String scriptFileName) {
+        if (scriptFileName != null) {
+            Reader r = null;
+            try {
+                File scriptFile = new File(scriptFileName);
+                r = new InputStreamReader(new FileInputStream(scriptFile));
+                CharArrayWriter w = new CharArrayWriter();
+                int n;
+                char[] buf = new char[8192];
+                while ((n = r.read(buf)) > 0) {
+                    w.write(buf, 0, n);
+                }
+                session.execute(new String(w.toCharArray()));
+            } catch (Exception e) {
+                LOGGER.debug("Error in initialization script", e);
+                System.err.println("Error in initialization script: " + e.getMessage());
+            } finally {
+                if (r != null) {
+                    try {
+                        r.close();
+                    } catch (IOException e) {
+                        // Ignore
+                    }
+                }
+            }
+        }
+    }
+
+    protected void welcome(Properties brandingProps) {
+        String welcome = brandingProps.getProperty("welcome");
+        if (welcome != null && welcome.length() > 0) {
+            session.getConsole().println(welcome);
+        }
+    }
+
+    protected void setSessionProperties(Properties brandingProps) {
+        for (Map.Entry<Object, Object> entry : brandingProps.entrySet()) {
+            String key = (String) entry.getKey();
+            if (key.startsWith("session.")) {
+                session.put(key.substring("session.".length()), entry.getValue());
+            }
+        }
+    }
+
+    protected String getPrompt() {
+        try {
+            String prompt;
+            try {
+                Object p = session.get(PROMPT);
+                if (p != null) {
+                    prompt = p.toString();
+                } else {
+                    Properties properties = Branding.loadBrandingProperties(terminal);
+                    if (properties.getProperty("prompt") != null) {
+                        prompt = properties.getProperty("prompt");
+                        // we put the PROMPT in ConsoleSession to avoid to read
+                        // the properties file each time.
+                        session.put(PROMPT, prompt);
+                    } else {
+                        prompt = DEFAULT_PROMPT;
+                    }
+                }
+            } catch (Throwable t) {
+                prompt = DEFAULT_PROMPT;
+            }
+            Matcher matcher = Pattern.compile("\\$\\{([^}]+)\\}").matcher(prompt);
+            while (matcher.find()) {
+                Object rep = session.get(matcher.group(1));
+                if (rep != null) {
+                    prompt = prompt.replace(matcher.group(0), rep.toString());
+                    matcher.reset(prompt);
+                }
+            }
+            return prompt;
+        } catch (Throwable t) {
+            return "$ ";
+        }
+    }
+
+    private void checkInterrupt() throws IOException {
+        if (Thread.interrupted() || interrupt) {
+            interrupt = false;
+            throw new InterruptedIOException("Keyboard interruption");
+        }
+    }
+
+    private void interrupt() {
+        interrupt = true;
+        thread.interrupt();
+    }
+
+    private String getPid() {
+        String name = ManagementFactory.getRuntimeMXBean().getName();
+        String[] parts = name.split("@");
+        return parts[0];
+    }
+
+    private class ConsoleInputStream extends InputStream {
+        private int read(boolean wait) throws IOException {
+            if (!running) {
+                return -1;
+            }
+            checkInterrupt();
+            if (eof && queue.isEmpty()) {
+                return -1;
+            }
+            Integer i;
+            if (wait) {
+                try {
+                    i = queue.take();
+                } catch (InterruptedException e) {
+                    throw new InterruptedIOException();
+                }
+                checkInterrupt();
+            } else {
+                i = queue.poll();
+            }
+            if (i == null) {
+                return -1;
+            }
+            return i;
+        }
+
+        @Override
+        public int read() throws IOException {
+            return read(true);
+        }
+
+        @Override
+        public int read(byte b[], int off, int len) throws IOException {
+            if (b == null) {
+                throw new NullPointerException();
+            } else if (off < 0 || len < 0 || len > b.length - off) {
+                throw new IndexOutOfBoundsException();
+            } else if (len == 0) {
+                return 0;
+            }
+
+            int nb = 1;
+            int i = read(true);
+            if (i < 0) {
+                return -1;
+            }
+            b[off++] = (byte) i;
+            while (nb < len) {
+                i = read(false);
+                if (i < 0) {
+                    return nb;
+                }
+                b[off++] = (byte) i;
+                nb++;
+            }
+            return nb;
+        }
+
+        @Override
+        public int available() throws IOException {
+            return queue.size();
+        }
+    }
+
+    private class Pipe extends Thread {
+        public Pipe() {
+            super("Karaf shell pipe thread");
+            setDaemon(true);
+        }
+
+        public void run() {
+            try {
+                while (running) {
+                    try {
+                        int c = in.read();
+                        if (c == -1) {
+                            return;
+                        } else if (c == 4 && !ShellUtil.getBoolean(ConsoleSessionImpl.this, Session.IGNORE_INTERRUPTS)) {
+                            err.println("^D");
+                            return;
+                        } else if (c == 3 && !ShellUtil.getBoolean(ConsoleSessionImpl.this, Session.IGNORE_INTERRUPTS)) {
+                            err.println("^C");
+                            reader.getCursorBuffer().clear();
+                            ConsoleSessionImpl.this.interrupt();
+                        }
+                        queue.put(c);
+                    } catch (Throwable t) {
+                        return;
+                    }
+                }
+            } finally {
+                eof = true;
+                try {
+                    queue.put(-1);
+                } catch (InterruptedException e) {
+                }
+            }
+        }
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/karaf/blob/e7d23bef/shell/core/src/main/java/org/apache/karaf/shell/impl/console/HeadlessSessionImpl.java
----------------------------------------------------------------------
diff --git a/shell/core/src/main/java/org/apache/karaf/shell/impl/console/HeadlessSessionImpl.java b/shell/core/src/main/java/org/apache/karaf/shell/impl/console/HeadlessSessionImpl.java
new file mode 100644
index 0000000..16e73ca
--- /dev/null
+++ b/shell/core/src/main/java/org/apache/karaf/shell/impl/console/HeadlessSessionImpl.java
@@ -0,0 +1,146 @@
+/*
+ * 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.apache.karaf.shell.impl.console;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.PrintStream;
+import java.util.List;
+import java.util.Properties;
+
+import org.apache.felix.service.command.CommandProcessor;
+import org.apache.felix.service.command.CommandSession;
+import org.apache.karaf.shell.api.console.Command;
+import org.apache.karaf.shell.api.console.History;
+import org.apache.karaf.shell.api.console.Registry;
+import org.apache.karaf.shell.api.console.Session;
+import org.apache.karaf.shell.api.console.SessionFactory;
+import org.apache.karaf.shell.api.console.Terminal;
+import org.apache.karaf.shell.support.ShellUtil;
+
+public class HeadlessSessionImpl implements Session {
+
+    final SessionFactory factory;
+    final CommandSession session;
+    final Registry registry;
+
+    public HeadlessSessionImpl(SessionFactory factory, CommandProcessor processor, InputStream in, PrintStream out, PrintStream err) {
+        // Factory
+        this.factory = factory;
+        // Registry
+        registry = new RegistryImpl(factory.getRegistry());
+        registry.register(factory);
+        registry.register(this);
+        registry.register(registry);
+        // Session
+        session = processor.createSession(in, out, err);
+        Properties sysProps = System.getProperties();
+        for (Object key : sysProps.keySet()) {
+            session.put(key.toString(), sysProps.get(key));
+        }
+        session.put(".session", this);
+        session.put(".commandSession", session);
+        session.put(Session.SCOPE, "shell:bundle:*");
+        session.put(Session.SUBSHELL, "");
+        session.put("USER", ShellUtil.getCurrentUserName());
+        session.put("APPLICATION", System.getProperty("karaf.name", "root"));
+    }
+
+    public CommandSession getSession() {
+        return session;
+    }
+
+    @Override
+    public Object execute(CharSequence commandline) throws Exception {
+        return session.execute(commandline);
+    }
+
+    @Override
+    public Object get(String name) {
+        return session.get(name);
+    }
+
+    @Override
+    public void put(String name, Object value) {
+        session.put(name, value);
+    }
+
+    @Override
+    public InputStream getKeyboard() {
+        return session.getKeyboard();
+    }
+
+    @Override
+    public PrintStream getConsole() {
+        return session.getConsole();
+    }
+
+    @Override
+    public String readLine(String prompt, Character mask) throws IOException {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public Terminal getTerminal() {
+        return null;
+    }
+
+    @Override
+    public History getHistory() {
+        return null;
+    }
+
+    @Override
+    public Registry getRegistry() {
+        return registry;
+    }
+
+    @Override
+    public SessionFactory getFactory() {
+        return factory;
+    }
+
+    @Override
+    public void run() {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public String resolveCommand(String name) {
+        // TODO: optimize
+        if (!name.contains(":")) {
+            String[] scopes = ((String) get(Session.SCOPE)).split(":");
+            List<Command> commands = registry.getCommands();
+            for (String scope : scopes) {
+                for (Command command : commands) {
+                    if ((Session.SCOPE_GLOBAL.equals(scope) || command.getScope().equals(scope)) && command.getName().equals(name)) {
+                        return command.getScope() + ":" + name;
+                    }
+                }
+            }
+        }
+        return name;
+    }
+
+    @Override
+    public void close() {
+        session.close();
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/karaf/blob/e7d23bef/shell/core/src/main/java/org/apache/karaf/shell/impl/console/HistoryWrapper.java
----------------------------------------------------------------------
diff --git a/shell/core/src/main/java/org/apache/karaf/shell/impl/console/HistoryWrapper.java b/shell/core/src/main/java/org/apache/karaf/shell/impl/console/HistoryWrapper.java
new file mode 100644
index 0000000..6f69991
--- /dev/null
+++ b/shell/core/src/main/java/org/apache/karaf/shell/impl/console/HistoryWrapper.java
@@ -0,0 +1,48 @@
+/*
+ * 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.apache.karaf.shell.impl.console;
+
+import org.apache.karaf.shell.api.console.History;
+
+public class HistoryWrapper implements History {
+
+    private final jline.console.history.History history;
+
+    public HistoryWrapper(jline.console.history.History history) {
+        this.history = history;
+    }
+
+    @Override
+    public void clear() {
+        history.clear();
+    }
+
+    public int first() {
+        return history.iterator().next().index() + 1;
+    }
+
+    public int last() {
+        return first() + history.size() - 1;
+    }
+
+    @Override
+    public CharSequence get(int index) {
+        return history.get(index - 1);
+    }
+}

http://git-wip-us.apache.org/repos/asf/karaf/blob/e7d23bef/shell/core/src/main/java/org/apache/karaf/shell/impl/console/JLineTerminal.java
----------------------------------------------------------------------
diff --git a/shell/core/src/main/java/org/apache/karaf/shell/impl/console/JLineTerminal.java b/shell/core/src/main/java/org/apache/karaf/shell/impl/console/JLineTerminal.java
new file mode 100644
index 0000000..d000885
--- /dev/null
+++ b/shell/core/src/main/java/org/apache/karaf/shell/impl/console/JLineTerminal.java
@@ -0,0 +1,62 @@
+/*
+ * 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.apache.karaf.shell.impl.console;
+
+import org.apache.karaf.shell.api.console.Terminal;
+
+/**
+* Created by gnodet on 27/02/14.
+*/
+public class JLineTerminal implements Terminal {
+
+    private final jline.Terminal terminal;
+
+    public JLineTerminal(jline.Terminal terminal) {
+        this.terminal = terminal;
+    }
+
+    public jline.Terminal getTerminal() {
+        return terminal;
+    }
+
+    @Override
+    public int getWidth() {
+        return terminal.getWidth();
+    }
+
+    @Override
+    public int getHeight() {
+        return terminal.getHeight();
+    }
+
+    @Override
+    public boolean isAnsiSupported() {
+        return terminal.isAnsiSupported();
+    }
+
+    @Override
+    public boolean isEchoEnabled() {
+        return terminal.isEchoEnabled();
+    }
+
+    @Override
+    public void setEchoEnabled(boolean enabled) {
+        terminal.setEchoEnabled(enabled);
+    }
+}

http://git-wip-us.apache.org/repos/asf/karaf/blob/e7d23bef/shell/core/src/main/java/org/apache/karaf/shell/impl/console/KarafFileHistory.java
----------------------------------------------------------------------
diff --git a/shell/core/src/main/java/org/apache/karaf/shell/impl/console/KarafFileHistory.java b/shell/core/src/main/java/org/apache/karaf/shell/impl/console/KarafFileHistory.java
new file mode 100644
index 0000000..aa4d2e4
--- /dev/null
+++ b/shell/core/src/main/java/org/apache/karaf/shell/impl/console/KarafFileHistory.java
@@ -0,0 +1,64 @@
+/*
+ * 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.apache.karaf.shell.impl.console;
+
+import java.io.File;
+import java.io.IOException;
+
+import jline.console.history.FileHistory;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Override the FileHistory impl to trap failures due to the
+ * user does not having write access to the history file.
+ */
+public final class KarafFileHistory extends FileHistory {
+
+	static final Logger LOGGER = LoggerFactory.getLogger(KarafFileHistory.class);
+	boolean failed = false;
+
+	public KarafFileHistory(File file) throws IOException {
+		super(file);
+	}
+
+	@Override
+	public void flush() throws IOException {
+	    if( !failed ) {
+	        try {
+	            super.flush();
+	        } catch (IOException e) {
+	            failed = true;
+	            LOGGER.debug("Could not write history file: "+ getFile(), e);
+	        }
+	    }
+	}
+
+	@Override
+	public void purge() throws IOException {
+	    if( !failed ) {
+	        try {
+	            super.purge();
+	        } catch (IOException e) {
+	            failed = true;
+	            LOGGER.debug("Could not delete history file: "+ getFile(), e);
+	        }
+	    }
+	}
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/karaf/blob/e7d23bef/shell/core/src/main/java/org/apache/karaf/shell/impl/console/KarafTerminal.java
----------------------------------------------------------------------
diff --git a/shell/core/src/main/java/org/apache/karaf/shell/impl/console/KarafTerminal.java b/shell/core/src/main/java/org/apache/karaf/shell/impl/console/KarafTerminal.java
new file mode 100644
index 0000000..0c8b0fe
--- /dev/null
+++ b/shell/core/src/main/java/org/apache/karaf/shell/impl/console/KarafTerminal.java
@@ -0,0 +1,57 @@
+/*
+ * 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.apache.karaf.shell.impl.console;
+
+import jline.TerminalSupport;
+import org.apache.karaf.shell.api.console.Terminal;
+
+public class KarafTerminal extends TerminalSupport {
+
+    private final Terminal terminal;
+
+    public KarafTerminal(Terminal terminal) {
+        super(true);
+        this.terminal = terminal;
+    }
+
+    @Override
+    public synchronized boolean isAnsiSupported() {
+        return terminal.isAnsiSupported();
+    }
+
+    @Override
+    public int getWidth() {
+        return terminal.getWidth();
+    }
+
+    @Override
+    public int getHeight() {
+        return terminal.getHeight();
+    }
+
+    @Override
+    public synchronized boolean isEchoEnabled() {
+        return terminal.isEchoEnabled();
+    }
+
+    @Override
+    public synchronized void setEchoEnabled(boolean enabled) {
+        terminal.setEchoEnabled(enabled);
+    }
+}

http://git-wip-us.apache.org/repos/asf/karaf/blob/e7d23bef/shell/core/src/main/java/org/apache/karaf/shell/impl/console/RegistryImpl.java
----------------------------------------------------------------------
diff --git a/shell/core/src/main/java/org/apache/karaf/shell/impl/console/RegistryImpl.java b/shell/core/src/main/java/org/apache/karaf/shell/impl/console/RegistryImpl.java
new file mode 100644
index 0000000..43ed27e
--- /dev/null
+++ b/shell/core/src/main/java/org/apache/karaf/shell/impl/console/RegistryImpl.java
@@ -0,0 +1,159 @@
+/*
+ * 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.apache.karaf.shell.impl.console;
+
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.Callable;
+
+import org.apache.karaf.shell.api.console.Command;
+import org.apache.karaf.shell.api.console.Registry;
+
+public class RegistryImpl implements Registry {
+
+    private final Registry parent;
+    private final Map<Object, Object> services = new LinkedHashMap<Object, Object>();
+
+    public RegistryImpl(Registry parent) {
+        this.parent = parent;
+    }
+
+    @Override
+    public List<Command> getCommands() {
+        return getServices(Command.class);
+    }
+
+    @Override
+    public <T> void register(Callable<T> factory, Class<T> clazz) {
+        synchronized (services) {
+            services.put(factory, new Factory<T>(clazz, factory));
+        }
+    }
+
+    @Override
+    public void register(Object service) {
+        synchronized (services) {
+            services.put(service, service);
+        }
+    }
+
+    @Override
+    public void unregister(Object service) {
+        synchronized (services) {
+            services.remove(service);
+        }
+    }
+
+    @Override
+    public <T> T getService(Class<T> clazz) {
+        synchronized (services) {
+            for (Object service : services.values()) {
+                if (service instanceof Factory) {
+                    if (clazz.isAssignableFrom(((Factory) service).clazz)) {
+                        if (isVisible(service)) {
+                            try {
+                                return clazz.cast(((Factory) service).callable.call());
+                            } catch (Exception e) {
+                                // TODO: log exception
+                            }
+                        }
+                    }
+                } else if (clazz.isInstance(service)) {
+                    if (isVisible(service)) {
+                        return clazz.cast(service);
+                    }
+                }
+            }
+        }
+        if (parent != null) {
+            return parent.getService(clazz);
+        }
+        return null;
+    }
+
+    @Override
+    public <T> List<T> getServices(Class<T> clazz) {
+        List<T> list = new ArrayList<T>();
+        synchronized (services) {
+            for (Object service : services.values()) {
+                if (service instanceof Factory) {
+                    if (clazz.isAssignableFrom(((Factory) service).clazz)) {
+                        if (isVisible(service)) {
+                            try {
+                                list.add(clazz.cast(((Factory) service).callable.call()));
+                            } catch (Exception e) {
+                                // TODO: log exception
+                            }
+                        }
+                    }
+                } else if (clazz.isInstance(service)) {
+                    if (isVisible(service)) {
+                        list.add(clazz.cast(service));
+                    }
+                }
+            }
+        }
+        if (parent != null) {
+            list.addAll(parent.getServices(clazz));
+        }
+        return list;
+    }
+
+    @Override
+    public boolean hasService(Class<?> clazz) {
+        synchronized (services) {
+            for (Object service : services.values()) {
+                if (service instanceof Factory) {
+                    if (clazz.isAssignableFrom(((Factory) service).clazz)) {
+                        if (isVisible(service)) {
+                            return true;
+                        }
+                    }
+                } else if (clazz.isInstance(service)) {
+                    if (isVisible(service)) {
+                        return true;
+                    }
+                }
+            }
+        }
+        if (parent != null) {
+            return parent.hasService(clazz);
+        }
+        return false;
+    }
+
+    protected boolean isVisible(Object service) {
+        return true;
+    }
+
+    static class Factory<T> {
+
+        final Class<T> clazz;
+        final Callable<T> callable;
+
+        Factory(Class<T> clazz, Callable<T> callable) {
+            this.clazz = clazz;
+            this.callable = callable;
+        }
+
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/karaf/blob/e7d23bef/shell/core/src/main/java/org/apache/karaf/shell/impl/console/SessionFactoryImpl.java
----------------------------------------------------------------------
diff --git a/shell/core/src/main/java/org/apache/karaf/shell/impl/console/SessionFactoryImpl.java b/shell/core/src/main/java/org/apache/karaf/shell/impl/console/SessionFactoryImpl.java
new file mode 100644
index 0000000..2f19499
--- /dev/null
+++ b/shell/core/src/main/java/org/apache/karaf/shell/impl/console/SessionFactoryImpl.java
@@ -0,0 +1,144 @@
+/*
+ * 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.apache.karaf.shell.impl.console;
+
+import java.io.InputStream;
+import java.io.PrintStream;
+import java.lang.management.ManagementFactory;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Properties;
+
+import org.apache.felix.gogo.runtime.CommandProcessorImpl;
+import org.apache.felix.service.command.CommandSession;
+import org.apache.felix.service.command.Function;
+import org.apache.felix.service.threadio.ThreadIO;
+import org.apache.karaf.shell.api.console.Command;
+import org.apache.karaf.shell.api.console.Registry;
+import org.apache.karaf.shell.api.console.Session;
+import org.apache.karaf.shell.api.console.SessionFactory;
+import org.apache.karaf.shell.api.console.Terminal;
+import org.apache.karaf.shell.impl.console.commands.ExitCommand;
+import org.apache.karaf.shell.impl.console.commands.SubShellCommand;
+import org.apache.karaf.shell.impl.console.commands.help.HelpCommand;
+import org.apache.karaf.shell.support.ShellUtil;
+
+public class SessionFactoryImpl extends RegistryImpl implements SessionFactory, Registry {
+
+    final CommandProcessorImpl commandProcessor;
+    final ThreadIO threadIO;
+    final List<Session> sessions = new ArrayList<Session>();
+    final Map<String, SubShellCommand> subshells = new HashMap<String, SubShellCommand>();
+    boolean closed;
+
+    public SessionFactoryImpl(ThreadIO threadIO) {
+        super(null);
+        this.threadIO = threadIO;
+        commandProcessor = new CommandProcessorImpl(threadIO);
+        register(new ExitCommand());
+        new HelpCommand(this);
+    }
+
+    public CommandProcessorImpl getCommandProcessor() {
+        return commandProcessor;
+    }
+
+    @Override
+    public Registry getRegistry() {
+        return this;
+    }
+
+    @Override
+    public void register(Object service) {
+        if (service instanceof Command) {
+            Command command = (Command) service;
+            String scope = command.getScope();
+            String name = command.getName();
+            if (!Session.SCOPE_GLOBAL.equals(scope)) {
+                if (!subshells.containsKey(scope)) {
+                    SubShellCommand subShell = new SubShellCommand(scope);
+                    subshells.put(scope, subShell);
+                    register(subShell);
+                }
+                subshells.get(scope).increment();
+            }
+            commandProcessor.addCommand(scope, wrap(command), name);
+        }
+        super.register(service);
+    }
+
+    protected Function wrap(Command command) {
+        return new CommandWrapper(command);
+    }
+
+    @Override
+    public void unregister(Object service) {
+        super.unregister(service);
+        if (service instanceof Command) {
+            Command command = (Command) service;
+            String scope = command.getScope();
+            String name = command.getName();
+            commandProcessor.removeCommand(scope, name);
+            if (!Session.SCOPE_GLOBAL.equals(scope)) {
+                if (subshells.get(scope).decrement() == 0) {
+                    SubShellCommand subShell = subshells.remove(scope);
+                    unregister(subShell);
+                }
+            }
+        }
+    }
+
+    @Override
+    public Session create(InputStream in, PrintStream out, PrintStream err, Terminal term, String encoding, Runnable closeCallback) {
+        synchronized (this) {
+            if (closed) {
+                throw new IllegalStateException("SessionFactory has been closed");
+            }
+            final Session session = new ConsoleSessionImpl(this, commandProcessor, threadIO, in, out, err, term, encoding, closeCallback);
+            sessions.add(session);
+            return session;
+        }
+    }
+
+    @Override
+    public Session create(InputStream in, PrintStream out, PrintStream err) {
+        synchronized (this) {
+            if (closed) {
+                throw new IllegalStateException("SessionFactory has been closed");
+            }
+            final Session session = new HeadlessSessionImpl(this, commandProcessor, in, out, err);
+            sessions.add(session);
+            return session;
+        }
+    }
+
+    public void stop() {
+        synchronized (this) {
+            closed = true;
+            for (Session session : sessions) {
+                session.close();
+            }
+            commandProcessor.stop();
+        }
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/karaf/blob/e7d23bef/shell/core/src/main/java/org/apache/karaf/shell/impl/console/TerminalFactory.java
----------------------------------------------------------------------
diff --git a/shell/core/src/main/java/org/apache/karaf/shell/impl/console/TerminalFactory.java b/shell/core/src/main/java/org/apache/karaf/shell/impl/console/TerminalFactory.java
new file mode 100644
index 0000000..bae3a25
--- /dev/null
+++ b/shell/core/src/main/java/org/apache/karaf/shell/impl/console/TerminalFactory.java
@@ -0,0 +1,47 @@
+/*
+ * 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.apache.karaf.shell.impl.console;
+
+import jline.NoInterruptUnixTerminal;
+import jline.Terminal;
+
+public class TerminalFactory {
+
+    private Terminal term;
+
+    public synchronized Terminal getTerminal() throws Exception {
+        if (term == null) {
+            init();
+        }
+        return term;
+    }
+
+    public void init() throws Exception {
+        jline.TerminalFactory.registerFlavor(jline.TerminalFactory.Flavor.UNIX, NoInterruptUnixTerminal.class);
+        term = jline.TerminalFactory.create();
+    }
+
+    public synchronized void destroy() throws Exception {
+        if (term != null) {
+            term.restore();
+            term = null;
+        }
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/karaf/blob/e7d23bef/shell/core/src/main/java/org/apache/karaf/shell/impl/console/commands/ExitCommand.java
----------------------------------------------------------------------
diff --git a/shell/core/src/main/java/org/apache/karaf/shell/impl/console/commands/ExitCommand.java b/shell/core/src/main/java/org/apache/karaf/shell/impl/console/commands/ExitCommand.java
new file mode 100644
index 0000000..147d403
--- /dev/null
+++ b/shell/core/src/main/java/org/apache/karaf/shell/impl/console/commands/ExitCommand.java
@@ -0,0 +1,51 @@
+/*
+ * 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.apache.karaf.shell.impl.console.commands;
+
+import org.apache.karaf.shell.api.console.Session;
+
+public class ExitCommand extends TopLevelCommand {
+
+    @Override
+    public String getName() {
+        return "exit";
+    }
+
+    @Override
+    public String getDescription() {
+        return "Exit from the current shell";
+    }
+
+    @Override
+    protected void doExecute(Session session) throws Exception {
+        // get the current sub-shell
+        String currentSubShell = (String) session.get(Session.SUBSHELL);
+        if (!currentSubShell.isEmpty()) {
+            if (currentSubShell.contains(":")) {
+                int index = currentSubShell.lastIndexOf(":");
+                session.put(Session.SUBSHELL, currentSubShell.substring(0, index));
+            } else {
+                session.put(Session.SUBSHELL, "");
+            }
+            String currentScope = (String) session.get(Session.SCOPE);
+            int index = currentScope.indexOf(":");
+            session.put(Session.SCOPE, currentScope.substring(index + 1));
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/karaf/blob/e7d23bef/shell/core/src/main/java/org/apache/karaf/shell/impl/console/commands/SubShellCommand.java
----------------------------------------------------------------------
diff --git a/shell/core/src/main/java/org/apache/karaf/shell/impl/console/commands/SubShellCommand.java b/shell/core/src/main/java/org/apache/karaf/shell/impl/console/commands/SubShellCommand.java
new file mode 100644
index 0000000..d2826f3
--- /dev/null
+++ b/shell/core/src/main/java/org/apache/karaf/shell/impl/console/commands/SubShellCommand.java
@@ -0,0 +1,58 @@
+/*
+ * 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.apache.karaf.shell.impl.console.commands;
+
+import java.util.concurrent.atomic.AtomicInteger;
+
+import org.apache.karaf.shell.api.console.Session;
+
+public class SubShellCommand extends TopLevelCommand {
+
+    private final String name;
+    private final AtomicInteger references = new AtomicInteger();
+
+    public SubShellCommand(String name) {
+        this.name = name;
+    }
+
+    public void increment() {
+        references.incrementAndGet();
+    }
+
+    public int decrement() {
+        return references.decrementAndGet();
+    }
+
+    @Override
+    public String getName() {
+        return name;
+    }
+
+    @Override
+    public String getDescription() {
+        return "Enter the subshell";
+    }
+
+    @Override
+    protected void doExecute(Session session) throws Exception {
+        session.put(Session.SUBSHELL, name);
+        session.put(Session.SCOPE, name + ":" + session.get(Session.SCOPE));
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/karaf/blob/e7d23bef/shell/core/src/main/java/org/apache/karaf/shell/impl/console/commands/TopLevelCommand.java
----------------------------------------------------------------------
diff --git a/shell/core/src/main/java/org/apache/karaf/shell/impl/console/commands/TopLevelCommand.java b/shell/core/src/main/java/org/apache/karaf/shell/impl/console/commands/TopLevelCommand.java
new file mode 100644
index 0000000..ad2c729
--- /dev/null
+++ b/shell/core/src/main/java/org/apache/karaf/shell/impl/console/commands/TopLevelCommand.java
@@ -0,0 +1,86 @@
+/*
+ * 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.apache.karaf.shell.impl.console.commands;
+
+import java.io.PrintStream;
+import java.util.List;
+
+import org.apache.karaf.shell.api.console.Command;
+import org.apache.karaf.shell.api.console.Completer;
+import org.apache.karaf.shell.api.console.Session;
+import org.apache.karaf.shell.support.CommandException;
+
+import static org.apache.karaf.shell.support.ansi.SimpleAnsi.COLOR_DEFAULT;
+import static org.apache.karaf.shell.support.ansi.SimpleAnsi.COLOR_RED;
+import static org.apache.karaf.shell.support.ansi.SimpleAnsi.INTENSITY_BOLD;
+import static org.apache.karaf.shell.support.ansi.SimpleAnsi.INTENSITY_NORMAL;
+
+public abstract class TopLevelCommand implements Command {
+
+    @Override
+    public String getScope() {
+        return Session.SCOPE_GLOBAL;
+    }
+
+    @Override
+    public Completer getCompleter(boolean scoped) {
+        return null;
+//        return new StringsCompleter(new String[] { getName() });
+    }
+
+    @Override
+    public Object execute(Session session, List<Object> arguments) throws Exception {
+        if (arguments.contains("--help")) {
+            printHelp(System.out);
+            return null;
+        }
+        if (!arguments.isEmpty()) {
+            String msg = COLOR_RED
+                    + "Error executing command "
+                    + INTENSITY_BOLD + getName() + INTENSITY_NORMAL
+                    + COLOR_DEFAULT + ": " + "too many arguments specified";
+            throw new CommandException(msg);
+        }
+        doExecute(session);
+        return null;
+    }
+
+    protected void printHelp(PrintStream out) {
+        out.println(INTENSITY_BOLD + "DESCRIPTION" + INTENSITY_NORMAL);
+        out.print("        ");
+        out.println(INTENSITY_BOLD + getName() + INTENSITY_NORMAL);
+        out.println();
+        out.print("\t");
+        out.println(getDescription());
+        out.println();
+        out.println(INTENSITY_BOLD + "SYNTAX" + INTENSITY_NORMAL);
+        out.print("        ");
+        out.println(getName() + " [options]");
+        out.println();
+        out.println(INTENSITY_BOLD + "OPTIONS" + INTENSITY_NORMAL);
+        out.print("        ");
+        out.println(INTENSITY_BOLD + "--help" + INTENSITY_NORMAL);
+        out.print("                ");
+        out.println("Display this help message");
+        out.println();
+    }
+
+    protected abstract void doExecute(Session session) throws Exception;
+
+}

http://git-wip-us.apache.org/repos/asf/karaf/blob/e7d23bef/shell/core/src/main/java/org/apache/karaf/shell/impl/console/commands/help/CommandListHelpProvider.java
----------------------------------------------------------------------
diff --git a/shell/core/src/main/java/org/apache/karaf/shell/impl/console/commands/help/CommandListHelpProvider.java b/shell/core/src/main/java/org/apache/karaf/shell/impl/console/commands/help/CommandListHelpProvider.java
new file mode 100644
index 0000000..9101520
--- /dev/null
+++ b/shell/core/src/main/java/org/apache/karaf/shell/impl/console/commands/help/CommandListHelpProvider.java
@@ -0,0 +1,117 @@
+/**
+ *
+ * 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.apache.karaf.shell.impl.console.commands.help;
+
+import java.io.ByteArrayOutputStream;
+import java.io.PrintStream;
+import java.util.List;
+import java.util.Map;
+import java.util.SortedMap;
+import java.util.TreeMap;
+
+import org.apache.karaf.shell.api.console.Command;
+import org.apache.karaf.shell.api.console.Session;
+import org.apache.karaf.shell.api.console.Terminal;
+import org.apache.karaf.shell.support.NameScoping;
+import org.apache.karaf.shell.support.ansi.SimpleAnsi;
+import org.apache.karaf.shell.support.table.Col;
+import org.apache.karaf.shell.support.table.ShellTable;
+
+public class CommandListHelpProvider implements HelpProvider {
+
+    public String getHelp(Session session, String path) {
+        if (path.indexOf('|') > 0) {
+            if (path.startsWith("command-list|")) {
+                path = path.substring("command-list|".length());
+            } else {
+                return null;
+            }
+        }
+        ByteArrayOutputStream baos = new ByteArrayOutputStream();
+        SortedMap<String, String> commands = getCommandDescriptions(session, path);
+        if (commands.isEmpty()) {
+            return null;
+        } else if (commands.size() == 1 && commands.containsKey(path)) {
+            return null;
+        } else {
+            printMethodList(session, new PrintStream(baos), commands);
+            return baos.toString();
+        }
+    }
+
+    private SortedMap<String, String> getCommandDescriptions(Session session, String path) {
+        // TODO: this is not really clean
+
+        List<Command> commands = session.getRegistry().getCommands();
+
+        String subshell = (String) session.get(Session.SUBSHELL);
+        String completionMode = (String) session.get(Session.COMPLETION_MODE);
+
+        SortedMap<String,String> descriptions = new TreeMap<String,String>();
+        for (Command command : commands) {
+
+            String name = command.getScope() + ":" + command.getName();
+
+            if (command != null && !name.startsWith(path)) {
+                continue;
+            }
+
+            if (completionMode != null && completionMode.equalsIgnoreCase(Session.COMPLETION_MODE_SUBSHELL)) {
+                // filter the help only for "global" commands
+                if (subshell == null || subshell.trim().isEmpty()) {
+                    if (!name.startsWith(Session.SCOPE_GLOBAL)) {
+                        continue;
+                    }
+                }
+            }
+
+            if (completionMode != null && (completionMode.equalsIgnoreCase(Session.COMPLETION_MODE_SUBSHELL)
+                                                || completionMode.equalsIgnoreCase(Session.COMPLETION_MODE_FIRST))) {
+                // filter the help only for commands local to the subshell
+                if (!name.startsWith(subshell)) {
+                    continue;
+                }
+            }
+
+            String description = command.getDescription();
+            if (name.startsWith("*:")) {
+                name = name.substring(2);
+            }
+            if (subshell != null && !subshell.trim().isEmpty() && name.startsWith(subshell + ":")) {
+                name = name.substring(subshell.length() + 1);
+            }
+            descriptions.put(name, description);
+        }
+        return descriptions;
+    }
+
+    protected void printMethodList(Session session, PrintStream out, SortedMap<String, String> commands) {
+        Terminal term = session.getTerminal();
+        int termWidth = term != null ? term.getWidth() : 80;
+        out.println(SimpleAnsi.INTENSITY_BOLD + "COMMANDS" + SimpleAnsi.INTENSITY_NORMAL);
+        ShellTable table = new ShellTable().noHeaders().separator(" ").size(termWidth);
+        table.column(new Col("Command").maxSize(35));
+        table.column(new Col("Description"));
+        for (Map.Entry<String,String> entry : commands.entrySet()) {
+            String key = NameScoping.getCommandNameWithoutGlobalPrefix(session, entry.getKey());
+            table.addRow().addContent(key, entry.getValue());
+        }
+        table.print(out, true);
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/karaf/blob/e7d23bef/shell/core/src/main/java/org/apache/karaf/shell/impl/console/commands/help/HelpCommand.java
----------------------------------------------------------------------
diff --git a/shell/core/src/main/java/org/apache/karaf/shell/impl/console/commands/help/HelpCommand.java b/shell/core/src/main/java/org/apache/karaf/shell/impl/console/commands/help/HelpCommand.java
new file mode 100644
index 0000000..086b83b
--- /dev/null
+++ b/shell/core/src/main/java/org/apache/karaf/shell/impl/console/commands/help/HelpCommand.java
@@ -0,0 +1,225 @@
+/*
+ * 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.apache.karaf.shell.impl.console.commands.help;
+
+import java.io.PrintStream;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.karaf.util.properties.InterpolationHelper;
+import org.apache.karaf.shell.api.console.Command;
+import org.apache.karaf.shell.api.console.CommandLine;
+import org.apache.karaf.shell.api.console.Completer;
+import org.apache.karaf.shell.api.console.Registry;
+import org.apache.karaf.shell.api.console.Session;
+import org.apache.karaf.shell.api.console.SessionFactory;
+import org.apache.karaf.shell.support.CommandException;
+import org.apache.karaf.shell.support.completers.StringsCompleter;
+
+import static org.apache.karaf.shell.support.ansi.SimpleAnsi.COLOR_DEFAULT;
+import static org.apache.karaf.shell.support.ansi.SimpleAnsi.COLOR_RED;
+import static org.apache.karaf.shell.support.ansi.SimpleAnsi.INTENSITY_BOLD;
+import static org.apache.karaf.shell.support.ansi.SimpleAnsi.INTENSITY_NORMAL;
+
+public class HelpCommand implements Command {
+
+    public HelpCommand(SessionFactory factory) {
+        Registry registry = factory.getRegistry();
+        registry.register(this);
+        registry.register(new SimpleHelpProvider());
+        registry.register(new CommandListHelpProvider());
+        registry.register(new SingleCommandHelpProvider());
+    }
+
+    @Override
+    public String getScope() {
+        return Session.SCOPE_GLOBAL;
+    }
+
+    @Override
+    public String getName() {
+        return "help";
+    }
+
+    @Override
+    public String getDescription() {
+        return "Displays this help or help about a command";
+    }
+
+    @Override
+    public Object execute(Session session, List<Object> arguments) throws Exception {
+        if (arguments.contains("--help")) {
+            printHelp(System.out);
+            return null;
+        }
+        if (arguments.size() > 1) {
+            String msg = COLOR_RED
+                    + "Error executing command "
+                    + INTENSITY_BOLD + getName() + INTENSITY_NORMAL
+                    + COLOR_DEFAULT + ": " + "too many arguments specified";
+            throw new CommandException(msg);
+        }
+        String path = arguments.isEmpty() ? null : arguments.get(0) == null ? null : arguments.get(0).toString();
+        String help = getHelp(session, path);
+        if (help != null) {
+            System.out.println(help);
+        }
+        return null;
+    }
+
+    @Override
+    public Completer getCompleter(final boolean scoped) {
+        return new Completer() {
+            @Override
+            public int complete(Session session, CommandLine commandLine, List<String> candidates) {
+                String[] args = commandLine.getArguments();
+                int argIndex = commandLine.getCursorArgumentIndex();
+                StringsCompleter completer = new StringsCompleter(Collections.singletonList(getName()));
+                if (argIndex == 0) {
+                    return completer.complete(session, new ArgumentCommandLine(args[argIndex], commandLine.getArgumentPosition()), candidates);
+                } else if (!verifyCompleter(session, completer, args[0])) {
+                    return -1;
+                }
+                // TODO: use CommandNamesCompleter and better completion wrt parsing etc...
+                completer = new StringsCompleter();
+                for (Command command : session.getRegistry().getCommands()) {
+                    if (!Session.SCOPE_GLOBAL.equals(command.getScope())) {
+                        completer.getStrings().add(command.getScope() + ":" + command.getName());
+                    }
+                    completer.getStrings().add(command.getName());
+                }
+                completer.getStrings().add("--help");
+                if (argIndex == 1) {
+                    int res;
+                    if (argIndex < args.length) {
+                        res = completer.complete(session, new ArgumentCommandLine(args[argIndex], commandLine.getArgumentPosition()), candidates);
+                    } else {
+                        res = completer.complete(session, new ArgumentCommandLine("", 0), candidates);
+                    }
+                    return res + (commandLine.getBufferPosition() - commandLine.getArgumentPosition());
+                } else if (!verifyCompleter(session, completer, args[1])) {
+                    return -1;
+                }
+                return -1;
+            }
+            protected boolean verifyCompleter(Session session, Completer completer, String argument) {
+                List<String> candidates = new ArrayList<String>();
+                return completer.complete(session, new ArgumentCommandLine(argument, argument.length()), candidates) != -1 && !candidates.isEmpty();
+            }
+        };
+    }
+
+    protected void printHelp(PrintStream out) {
+        out.println(INTENSITY_BOLD + "DESCRIPTION" + INTENSITY_NORMAL);
+        out.print("        ");
+        out.println(INTENSITY_BOLD + getName() + INTENSITY_NORMAL);
+        out.println();
+        out.print("\t");
+        out.println(getDescription());
+        out.println();
+        out.println(INTENSITY_BOLD + "SYNTAX" + INTENSITY_NORMAL);
+        out.print("        ");
+        out.println(getName() + " [options] [command]");
+        out.println();
+        out.println(INTENSITY_BOLD + "ARGUMENTS" + INTENSITY_NORMAL);
+        out.print("        ");
+        out.println(INTENSITY_BOLD + "command" + INTENSITY_NORMAL);
+        out.print("                ");
+        out.println("Command to display help for");
+        out.println();
+        out.println(INTENSITY_BOLD + "OPTIONS" + INTENSITY_NORMAL);
+        out.print("        ");
+        out.println(INTENSITY_BOLD + "--help" + INTENSITY_NORMAL);
+        out.print("                ");
+        out.println("Display this help message");
+        out.println();
+    }
+
+    public String getHelp(final Session session, String path) {
+        if (path == null) {
+            path = "%root%";
+        }
+        Map<String,String> props = new HashMap<String,String>();
+        props.put("data", "${" + path + "}");
+        final List<HelpProvider> providers = session.getRegistry().getServices(HelpProvider.class);
+        InterpolationHelper.performSubstitution(props, new InterpolationHelper.SubstitutionCallback() {
+            public String getValue(final String key) {
+                for (HelpProvider hp : providers) {
+                    String result = hp.getHelp(session, key);
+                    if (result != null) {
+                        return removeNewLine(result);
+                    }
+                }
+                return null;
+            }
+        });
+        return props.get("data");
+    }
+
+    private String removeNewLine(String help) {
+        if (help != null && help.endsWith("\n")) {
+            help = help.substring(0, help.length()  -1);
+        }
+        return help;
+    }
+
+    static class ArgumentCommandLine implements CommandLine {
+        private final String argument;
+        private final int position;
+
+        ArgumentCommandLine(String argument, int position) {
+            this.argument = argument;
+            this.position = position;
+        }
+
+        @Override
+        public int getCursorArgumentIndex() {
+            return 0;
+        }
+
+        @Override
+        public String getCursorArgument() {
+            return argument;
+        }
+
+        @Override
+        public int getArgumentPosition() {
+            return position;
+        }
+
+        @Override
+        public String[] getArguments() {
+            return new String[] { argument };
+        }
+
+        @Override
+        public int getBufferPosition() {
+            return position;
+        }
+
+        @Override
+        public String getBuffer() {
+            return argument;
+        }
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/karaf/blob/e7d23bef/shell/core/src/main/java/org/apache/karaf/shell/impl/console/commands/help/HelpProvider.java
----------------------------------------------------------------------
diff --git a/shell/core/src/main/java/org/apache/karaf/shell/impl/console/commands/help/HelpProvider.java b/shell/core/src/main/java/org/apache/karaf/shell/impl/console/commands/help/HelpProvider.java
new file mode 100644
index 0000000..ad5e215
--- /dev/null
+++ b/shell/core/src/main/java/org/apache/karaf/shell/impl/console/commands/help/HelpProvider.java
@@ -0,0 +1,27 @@
+/*
+ * 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.apache.karaf.shell.impl.console.commands.help;
+
+import org.apache.karaf.shell.api.console.Session;
+
+public interface HelpProvider {
+
+    String getHelp(Session session, String path);
+
+}

http://git-wip-us.apache.org/repos/asf/karaf/blob/e7d23bef/shell/core/src/main/java/org/apache/karaf/shell/impl/console/commands/help/SimpleHelpProvider.java
----------------------------------------------------------------------
diff --git a/shell/core/src/main/java/org/apache/karaf/shell/impl/console/commands/help/SimpleHelpProvider.java b/shell/core/src/main/java/org/apache/karaf/shell/impl/console/commands/help/SimpleHelpProvider.java
new file mode 100644
index 0000000..90ad506
--- /dev/null
+++ b/shell/core/src/main/java/org/apache/karaf/shell/impl/console/commands/help/SimpleHelpProvider.java
@@ -0,0 +1,47 @@
+/**
+ *
+ * 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.apache.karaf.shell.impl.console.commands.help;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import org.apache.karaf.shell.api.console.Session;
+
+
+public class SimpleHelpProvider implements HelpProvider {
+
+    private final Map<String, String> help;
+
+    public SimpleHelpProvider() {
+        help = new HashMap<String, String>();
+        help.put("%root%", "${command-list|}");
+        help.put("all", "${command-list|}");
+    }
+
+    public String getHelp(Session session, String path) {
+        if (path.indexOf('|') > 0) {
+            if (path.startsWith("simple|")) {
+                path = path.substring("simple|".length());
+            } else {
+                return null;
+            }
+        }
+        return help.get(path);
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/karaf/blob/e7d23bef/shell/core/src/main/java/org/apache/karaf/shell/impl/console/commands/help/SingleCommandHelpProvider.java
----------------------------------------------------------------------
diff --git a/shell/core/src/main/java/org/apache/karaf/shell/impl/console/commands/help/SingleCommandHelpProvider.java b/shell/core/src/main/java/org/apache/karaf/shell/impl/console/commands/help/SingleCommandHelpProvider.java
new file mode 100644
index 0000000..dafab98
--- /dev/null
+++ b/shell/core/src/main/java/org/apache/karaf/shell/impl/console/commands/help/SingleCommandHelpProvider.java
@@ -0,0 +1,54 @@
+/**
+ *
+ * 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.apache.karaf.shell.impl.console.commands.help;
+
+import java.io.ByteArrayOutputStream;
+import java.io.PrintStream;
+
+import org.apache.karaf.shell.api.console.Session;
+
+public class SingleCommandHelpProvider implements HelpProvider {
+
+    public String getHelp(Session session, String path) {
+        if (path.indexOf('|') > 0) {
+            if (path.startsWith("command|")) {
+                path = path.substring("command|".length());
+            } else {
+                return null;
+            }
+        }
+
+        String resolved = session.resolveCommand(path);
+
+
+        ByteArrayOutputStream baos = new ByteArrayOutputStream();
+        PrintStream ps = new PrintStream(baos, true);
+        Session s = session.getFactory().create(null, ps, ps);
+        s.put(Session.SCOPE, session.get(Session.SCOPE));
+        s.put(Session.SUBSHELL, session.get(Session.SUBSHELL));
+        try {
+            s.execute(path + " --help");
+        } catch (Throwable t) {
+            return null;
+        } finally {
+            s.close();
+        }
+        return baos.toString();
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/karaf/blob/e7d23bef/shell/core/src/main/java/org/apache/karaf/shell/impl/console/osgi/Activator.java
----------------------------------------------------------------------
diff --git a/shell/core/src/main/java/org/apache/karaf/shell/impl/console/osgi/Activator.java b/shell/core/src/main/java/org/apache/karaf/shell/impl/console/osgi/Activator.java
new file mode 100644
index 0000000..921dca3
--- /dev/null
+++ b/shell/core/src/main/java/org/apache/karaf/shell/impl/console/osgi/Activator.java
@@ -0,0 +1,78 @@
+/*
+ * 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.apache.karaf.shell.impl.console.osgi;
+
+import org.apache.felix.gogo.runtime.threadio.ThreadIOImpl;
+import org.apache.karaf.shell.api.console.SessionFactory;
+import org.apache.karaf.shell.impl.action.osgi.CommandExtender;
+import org.apache.karaf.shell.impl.action.command.ManagerImpl;
+import org.apache.karaf.shell.impl.console.SessionFactoryImpl;
+import org.apache.karaf.shell.impl.console.TerminalFactory;
+import org.apache.karaf.shell.impl.console.osgi.secured.SecuredSessionFactoryImpl;
+import org.osgi.framework.BundleActivator;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.ServiceRegistration;
+
+public class Activator implements BundleActivator {
+
+    private static final String START_CONSOLE = "karaf.startLocalConsole";
+
+    private ThreadIOImpl threadIO;
+
+    private SessionFactoryImpl sessionFactory;
+    private ServiceRegistration sessionFactoryRegistration;
+
+    private CommandExtender actionExtender;
+
+    private TerminalFactory terminalFactory;
+    private LocalConsoleManager localConsoleManager;
+
+    @Override
+    public void start(BundleContext context) throws Exception {
+        threadIO = new ThreadIOImpl();
+        threadIO.start();
+
+        sessionFactory = new SecuredSessionFactoryImpl(context, threadIO);
+        sessionFactory.getCommandProcessor().addConverter(new Converters(context));
+        sessionFactory.getCommandProcessor().addConstant(".context", context.getBundle(0).getBundleContext());
+
+        sessionFactory.register(new ManagerImpl(sessionFactory, sessionFactory));
+
+        sessionFactoryRegistration = context.registerService(SessionFactory.class.getName(), sessionFactory, null);
+
+        actionExtender = new CommandExtender(sessionFactory);
+        actionExtender.start(context);
+
+        if (Boolean.parseBoolean(context.getProperty(START_CONSOLE))) {
+            terminalFactory = new TerminalFactory();
+            localConsoleManager = new LocalConsoleManager(context, terminalFactory, sessionFactory);
+            localConsoleManager.start();
+        }
+    }
+
+    @Override
+    public void stop(BundleContext context) throws Exception {
+        sessionFactoryRegistration.unregister();
+        sessionFactory.stop();
+        localConsoleManager.stop();
+        actionExtender.stop(context);
+        threadIO.stop();
+        terminalFactory.destroy();
+    }
+}