[maven-release-plugin] copy for tag org.apache.felix.gogo.jline-1.1.2

git-svn-id: https://svn.apache.org/repos/asf/felix/releases/org.apache.felix.gogo.jline-1.1.2@1850304 13f79535-47bb-0310-9956-ffa450edef68
diff --git a/jline/DEPENDENCIES b/jline/DEPENDENCIES
new file mode 100644
index 0000000..664b8ac
--- /dev/null
+++ b/jline/DEPENDENCIES
@@ -0,0 +1,20 @@
+Apache Felix Gogo Shell
+Copyright 2011 The Apache Software Foundation
+
+This software was developed at the Apache Software Foundation
+(http://www.apache.org) and may have dependencies on other
+Apache software licensed under Apache License 2.0.
+
+I. Included Third-Party Software
+
+None.
+
+II. Used Third-Party Software
+
+This product uses software developed at
+The OSGi Alliance (http://www.osgi.org/).
+Copyright (c) OSGi Alliance (2000, 2009).
+Licensed under the Apache License 2.0.
+
+III. License Summary
+- Apache License 2.0
diff --git a/jline/LICENSE b/jline/LICENSE
new file mode 100644
index 0000000..d645695
--- /dev/null
+++ b/jline/LICENSE
@@ -0,0 +1,202 @@
+
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "[]"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright [yyyy] [name of copyright owner]
+
+   Licensed 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.
diff --git a/jline/NOTICE b/jline/NOTICE
new file mode 100644
index 0000000..4007bd3
--- /dev/null
+++ b/jline/NOTICE
@@ -0,0 +1,6 @@
+Apache Felix Gogo Shell
+Copyright 2011 The Apache Software Foundation
+
+This product includes software developed at
+The Apache Software Foundation (http://www.apache.org/).
+Licensed under the Apache License 2.0.
diff --git a/jline/doc/changelog.txt b/jline/doc/changelog.txt
new file mode 100644
index 0000000..7aa4756
--- /dev/null
+++ b/jline/doc/changelog.txt
@@ -0,0 +1,154 @@
+Changes from 1.1.0 to 1.1.2
+----------------------------
+Improvement
+    [FELIX-5970] - Add requirement & capabilities model so gogo can be resolved
+    [FELIX-5999] - cleanup compiler warnings
+    [FELIX-6003] - Add some resolver checks to make sure @RequireGogo annotation works for both jline and shell
+    [FELIX-6007] - create a gogo bom
+
+Changes from 1.0.12 to 1.1.0
+----------------------------
+New Feature
+    [FELIX-5833] - Support for completion of quoted arguments
+    [FELIX-5834] - Upgrade to JLine 3.7.0
+    [FELIX-5835] - Upgrade to JDK 8
+    [FELIX-5836] - Upgrade to OSGi r6
+    [FELIX-5837] - [gogo][jline] Improve styling support
+
+Improvement
+    [FELIX-5857] - Provide a context classloader on the session to help with class loading
+    [FELIX-5869] - [goto][jline] Weird error if the script contains unicode characters
+
+Changes from 1.0.10 to 1.0.12
+-----------------------------
+Improvement
+    [FELIX-5822] - [gogo][jline] Improve syntax of procedural functions
+
+Changes from 1.0.8 to 1.0.10
+----------------------------
+Bug
+    [FELIX-5635] - [gogo][jline] The "cd" command should not attempt to complete multiple directories
+    [FELIX-5714] - ArrayIndexOutOfBoundsException running history | grep
+
+Improvement
+    [FELIX-5651] - Disable Log history in Gogo console
+    [FELIX-5705] - Provide completion for SCR commands
+
+Changes from 1.0.6 to 1.0.8
+---------------------------
+Bug
+    [FELIX-5687] - Do not use the threaded streams when passing the input stream to a command
+
+Changes from 1.0.4 to 1.0.6
+---------------------------
+Bug
+    [FELIX-5600] - System streams are not correctly set when running a new shell
+    [FELIX-5629] - [gogo][jline] When a job is in the foreground, the shell should wait for its completion
+    [FELIX-5631] - [gogo][runtime] The parser indicates wrong repair string when parsing heredocs
+
+Improvement
+    [FELIX-5594] - [gogo][jline] Improve color support for ls/grep and syntax highlighting
+    [FELIX-5596] - Allow to configure the colors for the gogo grep command
+    [FELIX-5598] - [gogo][jline] Support the JLine ttop function if available
+
+Changes from 1.0.2 to 1.0.4
+---------------------------
+Bug
+    [FELIX-5463] - [gogo][jline] The Main class should add a default Function->FunctionInterface converter
+    [FELIX-5498] - [gogo][jline] The shell should display exception thrown from commands
+    [FELIX-5584] - [gogo][jline] Remove dependency on LineReaderImpl
+
+Changes from 1.0.0 to 1.0.2
+---------------------------
+Bug
+    [FELIX-5077] - Gogo shell prints out nasty error on shutdown
+    [FELIX-5388] - Strange Prompt characters in the eclipse console
+    [FELIX-5442] - [gogo][jline] The gosh_script can not resolve the motd file inside the jar
+    [FELIX-5447] - [gogo][jline] Command results should be printed by default
+
+Changes from 0.10.0 to 1.0.0
+----------------------------
+** Bug
+    * [FELIX-5342] - Division by zero in new gogo for jline3
+
+** New Feature
+    * [FELIX-5272] - New gogo features
+
+** Task
+    * [FELIX-5378] - [gogo] Upgrade packages and bundle to 1.0.0
+
+Changes from 0.8.0 to 0.10.0
+----------------------------
+
+** Improvement
+    * Added gosh_profile work around for issue wit.3 API to 1.0.0
+      ambiguity.
+      ** Bug
+    * [FELIX-5342] - Division by zero in new gogo for jline3
+
+** New Feature
+    * [FELIX-5272] - New gogo features
+
+** Task
+    * [FELIX-5378] - [gogo] Upgrade packages and bundle to 1.
+
+Changes from 0.6.1 to 0.8.0
+---------------------------
+
+** Bug
+    * [FELIX-2651] - [Gogo] MOTD formatting is broken under Windows
+
+** Improvement
+    * [FELIX-2661] - [Gogo] It should be easier to start Gogo shell
+      non-interactively
+
+** New Feature
+    * [FELIX-2767] - gogo telnet IP address
+
+Changes from 0.6.0 to 0.6.1
+---------------------------
+
+** Bug
+    * [FELIX-2446] - [Gogo] The bundle context command is not used with a
+      scope in gosh_profile
+    * [FELIX-2477] - [gogo] shell procedural commands don't inherit closure
+      arguments
+
+** Improvement
+    * [FELIX-2445] - [Gogo] Default gosh_profile should be updated to use
+      system bundle to load java.lang.System
+    * [FELIX-2543] - [Gogo] Should avoid using System.getProperty() to get
+      configuration properties
+
+Gogo Shell 0.6.0
+----------------
+
+** Bug
+    * [FELIX-1473] - [gogo] The syntax does not provide a way to call methods
+      on a string
+    * [FELIX-1474] - [gogo] result of commands is implicitly written to pipe
+    * [FELIX-1493] - [gogo] automatic expansion of $args in Closure stops
+      direct access to $args list
+    * [FELIX-2337] - [gogo] no way to access array[] elements produced by
+      assignment
+    * [FELIX-2375] - [gogo] when supplied args can't be coerced, the error
+      message prints the arg values, rather than their types
+    * [FELIX-2380] - [gogo] lock contention in piped writer when reader
+      doesn't read all input
+
+** Improvement
+    * [FELIX-1487] - Support for commands on multiple lines
+    * [FELIX-2328] - [gogo] tidy-up runtime to remove optional code etc
+    * [FELIX-2339] - [gogo] add support for running scripts
+    * [FELIX-2342] - [gogo] remove old felix command adaptor
+
+** New Feature
+    * [FELIX-2363] - [Gogo] Add annotations for creating commands with
+      optional and out-of-order arguments
+
+** Task
+    * [FELIX-1670] - [gogo] launcher bundle not required
+    * [FELIX-1889] - Gogo should depend on the official OSGi jars
+    * [FELIX-2334] - [Gogo] Use org.apache.felix as Maven groupId
+    * [FELIX-2367] - [Gogo] Use org.apache.felix namespace to avoid any
+      perceived legal issues
diff --git a/jline/pom.xml b/jline/pom.xml
new file mode 100644
index 0000000..5093d6a
--- /dev/null
+++ b/jline/pom.xml
@@ -0,0 +1,103 @@
+<?xml version="1.0"?>
+<!--
+ 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.
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+    <parent>
+        <groupId>org.apache.felix</groupId>
+        <artifactId>gogo-parent</artifactId>
+        <version>5</version>
+        <relativePath>../gogo-parent/pom.xml</relativePath>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+    <packaging>bundle</packaging>
+    <name>Apache Felix Gogo JLine Shell</name>
+    <artifactId>org.apache.felix.gogo.jline</artifactId>
+    <version>1.1.2</version>
+    <url>http://felix.apache.org/</url>
+    <scm>
+        <connection>scm:svn:https://svn.apache.org/repos/asf/felix/releases/org.apache.felix.gogo.jline-1.1.2</connection>
+        <developerConnection>scm:svn:https://svn.apache.org/repos/asf/felix/releases/org.apache.felix.gogo.jline-1.1.2</developerConnection>
+        <url>https://svn.apache.org/repos/asf/felix/releases/org.apache.felix.gogo.jline-1.1.2</url>
+    </scm>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.osgi</groupId>
+            <artifactId>osgi.annotation</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.osgi</groupId>
+            <artifactId>osgi.core</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.felix</groupId>
+            <artifactId>org.apache.felix.gogo.runtime</artifactId>
+            <version>1.1.0</version>
+        </dependency>
+        <dependency>
+            <groupId>org.jline</groupId>
+            <artifactId>jline</artifactId>
+            <version>3.7.0</version>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.sshd</groupId>
+            <artifactId>sshd-core</artifactId>
+            <version>1.2.0</version>
+            <optional>true</optional>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>junit</groupId>
+            <artifactId>junit</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.mockito</groupId>
+            <artifactId>mockito-core</artifactId>
+        </dependency>
+    </dependencies>
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.apache.felix</groupId>
+                <artifactId>maven-bundle-plugin</artifactId>
+                <configuration>
+                    <instructions>
+                        <Export-Package>
+                            org.apache.felix.gogo.jline
+                        </Export-Package>
+                        <Import-Package>
+                            !org.apache.felix.gogo.runtime.threadio,
+                            org.jline*;version="[3.0,4)",
+                            *
+                        </Import-Package>
+                    </instructions>
+                </configuration>
+            </plugin>
+            <plugin>
+                <groupId>org.apache.rat</groupId>
+                <artifactId>apache-rat-plugin</artifactId>
+                <configuration>
+                    <excludes>
+                        <param>src/main/resources/motd</param>
+                    </excludes>
+                </configuration>
+            </plugin>
+        </plugins>
+    </build>
+</project>
diff --git a/jline/src/main/java/org/apache/felix/gogo/jline/Activator.java b/jline/src/main/java/org/apache/felix/gogo/jline/Activator.java
new file mode 100644
index 0000000..62f86d9
--- /dev/null
+++ b/jline/src/main/java/org/apache/felix/gogo/jline/Activator.java
@@ -0,0 +1,203 @@
+/*
+ * 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.felix.gogo.jline;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Dictionary;
+import java.util.HashSet;
+import java.util.Hashtable;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import org.apache.felix.gogo.jline.Shell.Context;
+import org.apache.felix.gogo.jline.SingleServiceTracker.SingleServiceListener;
+import org.apache.felix.gogo.runtime.Token;
+import org.apache.felix.gogo.runtime.Tokenizer;
+import org.apache.felix.service.command.CommandProcessor;
+import org.apache.felix.service.command.CommandSession;
+import org.apache.felix.service.command.Converter;
+import org.jline.terminal.Terminal;
+import org.jline.terminal.TerminalBuilder;
+import org.osgi.annotation.bundle.Header;
+import org.osgi.framework.BundleActivator;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.Constants;
+import org.osgi.framework.ServiceRegistration;
+
+@Header(name = Constants.BUNDLE_ACTIVATOR, value = "${@class}")
+public class Activator implements BundleActivator, SingleServiceListener {
+    private final Set<ServiceRegistration<?>> regs = new HashSet<>();
+    private BundleContext context;
+    private SingleServiceTracker<CommandProcessor> commandProcessorTracker;
+
+    private Runnable closer;
+
+    public Activator() {
+    }
+
+    public void start(BundleContext context) throws Exception {
+        this.context = context;
+        this.commandProcessorTracker = new SingleServiceTracker<>(context, CommandProcessor.class, this);
+        this.commandProcessorTracker.open();
+    }
+
+    public void stop(BundleContext context) {
+        Iterator<ServiceRegistration<?>> iterator = regs.iterator();
+        while (iterator.hasNext()) {
+            ServiceRegistration<?> reg = iterator.next();
+            reg.unregister();
+            iterator.remove();
+        }
+        this.commandProcessorTracker.close();
+    }
+
+    @Override
+    public void serviceFound() {
+        try {
+            closer = startShell(context, commandProcessorTracker.getService());
+        } catch (Exception e) {
+            // Ignore
+        }
+    }
+
+    @Override
+    public void serviceLost() {
+        stopShell();
+    }
+
+    @Override
+    public void serviceReplaced() {
+        serviceLost();
+        serviceFound();
+    }
+
+    private Runnable startShell(BundleContext context, CommandProcessor processor) throws Exception {
+        Dictionary<String, Object> dict = new Hashtable<>();
+        dict.put(CommandProcessor.COMMAND_SCOPE, "gogo");
+
+        // register converters
+        regs.add(context.registerService(Converter.class.getName(), new Converters(context.getBundle(0).getBundleContext()), null));
+
+        // register commands
+
+        dict.put(CommandProcessor.COMMAND_FUNCTION, Builtin.functions);
+        regs.add(context.registerService(Builtin.class.getName(), new Builtin(), dict));
+
+        dict.put(CommandProcessor.COMMAND_FUNCTION, Procedural.functions);
+        regs.add(context.registerService(Procedural.class.getName(), new Procedural(), dict));
+
+        dict.put(CommandProcessor.COMMAND_FUNCTION, Posix.functions);
+        regs.add(context.registerService(Posix.class.getName(), new Posix(processor), dict));
+
+        Shell shell = new Shell(new ShellContext(), processor);
+        dict.put(CommandProcessor.COMMAND_FUNCTION, Shell.functions);
+        regs.add(context.registerService(Shell.class.getName(), shell, dict));
+
+        Terminal terminal = TerminalBuilder.builder()
+                .name("gogo")
+                .system(true)
+                .nativeSignals(true)
+                .signalHandler(Terminal.SignalHandler.SIG_IGN)
+                .build();
+        CommandSession session = processor.createSession(terminal.input(), terminal.output(), terminal.output());
+        AtomicBoolean closing = new AtomicBoolean();
+
+        Thread thread = new Thread(() -> {
+            String errorMessage = "gogo: unable to create console";
+            try {
+                session.put(Shell.VAR_TERMINAL, terminal);
+                try {
+                    List<String> args = new ArrayList<>();
+                    args.add("--login");
+                    String argstr = shell.getContext().getProperty("gosh.args");
+                    if (argstr != null) {
+                        Tokenizer tokenizer = new Tokenizer(argstr);
+                        Token token;
+                        while ((token = tokenizer.next()) != null) {
+                            args.add(token.toString());
+                        }
+                    }
+                    shell.gosh(session, args.toArray(new String[args.size()]));
+                } catch (Throwable e) {
+                    Object loc = session.get(".location");
+                    if (null == loc || !loc.toString().contains(":")) {
+                        loc = "gogo";
+                    }
+                    errorMessage = loc.toString();
+                    throw e;
+                }
+            } catch (Throwable e) {
+                if (!closing.get()) {
+                    System.err.println(errorMessage + e.getClass().getSimpleName() + ": " + e.getMessage());
+                    e.printStackTrace();
+                }
+            }
+        }, "Gogo shell");
+        // start shell on a separate thread...
+        thread.start();
+
+        return () -> {
+            closing.set(true);
+            shell.stop();
+            try {
+                terminal.close();
+            } catch (IOException e) {
+                // Ignore
+            }
+            try {
+                long t0 = System.currentTimeMillis();
+                while (thread.isAlive()) {
+                    thread.interrupt();
+                    thread.join(10);
+                    if (System.currentTimeMillis() - t0 > 5000) {
+                        System.err.println("!!! FAILED TO STOP EXECUTOR !!!");
+                        break;
+                    }
+                }
+            } catch (InterruptedException e) {
+                // Restore administration...
+                Thread.currentThread().interrupt();
+            }
+        };
+    }
+
+    private void stopShell() {
+        if (closer != null) {
+            closer.run();
+        }
+        while (!regs.isEmpty()) {
+            ServiceRegistration<?> reg = regs.iterator().next();
+            regs.remove(reg);
+            reg.unregister();
+        }
+    }
+
+    private class ShellContext implements Context {
+        public String getProperty(String name) {
+            return context.getProperty(name);
+        }
+
+        public void exit() throws Exception {
+            context.getBundle(0).stop();
+        }
+    }
+}
\ No newline at end of file
diff --git a/jline/src/main/java/org/apache/felix/gogo/jline/BaseConverters.java b/jline/src/main/java/org/apache/felix/gogo/jline/BaseConverters.java
new file mode 100644
index 0000000..4eb1149
--- /dev/null
+++ b/jline/src/main/java/org/apache/felix/gogo/jline/BaseConverters.java
@@ -0,0 +1,145 @@
+/*
+ * 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.felix.gogo.jline;
+
+import org.apache.felix.service.command.Converter;
+import org.apache.felix.service.command.Function;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.lang.invoke.MethodHandles;
+import java.lang.reflect.Field;
+import java.lang.reflect.InvocationHandler;
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import java.lang.reflect.Proxy;
+import java.util.Arrays;
+import java.util.Collections;
+
+public class BaseConverters implements Converter {
+
+    public Object convert(Class<?> desiredType, final Object in) throws Exception {
+        if (desiredType == Class.class) {
+            try {
+                return Class.forName(in.toString());
+            } catch (ClassNotFoundException e) {
+                return null;
+            }
+        }
+
+        if (desiredType.isAssignableFrom(String.class) && in instanceof InputStream) {
+            return read(((InputStream) in));
+        }
+
+        if (in instanceof Function && isFunctional(desiredType)) {
+            return Proxy.newProxyInstance(desiredType.getClassLoader(),
+                    new Class[]{desiredType}, new InvocationHandler() {
+                        Function command = ((Function) in);
+
+                        public Object invoke(Object proxy, Method method, Object[] args)
+                                throws Throwable {
+                            if (isObjectMethod(method)) {
+                                return method.invoke(command, args);
+                            } else if (method.isDefault()) {
+                                final Field field = MethodHandles.Lookup.class.getDeclaredField("IMPL_LOOKUP");
+                                field.setAccessible(true);
+                                final MethodHandles.Lookup lookup = (MethodHandles.Lookup) field.get(null);
+                                return lookup
+                                        .unreflectSpecial(method, method.getDeclaringClass())
+                                        .bindTo(proxy)
+                                        .invokeWithArguments(args);
+                            } else {
+                                return command.execute(null,
+                                        args != null ? Arrays.asList(args) : Collections.emptyList());
+                            }
+                        }
+                    });
+        }
+
+        return null;
+    }
+
+    public CharSequence format(Object target, int level, Converter converter)
+            throws IOException {
+        if (level == INSPECT && target instanceof InputStream) {
+            return read(((InputStream) target));
+        }
+        return null;
+    }
+
+    private CharSequence read(InputStream in) throws IOException {
+        int c;
+        StringBuffer sb = new StringBuffer();
+        while ((c = in.read()) > 0) {
+            if (c >= 32 && c <= 0x7F || c == '\n' || c == '\r') {
+                sb.append((char) c);
+            } else {
+                String s = Integer.toHexString(c).toUpperCase();
+                sb.append("\\");
+                if (s.length() < 1) {
+                    sb.append(0);
+                }
+                sb.append(s);
+            }
+        }
+        return sb;
+    }
+
+    public static boolean isFunctional(Class<?> clazz) {
+        if (!clazz.isInterface()) {
+            return false;
+        }
+        int nb = 0;
+        for (Method method : clazz.getMethods()) {
+            if (method.isDefault() || isObjectMethod(method) || isStatic(method)) {
+                continue;
+            }
+            nb++;
+        }
+        return nb == 1;
+    }
+
+    public static boolean isStatic(Method method) {
+        return (method.getModifiers() & Modifier.STATIC) == Modifier.STATIC;
+    }
+
+    public static boolean isObjectMethod(Method method) {
+        switch (method.getName()) {
+            case "toString":
+                if (method.getParameterCount() == 0 && method.getReturnType() == String.class) {
+                    return true;
+                }
+                break;
+            case "equals":
+                if (method.getParameterCount() == 1
+                        && method.getParameterTypes()[0] == Object.class
+                        && method.getReturnType() == boolean.class) {
+                    return true;
+                }
+                break;
+            case "hashCode":
+                if (method.getParameterCount() == 0 && method.getReturnType() == int.class) {
+                    return true;
+                }
+                break;
+        }
+        return false;
+    }
+
+}
diff --git a/jline/src/main/java/org/apache/felix/gogo/jline/Builtin.java b/jline/src/main/java/org/apache/felix/gogo/jline/Builtin.java
new file mode 100644
index 0000000..0e4190d
--- /dev/null
+++ b/jline/src/main/java/org/apache/felix/gogo/jline/Builtin.java
@@ -0,0 +1,771 @@
+/*
+ * 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.felix.gogo.jline;
+
+import java.io.BufferedReader;
+import java.io.BufferedWriter;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.PrintStream;
+import java.io.StringWriter;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.Field;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.OpenOption;
+import java.nio.file.Path;
+import java.nio.file.StandardOpenOption;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+import java.util.TreeMap;
+import java.util.TreeSet;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.apache.felix.service.command.Job;
+import org.apache.felix.service.command.Process;
+import org.apache.felix.gogo.runtime.CommandSessionImpl;
+import org.apache.felix.service.command.CommandSession;
+import org.apache.felix.service.command.Converter;
+import org.apache.felix.service.command.Function;
+import org.jline.builtins.Commands;
+import org.jline.builtins.Completers.DirectoriesCompleter;
+import org.jline.builtins.Completers.FilesCompleter;
+import org.jline.builtins.Options;
+import org.jline.reader.Candidate;
+import org.jline.reader.LineReader;
+import org.jline.reader.ParsedLine;
+import org.jline.reader.Widget;
+import org.jline.terminal.Terminal;
+
+import static org.apache.felix.gogo.jline.Shell.getCommands;
+
+/**
+ * gosh built-in commands.
+ */
+public class Builtin {
+
+    static final String[] functions = {
+            "format", "getopt", "new", "set", "tac", "type",
+            "jobs", "fg", "bg",
+            "keymap", "setopt", "unsetopt", "complete", "history", "widget",
+            "__files", "__directories", "__usage_completion"
+    };
+
+    private static final String[] packages = {"java.lang", "java.io", "java.net", "java.util"};
+
+    private final static Set<String> KEYWORDS = new HashSet<>(
+            Arrays.asList("abstract", "continue", "for", "new", "switch",
+                    "assert", "default", "goto", "package", "synchronized", "boolean", "do",
+                    "if", "private", "this", "break", "double", "implements", "protected",
+                    "throw", "byte", "else", "import", "public", "throws", "case", "enum",
+                    "instanceof", "return", "transient", "catch", "extends", "int", "short",
+                    "try", "char", "final", "interface", "static", "void", "class",
+                    "finally", "long", "strictfp", "volatile", "const", "float", "native",
+                    "super", "while"));
+
+    public CharSequence format(CommandSession session) {
+        return format(session, session.get("_"));    // last result
+    }
+
+    public CharSequence format(CommandSession session, Object arg) {
+        Process process = Process.Utils.current();
+        CharSequence result = session.format(arg, Converter.INSPECT);
+        process.out().println(result);
+        return result;
+    }
+
+    /**
+     * script access to Options.
+     */
+    public Options getopt(List<Object> spec, Object[] args) {
+        String[] optSpec = new String[spec.size()];
+        for (int i = 0; i < optSpec.length; ++i) {
+            optSpec[i] = spec.get(i).toString();
+        }
+        return Options.compile(optSpec).parse(args);
+    }
+
+    // FIXME: the "new" command should be provided by runtime,
+    // so it can leverage same argument coercion mechanism, used to invoke methods.
+    public Object _new(CommandSession session, Object name, Object[] argv) throws Exception {
+        Class<?> clazz;
+
+        if (name instanceof Class<?>) {
+            clazz = (Class<?>) name;
+        } else {
+            clazz = loadClass(session, name.toString());
+        }
+
+        for (Constructor<?> c : clazz.getConstructors()) {
+            Class<?>[] types = c.getParameterTypes();
+            if (types.length != argv.length) {
+                continue;
+            }
+
+            boolean match = true;
+
+            Object[] transformed = argv.clone();
+            for (int i = 0; i < transformed.length; ++i) {
+                try {
+                    transformed[i] = session.convert(types[i], transformed[i]);
+                } catch (IllegalArgumentException e) {
+                    match = false;
+                    break;
+                }
+            }
+
+            if (!match) {
+                continue;
+            }
+
+            try {
+                return c.newInstance(transformed);
+            } catch (InvocationTargetException ite) {
+                Throwable cause = ite.getCause();
+                if (cause instanceof Exception) {
+                    throw (Exception) cause;
+                }
+                throw ite;
+            }
+        }
+
+        throw new IllegalArgumentException("can't coerce " + Arrays.asList(argv)
+                + " to any of " + Arrays.asList(clazz.getConstructors()));
+    }
+
+    private Class<?> loadClass(CommandSession session, String name) throws ClassNotFoundException {
+        if (!name.contains(".")) {
+            for (String p : packages) {
+                String pkg = p + "." + name;
+                try {
+                    return Class.forName(pkg, true, session.classLoader());
+                } catch (ClassNotFoundException e) {
+                }
+            }
+        }
+        return Class.forName(name, true, session.classLoader());
+    }
+
+    public void set(CommandSession session, String[] argv) {
+        final String[] usage = {
+                "set - show session variables",
+                "Usage: set [OPTIONS] [PREFIX]",
+                "  -? --help                show help",
+                "  -a --all                 show all variables, including those starting with .",
+                "  -x                       set xtrace option",
+                "  +x                       unset xtrace option",
+                "If PREFIX given, then only show variable(s) starting with PREFIX"};
+
+        Process process = Process.Utils.current();
+        Options opt = Options.compile(usage).parse(argv);
+
+        if (opt.isSet("help")) {
+            opt.usage(process.err());
+            return;
+        }
+
+        List<String> args = opt.args();
+        String prefix = (args.isEmpty() ? "" : args.get(0));
+
+        if (opt.isSet("x")) {
+            session.put("echo", true);
+        } else if ("+x".equals(prefix)) {
+            session.put("echo", null);
+        } else {
+            boolean all = opt.isSet("all");
+            for (String key : new TreeSet<>(Shell.getVariables(session))) {
+                if (!key.startsWith(prefix))
+                    continue;
+
+                if (key.startsWith(".") && !(all || prefix.length() > 0))
+                    continue;
+
+                Object target = session.get(key);
+                String type = null;
+                String value = null;
+
+                if (target != null) {
+                    Class<?> clazz = target.getClass();
+                    type = clazz.getSimpleName();
+                    value = target.toString();
+                }
+
+                String trunc = value == null || value.length() < 55 ? "" : "...";
+                process.out().println(String.format("%-15.15s %-15s %.45s%s", type, key,
+                        value, trunc));
+            }
+        }
+    }
+
+    /*
+     * the following methods depend on the internals of the runtime implementation.
+     * ideally, they should be available via some API.
+     */
+
+    public Object tac(CommandSession session, String[] argv) throws IOException {
+        final String[] usage = {
+                "tac - capture stdin as String or List and optionally write to file.",
+                "Usage: tac [-al] [FILE]",
+                "  -a --append              append to FILE",
+                "  -l --list                return List<String>",
+                "  -? --help                show help"};
+
+        Process process = Process.Utils.current();
+        Options opt = Options.compile(usage).parse(argv);
+
+        if (opt.isSet("help")) {
+            opt.usage(process.err());
+            return null;
+        }
+
+        List<String> args = opt.args();
+        BufferedWriter fw = null;
+
+        if (args.size() == 1) {
+            Path path = session.currentDir().resolve(args.get(0));
+            Set<OpenOption> options = new HashSet<>();
+            options.add(StandardOpenOption.WRITE);
+            options.add(StandardOpenOption.CREATE);
+            if (opt.isSet("append")) {
+                options.add(StandardOpenOption.APPEND);
+            } else {
+                options.add(StandardOpenOption.TRUNCATE_EXISTING);
+            }
+            fw = Files.newBufferedWriter(path, StandardCharsets.UTF_8, options.toArray(new OpenOption[options.size()]));
+        }
+
+        StringWriter sw = new StringWriter();
+        BufferedReader rdr = new BufferedReader(new InputStreamReader(process.in()));
+
+        ArrayList<String> list = null;
+
+        if (opt.isSet("list")) {
+            list = new ArrayList<>();
+        }
+
+        boolean first = true;
+        String s;
+
+        while ((s = rdr.readLine()) != null) {
+            if (list != null) {
+                list.add(s);
+            } else {
+                if (!first) {
+                    sw.write(' ');
+                }
+                first = false;
+                sw.write(s);
+            }
+
+            if (fw != null) {
+                fw.write(s);
+                fw.newLine();
+            }
+        }
+
+        if (fw != null) {
+            fw.close();
+        }
+
+        return list != null ? list : sw.toString();
+    }
+
+    // FIXME: expose API in runtime so type command doesn't have to duplicate the runtime
+    // command search strategy.
+    public boolean type(CommandSession session, String[] argv) throws Exception {
+        final String[] usage = {"type - show command type",
+                "Usage: type [OPTIONS] [name[:]]",
+                "  -a --all                 show all matches",
+                "  -? --help                show help",
+                "  -q --quiet               don't print anything, just return status",
+                "  -s --scope=NAME          list all commands in named scope",
+                "  -t --types               show full java type names"};
+
+        Process process = Process.Utils.current();
+        Options opt = Options.compile(usage).parse(argv);
+        List<String> args = opt.args();
+
+        if (opt.isSet("help")) {
+            opt.usage(process.err());
+            return true;
+        }
+
+        boolean all = opt.isSet("all");
+
+        String optScope = null;
+        if (opt.isSet("scope")) {
+            optScope = opt.get("scope");
+        }
+
+        if (args.size() == 1) {
+            String arg = args.get(0);
+            if (arg.endsWith(":")) {
+                optScope = args.remove(0);
+            }
+        }
+
+        if (optScope != null || (args.isEmpty() && all)) {
+            Set<String> snames = new TreeSet<>();
+
+            for (String sname : (getCommands(session))) {
+                if ((optScope == null) || sname.startsWith(optScope)) {
+                    snames.add(sname);
+                }
+            }
+
+            for (String sname : snames) {
+                process.out().println(sname);
+            }
+
+            return true;
+        }
+
+        if (args.size() == 0) {
+            Map<String, Integer> scopes = new TreeMap<>();
+
+            for (String sname : getCommands(session)) {
+                int colon = sname.indexOf(':');
+                String scope = sname.substring(0, colon);
+                Integer count = scopes.get(scope);
+                if (count == null) {
+                    count = 0;
+                }
+                scopes.put(scope, ++count);
+            }
+
+            for (Entry<String, Integer> entry : scopes.entrySet()) {
+                process.out().println(entry.getKey() + ":" + entry.getValue());
+            }
+
+            return true;
+        }
+
+        final String name = args.get(0).toLowerCase();
+
+        final int colon = name.indexOf(':');
+        final String MAIN = "_main"; // FIXME: must match Reflective.java
+
+        StringBuilder buf = new StringBuilder();
+        Set<String> cmds = new LinkedHashSet<>();
+
+        // get all commands
+        if ((colon != -1) || (session.get(name) != null)) {
+            cmds.add(name);
+        } else if (session.get(MAIN) != null) {
+            cmds.add(MAIN);
+        } else {
+            String path = session.get("SCOPE") != null ? session.get("SCOPE").toString()
+                    : "*";
+
+            for (String s : path.split(":")) {
+                if (s.equals("*")) {
+                    for (String sname : getCommands(session)) {
+                        if (sname.endsWith(":" + name)) {
+                            cmds.add(sname);
+                            if (!all) {
+                                break;
+                            }
+                        }
+                    }
+                } else {
+                    String sname = s + ":" + name;
+                    if (session.get(sname) != null) {
+                        cmds.add(sname);
+                        if (!all) {
+                            break;
+                        }
+                    }
+                }
+            }
+        }
+
+        for (String key : cmds) {
+            Object target = session.get(key);
+            if (target == null) {
+                continue;
+            }
+
+            CharSequence source = getClosureSource(session, key);
+
+            if (source != null) {
+                buf.append(name);
+                buf.append(" is function {");
+                buf.append(source);
+                buf.append("}");
+                continue;
+            }
+
+            for (Method m : getMethods(session, key)) {
+                StringBuilder params = new StringBuilder();
+
+                for (Class<?> type : m.getParameterTypes()) {
+                    if (params.length() > 0) {
+                        params.append(", ");
+                    }
+                    params.append(type.getSimpleName());
+                }
+
+                String rtype = m.getReturnType().getSimpleName();
+
+                if (buf.length() > 0) {
+                    buf.append("\n");
+                }
+
+                if (opt.isSet("types")) {
+                    String cname = m.getDeclaringClass().getName();
+                    buf.append(String.format("%s %s.%s(%s)", rtype, cname, m.getName(),
+                            params));
+                } else {
+                    buf.append(String.format("%s is %s %s(%s)", name, rtype, key, params));
+                }
+            }
+        }
+
+        if (buf.length() > 0) {
+            if (!opt.isSet("quiet")) {
+                process.out().println(buf);
+            }
+            return true;
+        }
+
+        if (!opt.isSet("quiet")) {
+            process.err().println("type: " + name + " not found.");
+        }
+
+        return false;
+    }
+
+    public void jobs(CommandSession session, String[] argv) {
+        final String[] usage = {
+                "jobs - list jobs",
+                "Usage: jobs [OPTIONS]",
+                "  -? --help                show help",
+        };
+        Process process = Process.Utils.current();
+        Options opt = Options.compile(usage).parse(argv);
+        if (opt.isSet("help")) {
+            opt.usage(process.err());
+            return;
+        }
+        if (!opt.args().isEmpty()) {
+            process.err().println("usage: jobs");
+            process.error(2);
+            return;
+        }
+        List<Job> jobs = session.jobs();
+        Job current = Job.Utils.current();
+        for (Job job : jobs) {
+            if (job != current) {
+                process.out().println("[" + job.id() + "] " + job.status().toString().toLowerCase()
+                        + " " + job.command());
+            }
+        }
+    }
+
+    public void fg(CommandSession session, String[] argv) {
+        final String[] usage = {
+                "fg - put job in foreground",
+                "Usage: fg [OPTIONS] [jobid]",
+                "  -? --help                show help",
+        };
+        Process process = Process.Utils.current();
+        Options opt = Options.compile(usage).parse(argv);
+        if (opt.isSet("help")) {
+            opt.usage(process.err());
+            return;
+        }
+        if (opt.args().size() > 1) {
+            process.err().println("usage: fg [jobid]");
+            process.error(2);
+            return;
+        }
+        List<Job> jobs = new ArrayList<>(session.jobs());
+        Collections.reverse(jobs);
+        Job current = Job.Utils.current();
+        if (argv.length == 0) {
+            Job job = jobs.stream().filter(j -> j != current)
+                    .findFirst().orElse(null);
+            if (job != null) {
+                job.foreground();
+            } else {
+                process.err().println("fg: no current job");
+                process.error(1);
+            }
+        } else {
+            Job job = jobs.stream().filter(j -> j != current && argv[0].equals(Integer.toString(j.id())))
+                    .findFirst().orElse(null);
+            if (job != null) {
+                job.foreground();
+            } else {
+                process.err().println("fg: job not found: " + argv[0]);
+                process.error(1);
+            }
+        }
+    }
+
+    public void bg(CommandSession session, String[] argv) {
+        final String[] usage = {
+                "bg - put job in background",
+                "Usage: bg [OPTIONS] [jobid]",
+                "  -? --help                show help",
+        };
+        Process process = Process.Utils.current();
+        Options opt = Options.compile(usage).parse(argv);
+        if (opt.isSet("help")) {
+            opt.usage(process.err());
+            return;
+        }
+        if (opt.args().size() > 1) {
+            process.err().println("usage: bg [jobid]");
+            process.error(2);
+            return;
+        }
+        List<Job> jobs = new ArrayList<>(session.jobs());
+        Collections.reverse(jobs);
+        Job current = Job.Utils.current();
+        if (argv.length == 0) {
+            Job job = jobs.stream().filter(j -> j != current)
+                    .findFirst().orElse(null);
+            if (job != null) {
+                job.background();
+            } else {
+                process.err().println("bg: no current job");
+                process.error(1);
+            }
+        } else {
+            Job job = jobs.stream().filter(j -> j != current && argv[0].equals(Integer.toString(j.id())))
+                    .findFirst().orElse(null);
+            if (job != null) {
+                job.background();
+            } else {
+                process.err().println("bg: job not found: " + argv[0]);
+                process.error(1);
+            }
+        }
+    }
+
+    private boolean isClosure(Object target) {
+        return target.getClass().getSimpleName().equals("Closure");
+    }
+
+    private boolean isCommand(Object target) {
+        return target.getClass().getSimpleName().equals("CommandProxy");
+    }
+
+    private CharSequence getClosureSource(CommandSession session, String name)
+            throws Exception {
+        Object target = session.get(name);
+
+        if (target == null) {
+            return null;
+        }
+
+        if (!isClosure(target)) {
+            return null;
+        }
+
+        Field sourceField = target.getClass().getDeclaredField("source");
+        sourceField.setAccessible(true);
+        return (CharSequence) sourceField.get(target);
+    }
+
+    private List<Method> getMethods(CommandSession session, String scmd) throws Exception {
+        final int colon = scmd.indexOf(':');
+        final String function = colon == -1 ? scmd : scmd.substring(colon + 1);
+        final String name = KEYWORDS.contains(function) ? ("_" + function) : function;
+        final String get = "get" + function;
+        final String is = "is" + function;
+        final String set = "set" + function;
+        final String MAIN = "_main"; // FIXME: must match Reflective.java
+
+        Object target = session.get(scmd);
+        if (target == null) {
+            return null;
+        }
+
+        if (isClosure(target)) {
+            return null;
+        }
+
+        if (isCommand(target)) {
+            Method method = target.getClass().getMethod("getTarget", (Class[]) null);
+            method.setAccessible(true);
+            target = method.invoke(target, (Object[]) null);
+        }
+
+        ArrayList<Method> list = new ArrayList<>();
+        Class<?> tc = (target instanceof Class<?>) ? (Class<?>) target
+                : target.getClass();
+        Method[] methods = tc.getMethods();
+
+        for (Method m : methods) {
+            String mname = m.getName().toLowerCase();
+
+            if (mname.equals(name) || mname.equals(get) || mname.equals(set)
+                    || mname.equals(is) || mname.equals(MAIN)) {
+                list.add(m);
+            }
+        }
+
+        return list;
+    }
+
+    public void history(CommandSession session, String[] argv) throws IOException {
+        Process process = Process.Utils.current();
+        Commands.history(Shell.getReader(session), process.out(), process.err(), argv);
+    }
+
+    public void complete(CommandSession session, String[] argv) {
+        Process process = Process.Utils.current();
+        Commands.complete(Shell.getReader(session), process.out(), process.err(), Shell.getCompletions(session), argv);
+    }
+
+    public void widget(final CommandSession session, String[] argv) throws Exception {
+        java.util.function.Function<String, Widget> creator = func -> () -> {
+            try {
+                session.execute(func);
+            } catch (Exception e) {
+                // TODO: log exception ?
+                return false;
+            }
+            return true;
+        };
+        Process process = Process.Utils.current();
+        Commands.widget(Shell.getReader(session), process.out(), process.err(), creator, argv);
+    }
+
+    public void keymap(CommandSession session, String[] argv) {
+        Process process = Process.Utils.current();
+        Commands.keymap(Shell.getReader(session), process.out(), process.err(), argv);
+    }
+
+    public void setopt(CommandSession session, String[] argv) {
+        Process process = Process.Utils.current();
+        Commands.setopt(Shell.getReader(session), process.out(), process.err(), argv);
+    }
+
+    public void unsetopt(CommandSession session, String[] argv) {
+        Process process = Process.Utils.current();
+        Commands.unsetopt(Shell.getReader(session), process.out(), process.err(), argv);
+    }
+
+    public List<Candidate> __files(CommandSession session) {
+        ParsedLine line = Shell.getParsedLine(session);
+        LineReader reader = Shell.getReader(session);
+        List<Candidate> candidates = new ArrayList<>();
+        new FilesCompleter(session.currentDir()) {
+            @Override
+            protected String getDisplay(Terminal terminal, Path p) {
+                return getFileDisplay(session, p);
+            }
+        }.complete(reader, line, candidates);
+        return candidates;
+    }
+
+    public List<Candidate> __directories(CommandSession session) {
+        ParsedLine line = Shell.getParsedLine(session);
+        LineReader reader = Shell.getReader(session);
+        List<Candidate> candidates = new ArrayList<>();
+        new DirectoriesCompleter(session.currentDir()) {
+            @Override
+            protected String getDisplay(Terminal terminal, Path p) {
+                return getFileDisplay(session, p);
+            }
+        }.complete(reader, line, candidates);
+        return candidates;
+    }
+
+    private String getFileDisplay(CommandSession session, Path path) {
+        String type;
+        String suffix;
+        if (Files.isSymbolicLink(path)) {
+            type = "sl";
+            suffix = "@";
+        } else if (Files.isDirectory(path)) {
+            type = "dr";
+            suffix = "/";
+        } else if (Files.isExecutable(path)) {
+            type = "ex";
+            suffix = "*";
+        } else if (!Files.isRegularFile(path)) {
+            type = "ot";
+            suffix = "";
+        } else {
+            type = "";
+            suffix = "";
+        }
+        return Posix.applyStyle(path.getFileName().toString(), Posix.getLsColorMap(session), type) + suffix;
+
+    }
+
+    public void __usage_completion(CommandSession session, String command) throws Exception {
+        Object func = session.get(command.contains(":") ? command : "*:" + command);
+        if (func instanceof Function) {
+            ByteArrayInputStream bais = new ByteArrayInputStream(new byte[0]);
+            ByteArrayOutputStream baos = new ByteArrayOutputStream();
+            ByteArrayOutputStream baes = new ByteArrayOutputStream();
+            CommandSession ts = ((CommandSessionImpl) session).processor().createSession(bais, new PrintStream(baos), new PrintStream(baes));
+            ts.execute(command + " --help");
+
+            String regex = "(?x)\\s*" + "(?:-([^-]))?" +  // 1: short-opt-1
+                    "(?:,?\\s*-(\\w))?" +                 // 2: short-opt-2
+                    "(?:,?\\s*--(\\w[\\w-]*)(=\\w+)?)?" + // 3: long-opt-1 and 4:arg-1
+                    "(?:,?\\s*--(\\w[\\w-]*))?" +         // 5: long-opt-2
+                    ".*?(?:\\(default=(.*)\\))?\\s*" +    // 6: default
+                    "(.*)";                               // 7: description
+            Pattern pattern = Pattern.compile(regex);
+            for (String l : baes.toString().split("\n")) {
+                Matcher matcher = pattern.matcher(l);
+                if (matcher.matches()) {
+                    List<String> args = new ArrayList<>();
+                    if (matcher.group(1) != null) {
+                        args.add("--short-option");
+                        args.add(matcher.group(1));
+                    }
+                    if (matcher.group(3) != null) {
+                        args.add("--long-option");
+                        args.add(matcher.group(1));
+                    }
+                    if (matcher.group(4) != null) {
+                        args.add("--argument");
+                        args.add("");
+                    }
+                    if (matcher.group(7) != null) {
+                        args.add("--description");
+                        args.add(matcher.group(7));
+                    }
+                    complete(session, args.toArray(new String[args.size()]));
+                }
+            }
+        }
+    }
+
+}
diff --git a/jline/src/main/java/org/apache/felix/gogo/jline/Converters.java b/jline/src/main/java/org/apache/felix/gogo/jline/Converters.java
new file mode 100644
index 0000000..0ed4d89
--- /dev/null
+++ b/jline/src/main/java/org/apache/felix/gogo/jline/Converters.java
@@ -0,0 +1,180 @@
+/*
+ * 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.felix.gogo.jline;
+
+import java.io.IOException;
+import java.util.Formatter;
+
+import org.apache.felix.service.command.Converter;
+import org.osgi.framework.Bundle;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.InvalidSyntaxException;
+import org.osgi.framework.ServiceReference;
+import org.osgi.framework.startlevel.BundleStartLevel;
+
+public class Converters extends BaseConverters {
+    private final BundleContext context;
+
+    public Converters(BundleContext context) {
+        this.context = context;
+    }
+
+    private CharSequence print(Bundle bundle) {
+        // [ ID ] [STATE      ] [ SL ] symname
+        int level = bundle.adapt(BundleStartLevel.class).getStartLevel();
+
+        return String.format("%5d|%-11s|%5d|%s (%s)", bundle.getBundleId(),
+                getState(bundle), level, bundle.getSymbolicName(), bundle.getVersion());
+    }
+
+    private CharSequence print(ServiceReference<?> ref) {
+        StringBuilder sb = new StringBuilder();
+        try (Formatter f = new Formatter(sb);)
+        {
+            String spid = "";
+            Object pid = ref.getProperty("service.pid");
+            if (pid != null) {
+                spid = pid.toString();
+            }
+
+            f.format("%06d %3s %-40s %s", ref.getProperty("service.id"),
+                    ref.getBundle().getBundleId(),
+                    getShortNames((String[]) ref.getProperty("objectclass")), spid);
+            return sb;
+        }
+    }
+
+    private CharSequence getShortNames(String[] list) {
+        StringBuilder sb = new StringBuilder();
+        String del = "";
+        for (String s : list) {
+            sb.append(del).append(getShortName(s));
+            del = " | ";
+        }
+        return sb;
+    }
+
+    private CharSequence getShortName(String name) {
+        int n = name.lastIndexOf('.');
+        if (n < 0) {
+            n = 0;
+        } else {
+            n++;
+        }
+        return name.subSequence(n, name.length());
+    }
+
+    private String getState(Bundle bundle) {
+        switch (bundle.getState()) {
+            case Bundle.ACTIVE:
+                return "Active";
+
+            case Bundle.INSTALLED:
+                return "Installed";
+
+            case Bundle.RESOLVED:
+                return "Resolved";
+
+            case Bundle.STARTING:
+                return "Starting";
+
+            case Bundle.STOPPING:
+                return "Stopping";
+
+            case Bundle.UNINSTALLED:
+                return "Uninstalled ";
+        }
+        return null;
+    }
+
+    public Bundle bundle(Bundle i) {
+        return i;
+    }
+
+    public Object convert(Class<?> desiredType, final Object in) throws Exception {
+        if (desiredType == Bundle.class) {
+            return convertBundle(in);
+        }
+
+        if (desiredType == ServiceReference.class) {
+            return convertServiceReference(in);
+        }
+
+        return super.convert(desiredType, in);
+    }
+
+    private Object convertServiceReference(Object in) throws InvalidSyntaxException {
+        String s = in.toString();
+        if (s.startsWith("(") && s.endsWith(")")) {
+            ServiceReference<?> refs[] = context.getServiceReferences((String) null, String.format(
+                    "(|(service.id=%s)(service.pid=%s))", in, in));
+            if (refs != null && refs.length > 0) {
+                return refs[0];
+            }
+        }
+
+        ServiceReference<?> refs[] = context.getServiceReferences((String) null, String.format(
+                "(|(service.id=%s)(service.pid=%s))", in, in));
+        if (refs != null && refs.length > 0) {
+            return refs[0];
+        }
+        return null;
+    }
+
+    private Object convertBundle(Object in) {
+        String s = in.toString();
+        try {
+            long id = Long.parseLong(s);
+            return context.getBundle(id);
+        } catch (NumberFormatException nfe) {
+            // Ignore
+        }
+
+        Bundle bundles[] = context.getBundles();
+        for (Bundle b : bundles) {
+            if (b.getLocation().equals(s)) {
+                return b;
+            }
+
+            if (b.getSymbolicName().equals(s)) {
+                return b;
+            }
+        }
+
+        return null;
+    }
+
+    public CharSequence format(Object target, int level, Converter converter)
+            throws IOException {
+        if (level == LINE && target instanceof Bundle) {
+            return print((Bundle) target);
+        }
+        if (level == LINE && target instanceof ServiceReference) {
+            return print((ServiceReference<?>) target);
+        }
+        if (level == PART && target instanceof Bundle) {
+            return ((Bundle) target).getSymbolicName();
+        }
+        if (level == PART && target instanceof ServiceReference) {
+            return getShortNames((String[]) ((ServiceReference<?>) target).getProperty("objectclass"));
+        }
+        return super.format(target, level, converter);
+    }
+
+}
diff --git a/jline/src/main/java/org/apache/felix/gogo/jline/Expander.java b/jline/src/main/java/org/apache/felix/gogo/jline/Expander.java
new file mode 100644
index 0000000..e9a8d2d
--- /dev/null
+++ b/jline/src/main/java/org/apache/felix/gogo/jline/Expander.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.felix.gogo.jline;
+
+import java.util.Collection;
+import java.util.stream.Collectors;
+
+import org.apache.felix.gogo.runtime.Closure;
+import org.apache.felix.gogo.runtime.CommandSessionImpl;
+import org.apache.felix.service.command.CommandSession;
+import org.jline.reader.impl.DefaultExpander;
+
+public class Expander extends DefaultExpander {
+
+    private final CommandSession session;
+
+    public Expander(CommandSession session) {
+        this.session = session;
+    }
+
+    @Override
+    @SuppressWarnings("unchecked")
+    public String expandVar(String word) {
+        try {
+            Object o = org.apache.felix.gogo.runtime.Expander.expand(
+                    word,
+                    new Closure((CommandSessionImpl) session, null, null));
+            if (o instanceof Collection) {
+                return ((Collection<Object>) o).stream()
+                        .map(String::valueOf)
+                        .collect(Collectors.joining(" "));
+            }
+            else if (o != null) {
+                return o.toString();
+            }
+        } catch (Exception e) {
+            // ignore
+        }
+        return word;
+    }
+
+}
diff --git a/jline/src/main/java/org/apache/felix/gogo/jline/Highlighter.java b/jline/src/main/java/org/apache/felix/gogo/jline/Highlighter.java
new file mode 100644
index 0000000..d0156ed
--- /dev/null
+++ b/jline/src/main/java/org/apache/felix/gogo/jline/Highlighter.java
@@ -0,0 +1,226 @@
+/*
+ * 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.felix.gogo.jline;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import org.apache.felix.gogo.runtime.CommandSessionImpl;
+import org.apache.felix.gogo.runtime.EOFError;
+import org.apache.felix.gogo.runtime.Parser.Program;
+import org.apache.felix.gogo.runtime.Parser.Statement;
+import org.apache.felix.gogo.runtime.SyntaxError;
+import org.apache.felix.gogo.runtime.Token;
+import org.apache.felix.service.command.CommandSession;
+import org.apache.felix.service.command.Function;
+import org.jline.reader.LineReader;
+import org.jline.reader.LineReader.RegionType;
+import org.jline.reader.impl.DefaultHighlighter;
+import org.jline.utils.AttributedString;
+import org.jline.utils.AttributedStringBuilder;
+import org.jline.utils.AttributedStyle;
+import org.jline.utils.WCWidth;
+
+public class Highlighter extends DefaultHighlighter {
+
+    public static final String DEFAULT_HIGHLIGHTER_COLORS = "rs=35:st=32:nu=32:co=32:va=36:vn=36:fu=94:bf=91:re=90";
+
+    private final CommandSession session;
+
+    public Highlighter(CommandSession session) {
+        this.session = session;
+    }
+
+    public AttributedString highlight(LineReader reader, String buffer) {
+        try {
+            Program program = null;
+            List<Token> tokens = null;
+            List<Statement> statements = null;
+            String repaired = buffer;
+            while (program == null) {
+                try {
+                    org.apache.felix.gogo.runtime.Parser parser = new org.apache.felix.gogo.runtime.Parser(repaired);
+                    program = parser.program();
+                    tokens = parser.tokens();
+                    statements = parser.statements();
+                } catch (EOFError e) {
+                    repaired = repaired + " " + e.repair();
+                    // Make sure we don't loop forever
+                    if (repaired.length() > buffer.length() + 1024) {
+                        return new AttributedStringBuilder().append(buffer).toAttributedString();
+                    }
+                }
+            }
+
+            Map<String, String> colors = Posix.getColorMap(session, "HIGHLIGHTER", DEFAULT_HIGHLIGHTER_COLORS);
+
+            int underlineStart = -1;
+            int underlineEnd = -1;
+            int negativeStart = -1;
+            int negativeEnd = -1;
+            String search = reader.getSearchTerm();
+            if (search != null && search.length() > 0) {
+                underlineStart = buffer.indexOf(search);
+                if (underlineStart >= 0) {
+                    underlineEnd = underlineStart + search.length() - 1;
+                }
+            }
+            if (reader.getRegionActive() != RegionType.NONE) {
+                negativeStart = reader.getRegionMark();
+                negativeEnd = reader.getBuffer().cursor();
+                if (negativeStart > negativeEnd) {
+                    int x = negativeEnd;
+                    negativeEnd = negativeStart;
+                    negativeStart = x;
+                }
+                if (reader.getRegionActive() == RegionType.LINE) {
+                    while (negativeStart > 0 && reader.getBuffer().atChar(negativeStart - 1) != '\n') {
+                        negativeStart--;
+                    }
+                    while (negativeEnd < reader.getBuffer().length() - 1 && reader.getBuffer().atChar(negativeEnd + 1) != '\n') {
+                        negativeEnd++;
+                    }
+                }
+            }
+
+            Type[] types = new Type[repaired.length()];
+
+            Arrays.fill(types, Type.Unknown);
+
+            int cur = 0;
+            for (Token token : tokens) {
+                // We're on the repair side, so exit now
+                if (token.start() >= buffer.length()) {
+                    break;
+                }
+                if (token.start() > cur) {
+                    cur = token.start();
+                }
+                // Find corresponding statement
+                Statement statement = null;
+                for (int i = statements.size() - 1; i >= 0; i--) {
+                    Statement s = statements.get(i);
+                    if (s.start() <= cur && cur < s.start() + s.length()) {
+                        statement = s;
+                        break;
+                    }
+                }
+
+                // Reserved tokens
+                Type type = Type.Unknown;
+                if (Token.eq(token, "{")
+                        || Token.eq(token, "}")
+                        || Token.eq(token, "(")
+                        || Token.eq(token, ")")
+                        || Token.eq(token, "[")
+                        || Token.eq(token, "]")
+                        || Token.eq(token, "|")
+                        || Token.eq(token, ";")
+                        || Token.eq(token, "=")) {
+                    type = Type.Reserved;
+                } else if (token.charAt(0) == '\'' || token.charAt(0) == '"') {
+                    type = Type.String;
+                } else if (token.toString().matches("^[-+]?[0-9]*\\.?[0-9]+([eE][-+]?[0-9]+)?$")) {
+                    type = Type.Number;
+                } else if (token.charAt(0) == '$') {
+                    type = Type.Variable;
+                } else if (((Set<?>) session.get(CommandSessionImpl.CONSTANTS)).contains(token.toString())
+                        || Token.eq(token, "null") || Token.eq(token, "false") || Token.eq(token, "true")) {
+                    type = Type.Constant;
+                } else {
+                    boolean isFirst = statement != null && statement.tokens().size() > 0
+                            && token == statement.tokens().get(0);
+                    boolean isThirdWithNext = statement != null && statement.tokens().size() > 3
+                            && token == statement.tokens().get(2);
+                    boolean isAssign = statement != null && statement.tokens().size() > 1
+                            && Token.eq(statement.tokens().get(1), "=");
+                    if (isFirst && isAssign) {
+                        type = Type.VariableName;
+                    }
+                    if (isFirst && !isAssign || isAssign && isThirdWithNext) {
+                        Object v = session.get(Shell.resolve(session, token.toString()));
+                        type = (v instanceof Function) ? Type.Function : Type.BadFunction;
+                    }
+                }
+                Arrays.fill(types, token.start(), Math.min(token.start() + token.length(), types.length), type);
+                cur = Math.min(token.start() + token.length(), buffer.length());
+            }
+
+            if (buffer.length() < repaired.length()) {
+                Arrays.fill(types, buffer.length(), repaired.length(), Type.Repair);
+            }
+
+            AttributedStringBuilder sb = new AttributedStringBuilder();
+            for (int i = 0; i < repaired.length(); i++) {
+                sb.style(AttributedStyle.DEFAULT);
+                applyStyle(sb, colors, types[i]);
+                if (i >= underlineStart && i <= underlineEnd) {
+                    sb.style(sb.style().underline());
+                }
+                if (i >= negativeStart && i <= negativeEnd) {
+                    sb.style(sb.style().inverse());
+                }
+                char c = repaired.charAt(i);
+                if (c == '\t' || c == '\n') {
+                    sb.append(c);
+                } else if (c < 32) {
+                    sb.style(sb.style().inverseNeg())
+                            .append('^')
+                            .append((char) (c + '@'))
+                            .style(sb.style().inverseNeg());
+                } else {
+                    int w = WCWidth.wcwidth(c);
+                    if (w > 0) {
+                        sb.append(c);
+                    }
+                }
+            }
+
+            return sb.toAttributedString();
+        } catch (SyntaxError e) {
+            return super.highlight(reader, buffer);
+        }
+    }
+
+    private void applyStyle(AttributedStringBuilder sb, Map<String, String> colors, Type type) {
+        Posix.applyStyle(sb, colors, type.color);
+    }
+
+    enum Type {
+        Reserved("rs"),
+        String("st"),
+        Number("nu"),
+        Variable("va"),
+        VariableName("vn"),
+        Function("fu"),
+        BadFunction("bf"),
+        Constant("co"),
+        Unknown("un"),
+        Repair("re");
+
+        private final String color;
+
+        Type(String color) {
+            this.color = color;
+        }
+    }
+
+}
diff --git a/jline/src/main/java/org/apache/felix/gogo/jline/Main.java b/jline/src/main/java/org/apache/felix/gogo/jline/Main.java
new file mode 100644
index 0000000..4fcaecc
--- /dev/null
+++ b/jline/src/main/java/org/apache/felix/gogo/jline/Main.java
@@ -0,0 +1,108 @@
+/*
+ * 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.felix.gogo.jline;
+
+import java.io.FilterInputStream;
+import java.io.FilterOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+import org.apache.felix.gogo.jline.Shell.Context;
+import org.apache.felix.gogo.runtime.CommandProcessorImpl;
+import org.apache.felix.gogo.runtime.threadio.ThreadIOImpl;
+import org.apache.felix.service.command.CommandSession;
+import org.jline.terminal.Terminal;
+import org.jline.terminal.TerminalBuilder;
+
+public class Main {
+
+    public static void main(String[] args) throws IOException {
+        try (Terminal terminal = TerminalBuilder.builder()
+                .name("gogo")
+                .system(true)
+                .nativeSignals(true)
+                .signalHandler(Terminal.SignalHandler.SIG_IGN)
+                .build()) {
+            ThreadIOImpl tio = new ThreadIOImpl();
+            tio.start();
+            try {
+                CommandProcessorImpl processor = new CommandProcessorImpl(tio);
+                Context context = new MyContext();
+                Shell shell = new Shell(context, processor, tio, null);
+                processor.addCommand("gogo", processor, "addCommand");
+                processor.addCommand("gogo", processor, "removeCommand");
+                processor.addCommand("gogo", processor, "eval");
+                processor.addConverter(new BaseConverters());
+                register(processor, new Builtin(), Builtin.functions);
+                register(processor, new Procedural(), Procedural.functions);
+                register(processor, new Posix(processor), Posix.functions);
+                register(processor, shell, Shell.functions);
+                InputStream in = new FilterInputStream(terminal.input()) {
+                    @Override
+                    public void close() {
+                    }
+                };
+                OutputStream out = new FilterOutputStream(terminal.output()) {
+                    @Override
+                    public void close() {
+                    }
+                };
+                CommandSession session = processor.createSession(in, out, out);
+                session.put(Shell.VAR_CONTEXT, context);
+                session.put(Shell.VAR_TERMINAL, terminal);
+                try {
+                    String[] argv = new String[args.length + 1];
+                    argv[0] = "--login";
+                    System.arraycopy(args, 0, argv, 1, args.length);
+                    shell.gosh(session, argv);
+                } catch (Exception e) {
+                    Object loc = session.get(".location");
+                    if (null == loc || !loc.toString().contains(":")) {
+                        loc = "gogo";
+                    }
+
+                    System.err.println(loc + ": " + e.getClass().getSimpleName() + ": " + e.getMessage());
+                    e.printStackTrace();
+                } finally {
+                    session.close();
+                }
+            } finally {
+                tio.stop();
+            }
+        }
+    }
+
+    static void register(CommandProcessorImpl processor, Object target, String[] functions) {
+        for (String function : functions) {
+            processor.addCommand("gogo", target, function);
+        }
+    }
+
+    private static class MyContext implements Context {
+
+        public String getProperty(String name) {
+            return System.getProperty(name);
+        }
+
+        public void exit() {
+            System.exit(0);
+        }
+    }
+}
diff --git a/jline/src/main/java/org/apache/felix/gogo/jline/ParsedLineImpl.java b/jline/src/main/java/org/apache/felix/gogo/jline/ParsedLineImpl.java
new file mode 100644
index 0000000..25b5270
--- /dev/null
+++ b/jline/src/main/java/org/apache/felix/gogo/jline/ParsedLineImpl.java
@@ -0,0 +1,202 @@
+/*
+ * 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.felix.gogo.jline;
+
+import org.apache.felix.gogo.runtime.Parser.Program;
+import org.apache.felix.gogo.runtime.Token;
+import org.jline.reader.CompletingParsedLine;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.function.Predicate;
+
+public class ParsedLineImpl implements CompletingParsedLine {
+
+    private final Program program;
+    private final String source;
+    private final int cursor;
+    private final List<String> tokens;
+    private final int wordIndex;
+    private final int wordCursor;
+    private final CharSequence rawWord;
+    private final int rawWordCursor;
+
+    public ParsedLineImpl(Program program, Token line, int cursor, List<Token> tokens) {
+        this.program = program;
+        this.source = line.toString();
+        this.cursor = cursor - line.start();
+        this.tokens = new ArrayList<>();
+        for (Token token : tokens) {
+            this.tokens.add(unquote(token, null).toString());
+        }
+        int wi = tokens.size();
+        int wc = 0;
+        if (cursor >= 0) {
+            for (int i = 0; i < tokens.size(); i++) {
+                Token t = tokens.get(i);
+                if (t.start() > cursor) {
+                    wi = i;
+                    wc = 0;
+                    this.tokens.add(i, "");
+                    break;
+                }
+                if (t.start() + t.length() >= cursor) {
+                    wi = i;
+                    wc = cursor - t.start();
+                    break;
+                }
+            }
+        }
+        if (wi == tokens.size()) {
+            this.tokens.add("");
+            rawWord = "";
+            wordCursor = 0;
+        } else {
+            rawWord = tokens.get(wi);
+            int[] c = new int[] { wc };
+            unquote(rawWord, c);
+            wordCursor = c[0];
+        }
+        wordIndex = wi;
+        rawWordCursor = wc;
+    }
+
+    public String word() {
+        return tokens.get(wordIndex());
+    }
+
+    public int wordCursor() {
+        return wordCursor;
+    }
+
+    public int wordIndex() {
+        return wordIndex;
+    }
+
+    public List<String> words() {
+        return tokens;
+    }
+
+    public String line() {
+        return source;
+    }
+
+    public int cursor() {
+        return cursor;
+    }
+
+    public Program program() {
+        return program;
+    }
+
+    public int rawWordCursor() {
+        return rawWordCursor;
+    }
+
+    public int rawWordLength() {
+        return rawWord.length();
+    }
+
+    public CharSequence escape(CharSequence str, boolean complete) {
+        StringBuilder sb = new StringBuilder(str);
+        Predicate<Character> needToBeEscaped;
+        char quote = 0;
+        char first = rawWord.length() > 0 ? rawWord.charAt(0) : 0;
+        if (first == '\'') {
+            quote = '\'';
+            needToBeEscaped = i -> i == '\'';
+        } else if (first == '"') {
+            quote = '"';
+            needToBeEscaped = i -> i == '"';
+        } else {
+            needToBeEscaped = i -> i == ' ' || i == '\t';
+        }
+        for (int i = 0; i < sb.length(); i++) {
+            if (needToBeEscaped.test(str.charAt(i))) {
+                sb.insert(i++, '\\');
+            }
+        }
+        if (quote != 0) {
+            sb.insert(0, quote);
+            if (complete) {
+                sb.append(quote);
+            }
+        }
+        return sb;
+    }
+
+    private CharSequence unquote(CharSequence arg, int[] cursor) {
+        boolean hasEscape = false;
+        for (int i = 0; i < arg.length(); i++) {
+            int c = arg.charAt(i);
+            if (c == '\\' || c == '"' || c == '\'') {
+                hasEscape = true;
+                break;
+            }
+        }
+        if (!hasEscape) {
+            return arg;
+        }
+        boolean singleQuoted = false;
+        boolean doubleQuoted = false;
+        boolean escaped = false;
+        StringBuilder buf = new StringBuilder(arg.length());
+        for (int i = 0; i < arg.length(); i++) {
+            if (cursor != null && cursor[0] == i) {
+                cursor[0] = buf.length();
+                cursor = null;
+            }
+            char c = arg.charAt(i);
+            if (doubleQuoted && escaped) {
+                if (c != '"' && c != '\\' && c != '$' && c != '%') {
+                    buf.append('\\');
+                }
+                buf.append(c);
+                escaped = false;
+            } else if (escaped) {
+                buf.append(c);
+                escaped = false;
+            } else if (singleQuoted) {
+                if (c == '\'') {
+                    singleQuoted = false;
+                } else {
+                    buf.append(c);
+                }
+            } else if (doubleQuoted) {
+                if (c == '\\') {
+                    escaped = true;
+                } else if (c == '\"') {
+                    doubleQuoted = false;
+                } else {
+                    buf.append(c);
+                }
+            } else if (c == '\\') {
+                escaped = true;
+            } else if (c == '\'') {
+                singleQuoted = true;
+            } else if (c == '"') {
+                doubleQuoted = true;
+            } else {
+                buf.append(c);
+            }
+        }
+        return buf.toString();
+    }
+
+}
diff --git a/jline/src/main/java/org/apache/felix/gogo/jline/Parser.java b/jline/src/main/java/org/apache/felix/gogo/jline/Parser.java
new file mode 100644
index 0000000..d307b5a
--- /dev/null
+++ b/jline/src/main/java/org/apache/felix/gogo/jline/Parser.java
@@ -0,0 +1,93 @@
+/*
+ * 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.felix.gogo.jline;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import org.apache.felix.gogo.runtime.EOFError;
+import org.apache.felix.gogo.runtime.Parser.Program;
+import org.apache.felix.gogo.runtime.Parser.Statement;
+import org.apache.felix.gogo.runtime.SyntaxError;
+import org.apache.felix.gogo.runtime.Token;
+import org.jline.reader.ParsedLine;
+
+public class Parser implements org.jline.reader.Parser {
+
+    public ParsedLine parse(String line, int cursor, ParseContext context) throws org.jline.reader.SyntaxError {
+        try {
+            return doParse(line, cursor, context);
+        } catch (EOFError e) {
+            throw new org.jline.reader.EOFError(e.line(), e.column(), e.getMessage(), e.missing());
+        } catch (SyntaxError e) {
+            throw new org.jline.reader.SyntaxError(e.line(), e.column(), e.getMessage());
+        }
+    }
+
+    private ParsedLine doParse(String line, int cursor, ParseContext parseContext) throws SyntaxError {
+        Program program = null;
+        List<Statement> statements = null;
+        String repaired = line;
+        while (program == null) {
+            try {
+                org.apache.felix.gogo.runtime.Parser parser = new org.apache.felix.gogo.runtime.Parser(repaired);
+                program = parser.program();
+                statements = parser.statements();
+            } catch (EOFError e) {
+                // Make sure we don't loop forever
+                if (parseContext == ParseContext.COMPLETE && repaired.length() < line.length() + 1024) {
+                    repaired = repaired + " " + e.repair();
+                } else {
+                    throw e;
+                }
+            }
+        }
+        // Find corresponding statement
+        Statement statement = null;
+        for (int i = statements.size() - 1; i >= 0; i--) {
+            Statement s = statements.get(i);
+            if (s.start() <= cursor) {
+                boolean isOk = true;
+                // check if there are only spaces after the previous statement
+                if (s.start() + s.length() < cursor) {
+                    for (int j = s.start() + s.length(); isOk && j < cursor; j++) {
+                        isOk = Character.isWhitespace(line.charAt(j));
+                    }
+                }
+                statement = s;
+                break;
+            }
+        }
+        if (statement != null && statement.tokens() != null && !statement.tokens().isEmpty()) {
+            if (repaired != line) {
+                Token stmt = statement.subSequence(0, line.length() - statement.start());
+                List<Token> tokens = new ArrayList<>(statement.tokens());
+                Token last = tokens.get(tokens.size() - 1);
+                tokens.set(tokens.size() - 1, last.subSequence(0, line.length() - last.start()));
+                return new ParsedLineImpl(program, stmt, cursor, tokens);
+            }
+            return new ParsedLineImpl(program, statement, cursor, statement.tokens());
+        } else {
+            // TODO:
+            return new ParsedLineImpl(program, program, cursor, Collections.singletonList(program));
+        }
+    }
+
+}
diff --git a/jline/src/main/java/org/apache/felix/gogo/jline/Posix.java b/jline/src/main/java/org/apache/felix/gogo/jline/Posix.java
new file mode 100644
index 0000000..43723fc
--- /dev/null
+++ b/jline/src/main/java/org/apache/felix/gogo/jline/Posix.java
@@ -0,0 +1,2110 @@
+/*
+ * 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.felix.gogo.jline;
+
+import java.io.BufferedReader;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.Closeable;
+import java.io.File;
+import java.io.FilterInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.io.PrintStream;
+import java.io.Reader;
+import java.nio.file.Files;
+import java.nio.file.LinkOption;
+import java.nio.file.Path;
+import java.nio.file.StandardWatchEventKinds;
+import java.nio.file.WatchKey;
+import java.nio.file.WatchService;
+import java.nio.file.attribute.FileTime;
+import java.nio.file.attribute.PosixFilePermission;
+import java.nio.file.attribute.PosixFilePermissions;
+import java.text.SimpleDateFormat;
+import java.time.Instant;
+import java.time.ZoneId;
+import java.time.ZonedDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.Date;
+import java.util.EnumSet;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.TreeMap;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.function.Consumer;
+import java.util.function.IntBinaryOperator;
+import java.util.function.IntConsumer;
+import java.util.function.Predicate;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import org.apache.felix.service.command.Process;
+import org.apache.felix.gogo.jline.Shell.Context;
+import org.apache.felix.service.command.CommandProcessor;
+import org.apache.felix.service.command.CommandSession;
+import org.jline.builtins.Commands;
+import org.jline.builtins.Less;
+import org.jline.builtins.Nano;
+import org.jline.builtins.Options;
+import org.jline.builtins.Source;
+import org.jline.builtins.Source.PathSource;
+import org.jline.builtins.TTop;
+import org.jline.terminal.Attributes;
+import org.jline.terminal.Terminal;
+import org.jline.utils.AttributedString;
+import org.jline.utils.AttributedStringBuilder;
+import org.jline.utils.InfoCmp.Capability;
+import org.jline.utils.OSUtils;
+import org.jline.utils.StyleResolver;
+
+/**
+ * Posix-like utilities.
+ *
+ * @see <a href="http://www.opengroup.org/onlinepubs/009695399/utilities/contents.html">
+ * http://www.opengroup.org/onlinepubs/009695399/utilities/contents.html</a>
+ */
+public class Posix {
+
+    static final String[] functions;
+
+    static {
+        // TTop function is new in JLine 3.2
+        String[] func;
+        try {
+            @SuppressWarnings("unused")
+            Class<?> cl = TTop.class;
+            func = new String[] {
+                    "cat", "echo", "grep", "sort", "sleep", "cd", "pwd", "ls",
+                    "less", "watch", "nano", "tmux",
+                    "head", "tail", "clear", "wc",
+                    "date", "ttop",
+            };
+        } catch (Throwable t) {
+            func = new String[] {
+                    "cat", "echo", "grep", "sort", "sleep", "cd", "pwd", "ls",
+                    "less", "watch", "nano", "tmux",
+                    "head", "tail", "clear", "wc",
+                    "date"
+            };
+        }
+        functions = func;
+    }
+
+    public static final String DEFAULT_LS_COLORS = "dr=1;91:ex=1;92:sl=1;96:ot=34;43";
+    public static final String DEFAULT_GREP_COLORS = "mt=1;31:fn=35:ln=32:se=36";
+
+    private static final LinkOption[] NO_FOLLOW_OPTIONS = new LinkOption[]{LinkOption.NOFOLLOW_LINKS};
+    private static final List<String> WINDOWS_EXECUTABLE_EXTENSIONS = Collections.unmodifiableList(Arrays.asList(".bat", ".exe", ".cmd"));
+    private static final LinkOption[] EMPTY_LINK_OPTIONS = new LinkOption[0];
+
+    private final CommandProcessor processor;
+
+    public Posix(CommandProcessor processor) {
+        this.processor = processor;
+    }
+
+    public void _main(CommandSession session, String[] argv) {
+        if (argv == null || argv.length < 1) {
+            throw new IllegalArgumentException();
+        }
+        Process process = Process.Utils.current();
+        try {
+            run(session, process, argv);
+        } catch (IllegalArgumentException e) {
+            process.err().println(e.getMessage());
+            process.error(2);
+        } catch (HelpException e) {
+            process.err().println(e.getMessage());
+            process.error(0);
+        } catch (Exception e) {
+            process.err().println(argv[0] + ": " + e.toString());
+            process.error(1);
+        }
+    }
+
+    @SuppressWarnings("serial")
+    protected static class HelpException extends Exception {
+        public HelpException(String message) {
+            super(message);
+        }
+    }
+
+    protected Options parseOptions(CommandSession session, String[] usage, Object[] argv) throws Exception {
+        Options opt = Options.compile(usage, s -> get(session, s)).parse(argv, true);
+        if (opt.isSet("help")) {
+            ByteArrayOutputStream baos = new ByteArrayOutputStream();
+            opt.usage(new PrintStream(baos));
+            throw new HelpException(baos.toString());
+        }
+        return opt;
+    }
+
+    protected String get(CommandSession session, String name) {
+        Object o = session.get(name);
+        return o != null ? o.toString() : null;
+    }
+
+    protected Object run(CommandSession session, Process process, String[] argv) throws Exception {
+        switch (argv[0]) {
+            case "cat":
+                cat(session, process, argv);
+                break;
+            case "echo":
+                echo(session, process, argv);
+                break;
+            case "grep":
+                grep(session, process, argv);
+                break;
+            case "sort":
+                sort(session, process, argv);
+                break;
+            case "sleep":
+                sleep(session, process, argv);
+                break;
+            case "cd":
+                cd(session, process, argv);
+                break;
+            case "pwd":
+                pwd(session, process, argv);
+                break;
+            case "ls":
+                ls(session, process, argv);
+                break;
+            case "less":
+                less(session, process, argv);
+                break;
+            case "watch":
+                watch(session, process, argv);
+                break;
+            case "nano":
+                nano(session, process, argv);
+                break;
+            case "tmux":
+                tmux(session, process, argv);
+                break;
+            case "ttop":
+                ttop(session, process, argv);
+                break;
+            case "clear":
+                clear(session, process, argv);
+                break;
+            case "head":
+                head(session, process, argv);
+                break;
+            case "tail":
+                tail(session, process, argv);
+                break;
+            case "wc":
+                wc(session, process, argv);
+                break;
+            case "date":
+                date(session, process, argv);
+                break;
+        }
+        return null;
+    }
+
+    protected void date(CommandSession session, Process process, String[] argv) throws Exception {
+        String[] usage = {
+                "date -  display date",
+                "Usage: date [-r seconds] [-v[+|-]val[mwdHMS] ...] [-f input_fmt new_date] [+output_fmt]",
+                "  -? --help                    Show help",
+                "  -u                           Use UTC",
+                "  -r                           Print the date represented by 'seconds' since January 1, 1970",
+                "  -f                           Use 'input_fmt' to parse 'new_date'"
+        };
+        Date input = new Date();
+        String output = null;
+        for (int i = 1; i < argv.length; i++) {
+            if ("-?".equals(argv[i]) || "--help".equals(argv[i])) {
+                throw new HelpException(String.join("\n", usage));
+            }
+            else if ("-r".equals(argv[i])) {
+                if (i + 1 < argv.length) {
+                    input = new Date(Long.parseLong(argv[++i]) * 1000L);
+                } else {
+                    throw new IllegalArgumentException("usage: date [-u] [-r seconds] [-v[+|-]val[mwdHMS] ...] [-f input_fmt new_date] [+output_fmt]");
+                }
+            }
+            else if ("-f".equals(argv[i])) {
+                if (i + 2 < argv.length) {
+                    String fmt = argv[++i];
+                    String inp = argv[++i];
+                    String jfmt = toJavaDateFormat(fmt);
+                    input = new SimpleDateFormat(jfmt).parse(inp);
+                } else {
+                    throw new IllegalArgumentException("usage: date [-u] [-r seconds] [-v[+|-]val[mwdHMS] ...] [-f input_fmt new_date] [+output_fmt]");
+                }
+            }
+            else if (argv[i].startsWith("+")) {
+                if (output == null) {
+                    output = argv[i].substring(1);
+                } else {
+                    throw new IllegalArgumentException("usage: date [-u] [-r seconds] [-v[+|-]val[mwdHMS] ...] [-f input_fmt new_date] [+output_fmt]");
+                }
+            }
+            else {
+                throw new IllegalArgumentException("usage: date [-u] [-r seconds] [-v[+|-]val[mwdHMS] ...] [-f input_fmt new_date] [+output_fmt]");
+            }
+        }
+        if (output == null) {
+            output = "%c";
+        }
+        // Print output
+        process.out().println(new SimpleDateFormat(toJavaDateFormat(output)).format(input));
+    }
+
+    private String toJavaDateFormat(String format) {
+        // transform Unix format to Java SimpleDateFormat (if required)
+        StringBuilder sb = new StringBuilder();
+        boolean quote = false;
+        for (int i = 0; i < format.length(); i++) {
+            char c = format.charAt(i);
+            if (c == '%') {
+                if (i + 1 < format.length()) {
+                    if (quote) {
+                        sb.append('\'');
+                        quote = false;
+                    }
+                    c = format.charAt(++i);
+                    switch (c) {
+                        case '+':
+                        case 'A': sb.append("MMM EEE d HH:mm:ss yyyy"); break;
+                        case 'a': sb.append("EEE"); break;
+                        case 'B': sb.append("MMMMMMM"); break;
+                        case 'b': sb.append("MMM"); break;
+                        case 'C': sb.append("yy"); break;
+                        case 'c': sb.append("MMM EEE d HH:mm:ss yyyy"); break;
+                        case 'D': sb.append("MM/dd/yy"); break;
+                        case 'd': sb.append("dd"); break;
+                        case 'e': sb.append("dd"); break;
+                        case 'F': sb.append("yyyy-MM-dd"); break;
+                        case 'G': sb.append("YYYY"); break;
+                        case 'g': sb.append("YY"); break;
+                        case 'H': sb.append("HH"); break;
+                        case 'h': sb.append("MMM"); break;
+                        case 'I': sb.append("hh"); break;
+                        case 'j': sb.append("DDD"); break;
+                        case 'k': sb.append("HH"); break;
+                        case 'l': sb.append("hh"); break;
+                        case 'M': sb.append("mm"); break;
+                        case 'm': sb.append("MM"); break;
+                        case 'N': sb.append("S"); break;
+                        case 'n': sb.append("\n"); break;
+                        case 'P': sb.append("aa"); break;
+                        case 'p': sb.append("aa"); break;
+                        case 'r': sb.append("hh:mm:ss aa"); break;
+                        case 'R': sb.append("HH:mm"); break;
+                        case 'S': sb.append("ss"); break;
+                        case 's': sb.append("S"); break;
+                        case 'T': sb.append("HH:mm:ss"); break;
+                        case 't': sb.append("\t"); break;
+                        case 'U': sb.append("w"); break;
+                        case 'u': sb.append("u"); break;
+                        case 'V': sb.append("W"); break;
+                        case 'v': sb.append("dd-MMM-yyyy"); break;
+                        case 'W': sb.append("w"); break;
+                        case 'w': sb.append("u"); break;
+                        case 'X': sb.append("HH:mm:ss"); break;
+                        case 'x': sb.append("MM/dd/yy"); break;
+                        case 'Y': sb.append("yyyy"); break;
+                        case 'y': sb.append("yy"); break;
+                        case 'Z': sb.append("z"); break;
+                        case 'z': sb.append("X"); break;
+                        case '%': sb.append("%"); break;
+                    }
+                } else {
+                    if (!quote) {
+                        sb.append('\'');
+                    }
+                    sb.append(c);
+                    sb.append('\'');
+                }
+            } else {
+                if ((c >= 'A' && c <= 'Z' || c >= 'a' && c <= 'z') && !quote) {
+                    sb.append('\'');
+                    quote = true;
+                }
+                sb.append(c);
+            }
+        }
+        return sb.toString();
+    }
+
+    protected void wc(CommandSession session, Process process, String[] argv) throws Exception {
+        String[] usage = {
+                "wc -  word, line, character, and byte count",
+                "Usage: wc [OPTIONS] [FILES]",
+                "  -? --help                    Show help",
+                "  -l --lines                   Print line counts",
+                "  -c --bytes                   Print byte counts",
+                "  -m --chars                   Print character counts",
+                "  -w --words                   Print word counts",
+        };
+        Options opt = parseOptions(session, usage, argv);
+        List<Source> sources = new ArrayList<>();
+        if (opt.args().isEmpty()) {
+            opt.args().add("-");
+        }
+        for (String arg : opt.args()) {
+            if ("-".equals(arg)) {
+                sources.add(new StdInSource(process));
+            } else {
+                sources.add(new PathSource(session.currentDir().resolve(arg), arg));
+            }
+        }
+        boolean displayLines = opt.isSet("lines");
+        boolean displayWords = opt.isSet("words");
+        boolean displayChars = opt.isSet("chars");
+        boolean displayBytes = opt.isSet("bytes");
+        if (displayChars) {
+            displayBytes = false;
+        }
+        if (!displayLines && !displayWords && !displayChars && !displayBytes) {
+            displayLines = true;
+            displayWords = true;
+            displayBytes = true;
+        }
+        String format = "";
+        if (displayLines) {
+            format += "%1$8d";
+        }
+        if (displayWords) {
+            format += "%2$8d";
+        }
+        if (displayChars) {
+            format += "%3$8d";
+        }
+        if (displayBytes) {
+            format += "%4$8d";
+        }
+        format += "  %5s";
+        int totalLines = 0;
+        int totalBytes = 0;
+        int totalChars = 0;
+        int totalWords = 0;
+        for (Source src : sources) {
+            try (InputStream is = src.read()) {
+                AtomicInteger lines = new AtomicInteger();
+                AtomicInteger bytes = new AtomicInteger();
+                AtomicInteger chars = new AtomicInteger();
+                AtomicInteger words = new AtomicInteger();
+                AtomicBoolean inWord = new AtomicBoolean();
+                AtomicBoolean lastNl = new AtomicBoolean(true);
+                InputStream isc = new FilterInputStream(is) {
+                    @Override
+                    public int read() throws IOException {
+                        int b = super.read();
+                        if (b >= 0) {
+                            bytes.incrementAndGet();
+                        }
+                        return b;
+                    }
+
+                    @Override
+                    public int read(byte[] b, int off, int len) throws IOException {
+                        int nb = super.read(b, off, len);
+                        if (nb > 0) {
+                            bytes.addAndGet(nb);
+                        }
+                        return nb;
+                    }
+                };
+                IntConsumer consumer = cp -> {
+                    chars.incrementAndGet();
+                    boolean ws = Character.isWhitespace(cp);
+                    if (inWord.getAndSet(!ws) && ws) {
+                        words.incrementAndGet();
+                    }
+                    if (cp == '\n') {
+                        lines.incrementAndGet();
+                        lastNl.set(true);
+                    } else {
+                        lastNl.set(false);
+                    }
+                };
+                Reader reader = new InputStreamReader(isc);
+                while (true) {
+                    int h = reader.read();
+                    if (Character.isHighSurrogate((char) h)) {
+                        int l = reader.read();
+                        if (Character.isLowSurrogate((char) l)) {
+                            int cp = Character.toCodePoint((char) h, (char) l);
+                            consumer.accept(cp);
+                        } else {
+                            consumer.accept(h);
+                            if (l >= 0) {
+                                consumer.accept(l);
+                            } else {
+                                break;
+                            }
+                        }
+                    } else if (h >= 0) {
+                        consumer.accept(h);
+                    } else {
+                        break;
+                    }
+                }
+                if (inWord.get()) {
+                    words.incrementAndGet();
+                }
+                if (!lastNl.get()) {
+                    lines.incrementAndGet();
+                }
+                process.out().println(String.format(format, lines.get(), words.get(), chars.get(), bytes.get(), src.getName()));
+                totalBytes += bytes.get();
+                totalChars += chars.get();
+                totalWords += words.get();
+                totalLines += lines.get();
+            }
+        }
+        if (sources.size() > 1) {
+            process.out().println(String.format(format, totalLines, totalWords, totalChars, totalBytes, "total"));
+        }
+    }
+
+    protected void head(CommandSession session, Process process, String[] argv) throws Exception {
+        String[] usage = {
+                "head -  displays first lines of file",
+                "Usage: head [-n lines | -c bytes] [file ...]",
+                "  -? --help                    Show help",
+                "  -n --lines=LINES             Print line counts",
+                "  -c --bytes=BYTES             Print byte counts",
+        };
+        Options opt = parseOptions(session, usage, argv);
+        if (opt.isSet("lines") && opt.isSet("bytes")) {
+            throw new IllegalArgumentException("usage: head [-n # | -c #] [file ...]");
+        }
+        int nbLines = Integer.MAX_VALUE;
+        int nbBytes = Integer.MAX_VALUE;
+        if (opt.isSet("lines")) {
+            nbLines = opt.getNumber("lines");
+        } else if (opt.isSet("bytes")) {
+            nbBytes = opt.getNumber("bytes");
+        } else {
+            nbLines = 10;
+        }
+        List<Source> sources = new ArrayList<>();
+        if (opt.args().isEmpty()) {
+            opt.args().add("-");
+        }
+        for (String arg : opt.args()) {
+            if ("-".equals(arg)) {
+                sources.add(new StdInSource(process));
+            } else {
+                sources.add(new PathSource(session.currentDir().resolve(arg), arg));
+            }
+        }
+        for (Source src : sources) {
+            int bytes = nbBytes;
+            int lines = nbLines;
+            if (sources.size() > 1) {
+                if (src != sources.get(0)) {
+                    process.out().println();
+                }
+                process.out().println("==> " + src.getName() + " <==");
+            }
+            try (InputStream is = src.read()) {
+                byte[] buf = new byte[1024];
+                int nb;
+                do {
+                    nb = is.read(buf);
+                    if (nb > 0 && lines > 0 && bytes > 0) {
+                        nb = Math.min(nb, bytes);
+                        for (int i = 0; i < nb; i++) {
+                            if (buf[i] == '\n' && --lines <= 0) {
+                                nb = i + 1;
+                                break;
+                            }
+                        }
+                        bytes -= nb;
+                        process.out().write(buf, 0, nb);
+                    }
+                } while (nb > 0 && lines > 0 && bytes > 0);
+            }
+        }
+    }
+
+    protected void tail(CommandSession session, Process process, String[] argv) throws Exception {
+        String[] usage = {
+                "tail -  displays last lines of file",
+                "Usage: tail [-f] [-q] [-c # | -n #] [file ...]",
+                "  -? --help                    Show help",
+                "  -q --quiet                   Suppress headers when printing multiple sources",
+                "  -f --follow                  Do not stop at end of file",
+                "  -F --FOLLOW                  Follow and check for file renaming or rotation",
+                "  -n --lines=LINES             Number of lines to print",
+                "  -c --bytes=BYTES             Number of bytes to print",
+        };
+        Options opt = parseOptions(session, usage, argv);
+        if (opt.isSet("lines") && opt.isSet("bytes")) {
+            throw new IllegalArgumentException("usage: tail [-f] [-q] [-c # | -n #] [file ...]");
+        }
+        int lines;
+        int bytes;
+        if (opt.isSet("lines")) {
+            lines = opt.getNumber("lines");
+            bytes = Integer.MAX_VALUE;
+        } else if (opt.isSet("bytes")) {
+            lines = Integer.MAX_VALUE;
+            bytes = opt.getNumber("bytes");
+        } else {
+            lines = 10;
+            bytes = Integer.MAX_VALUE;
+        }
+        boolean follow = opt.isSet("follow") || opt.isSet("FOLLOW");
+
+        AtomicReference<Object> lastPrinted = new AtomicReference<>();
+        WatchService watchService = follow ? session.currentDir().getFileSystem().newWatchService() : null;
+        Set<Path> watched = new HashSet<>();
+
+        class Input implements Closeable {
+            String name;
+            Path path;
+            Reader reader;
+            StringBuilder buffer;
+            long ino;
+            long size;
+
+            public Input(String name) {
+                this.name = name;
+                this.buffer = new StringBuilder();
+            }
+
+            public void open() {
+                if (reader == null) {
+                    try {
+                        InputStream is;
+                        if ("-".equals(name)) {
+                            is = new StdInSource(process).read();
+                        } else {
+                            path = session.currentDir().resolve(name);
+                            is = Files.newInputStream(path);
+                            if (opt.isSet("FOLLOW")) {
+                                try {
+                                    ino = (Long) Files.getAttribute(path, "unix:ino");
+                                } catch (Exception e) {
+                                    // Ignore
+                                }
+                            }
+                            size = Files.size(path);
+                        }
+                        reader = new InputStreamReader(is);
+                    } catch (IOException e) {
+                        // Ignore
+                    }
+                }
+            }
+
+            @Override
+            public void close() throws IOException {
+                if (reader != null) {
+                    try {
+                        reader.close();
+                    } finally {
+                        reader = null;
+                    }
+                }
+            }
+
+            public boolean tail() throws IOException {
+                open();
+                if (reader != null) {
+                    if (buffer != null) {
+                        char[] buf = new char[1024];
+                        int nb;
+                        while ((nb = reader.read(buf)) > 0) {
+                            buffer.append(buf, 0, nb);
+                            if (bytes > 0 && buffer.length() > bytes) {
+                                buffer.delete(0, buffer.length() - bytes);
+                            } else {
+                                int l = 0;
+                                int i = -1;
+                                while ((i = buffer.indexOf("\n", i + 1)) >= 0) {
+                                    l++;
+                                }
+                                if (l > lines) {
+                                    i = -1;
+                                    l = l - lines;
+                                    while (--l >= 0) {
+                                        i = buffer.indexOf("\n", i + 1);
+                                    }
+                                    buffer.delete(0, i + 1);
+                                }
+                            }
+                        }
+                        String toPrint = buffer.toString();
+                        print(toPrint);
+                        buffer = null;
+                        if (follow && path != null) {
+                            Path parent = path.getParent();
+                            if (!watched.contains(parent)) {
+                                parent.register(watchService,
+                                        StandardWatchEventKinds.ENTRY_CREATE,
+                                        StandardWatchEventKinds.ENTRY_DELETE,
+                                        StandardWatchEventKinds.ENTRY_MODIFY);
+                                watched.add(parent);
+                            }
+                        }
+                        return follow;
+                    }
+                    else if (follow && path != null) {
+                        while (true) {
+                            long newSize = Files.size(path);
+                            if (size != newSize) {
+                                char[] buf = new char[1024];
+                                int nb;
+                                while ((nb = reader.read(buf)) > 0) {
+                                    print(new String(buf, 0, nb));
+                                }
+                                size = newSize;
+                            }
+                            if (opt.isSet("FOLLOW")) {
+                                long newIno = 0;
+                                try {
+                                    newIno = (Long) Files.getAttribute(path, "unix:ino");
+                                } catch (Exception e) {
+                                    // Ignore
+                                }
+                                if (ino != newIno) {
+                                    close();
+                                    open();
+                                    ino = newIno;
+                                    size = -1;
+                                    continue;
+                                }
+                            }
+                            break;
+                        }
+                        return true;
+                    } else {
+                        return false;
+                    }
+                } else {
+                    Path parent = path.getParent();
+                    if (!watched.contains(parent)) {
+                        parent.register(watchService,
+                                StandardWatchEventKinds.ENTRY_CREATE,
+                                StandardWatchEventKinds.ENTRY_DELETE,
+                                StandardWatchEventKinds.ENTRY_MODIFY);
+                        watched.add(parent);
+                    }
+                    return true;
+                }
+            }
+
+            private void print(String toPrint) {
+                if (lastPrinted.get() != this && opt.args().size() > 1 && !opt.isSet("quiet")) {
+                    process.out().println();
+                    process.out().println("==> " + name + " <==");
+                }
+                process.out().print(toPrint);
+                lastPrinted.set(this);
+            }
+        }
+
+        if (opt.args().isEmpty()) {
+            opt.args().add("-");
+        }
+        List<Input> inputs = new ArrayList<>();
+        for (String name : opt.args()) {
+            Input input = new Input(name);
+            inputs.add(input);
+        }
+        try {
+            boolean cont = true;
+            while (cont) {
+                cont = false;
+                for (Input input : inputs) {
+                    cont |= input.tail();
+                }
+                if (cont) {
+                    WatchKey key = watchService.take();
+                    key.pollEvents();
+                    key.reset();
+                }
+            }
+        } catch (InterruptedException e) {
+            // Ignore, this is the only way to quit
+        } finally {
+            for (Input input : inputs) {
+                input.close();
+            }
+        }
+    }
+
+    protected void clear(CommandSession session, Process process, String[] argv) throws Exception {
+        final String[] usage = {
+                "clear -  clear screen",
+                "Usage: clear [OPTIONS]",
+                "  -? --help                    Show help",
+        };
+        @SuppressWarnings("unused")
+        Options opt = parseOptions(session, usage, argv);
+        if (process.isTty(1)) {
+            Shell.getTerminal(session).puts(Capability.clear_screen);
+            Shell.getTerminal(session).flush();
+        }
+    }
+
+    protected void tmux(final CommandSession session, Process process, String[] argv) throws Exception {
+        Commands.tmux(Shell.getTerminal(session),
+                process.out(), System.err,
+                () -> session.get(".tmux"),
+                t -> session.put(".tmux", t),
+                c -> startShell(session, c),
+                Arrays.copyOfRange(argv, 1, argv.length));
+    }
+
+    private void startShell(CommandSession session, Terminal terminal) {
+        new Thread(() -> runShell(session, terminal), terminal.getName() + " shell").start();
+    }
+
+    private void runShell(CommandSession session, Terminal terminal) {
+        InputStream in = terminal.input();
+        OutputStream out = terminal.output();
+        CommandSession newSession = processor.createSession(in, out, out);
+        newSession.put(Shell.VAR_TERMINAL, terminal);
+        newSession.put(".tmux", session.get(".tmux"));
+        Context context = new Context() {
+            public String getProperty(String name) {
+                return System.getProperty(name);
+            }
+            public void exit() throws Exception {
+                terminal.close();
+            }
+        };
+        try {
+            new Shell(context, processor).gosh(newSession, new String[]{"--login"});
+        } catch (Exception e) {
+            e.printStackTrace();
+        } finally {
+            try {
+                terminal.close();
+            } catch (IOException e) {
+                e.printStackTrace();
+            }
+        }
+    }
+
+    protected void ttop(final CommandSession session, Process process, String[] argv) throws Exception {
+        TTop.ttop(Shell.getTerminal(session), process.out(), process.err(), argv);
+    }
+
+    protected void nano(final CommandSession session, Process process, String[] argv) throws Exception {
+        final String[] usage = {
+                "nano -  edit files",
+                "Usage: nano [FILES]",
+                "  -? --help                    Show help",
+        };
+        Options opt = parseOptions(session, usage, argv);
+        Nano edit = new Nano(Shell.getTerminal(session), session.currentDir());
+        edit.open(opt.args());
+        edit.run();
+    }
+
+    protected void watch(final CommandSession session, Process process, String[] argv) throws Exception {
+        final String[] usage = {
+                "watch - watches & refreshes the output of a command",
+                "Usage: watch [OPTIONS] COMMAND",
+                "  -? --help                    Show help",
+                "  -n --interval                Interval between executions of the command in seconds",
+                "  -a --append                  The output should be appended but not clear the console"
+        };
+
+        Options opt = parseOptions(session, usage, argv);
+
+        List<String> args = opt.args();
+        if (args.isEmpty()) {
+            throw new IllegalArgumentException("usage: watch COMMAND");
+        }
+        ScheduledExecutorService executorService = Executors.newSingleThreadScheduledExecutor();
+        final Terminal terminal = Shell.getTerminal(session);
+        final CommandProcessor processor = Shell.getProcessor(session);
+        try {
+            int interval = 1;
+            if (opt.isSet("interval")) {
+                interval = opt.getNumber("interval");
+                if (interval < 1) {
+                    interval = 1;
+                }
+            }
+            final String cmd = String.join(" ", args);
+            Runnable task = () -> {
+                ByteArrayOutputStream baos = new ByteArrayOutputStream();
+                PrintStream os = new PrintStream(baos);
+                InputStream is = new ByteArrayInputStream(new byte[0]);
+                if (opt.isSet("append") || !terminal.puts(Capability.clear_screen)) {
+                    terminal.writer().println();
+                }
+                try {
+                    CommandSession ns = processor.createSession(is, os, os);
+                    Set<String> vars = Shell.getCommands(session);
+                    for (String n : vars) {
+                        ns.put(n, session.get(n));
+                    }
+                    ns.execute(cmd);
+                } catch (Throwable t) {
+                    t.printStackTrace(os);
+                }
+                os.flush();
+                terminal.writer().print(baos.toString());
+                terminal.writer().flush();
+            };
+            executorService.scheduleAtFixedRate(task, 0, interval, TimeUnit.SECONDS);
+            Attributes attr = terminal.enterRawMode();
+            terminal.reader().read();
+            terminal.setAttributes(attr);
+        } finally {
+            executorService.shutdownNow();
+        }
+    }
+
+    protected void less(CommandSession session, Process process, String[] argv) throws Exception {
+        String[] usage = {
+                "less -  file pager",
+                "Usage: less [OPTIONS] [FILES]",
+                "  -? --help                    Show help",
+                "  -e --quit-at-eof             Exit on second EOF",
+                "  -E --QUIT-AT-EOF             Exit on EOF",
+                "  -F --quit-if-one-screen      Exit if entire file fits on first screen",
+                "  -q --quiet --silent          Silent mode",
+                "  -Q --QUIET --SILENT          Completely  silent",
+                "  -S --chop-long-lines         Do not fold long lines",
+                "  -i --ignore-case             Search ignores lowercase case",
+                "  -I --IGNORE-CASE             Search ignores all case",
+                "  -x --tabs                    Set tab stops",
+                "  -N --LINE-NUMBERS            Display line number for each line",
+                "     --no-init                 Disable terminal initialization",
+                "     --no-keypad               Disable keypad handling"
+        };
+        boolean hasExtendedOptions = false;
+        try {
+            Less.class.getField("quitIfOneScreen");
+            hasExtendedOptions = true;
+        } catch (NoSuchFieldException e) {
+            List<String> ustrs = new ArrayList<>(Arrays.asList(usage));
+            ustrs.removeIf(s -> s.contains("--quit-if-one-screen") || s.contains("--no-init") || s.contains("--no-keypad"));
+            usage = ustrs.toArray(new String[ustrs.size()]);
+        }
+        Options opt = parseOptions(session, usage, argv);
+        List<Source> sources = new ArrayList<>();
+        if (opt.args().isEmpty()) {
+            opt.args().add("-");
+        }
+        for (String arg : opt.args()) {
+            if ("-".equals(arg)) {
+                sources.add(new StdInSource(process));
+            } else {
+                sources.add(new PathSource(session.currentDir().resolve(arg), arg));
+            }
+        }
+
+        if (!process.isTty(1)) {
+            for (Source source : sources) {
+                try (BufferedReader reader = new BufferedReader(new InputStreamReader(source.read()))) {
+                    cat(process, reader, opt.isSet("LINE-NUMBERS"));
+                }
+            }
+            return;
+        }
+
+        Less less = new Less(Shell.getTerminal(session));
+        less.quitAtFirstEof = opt.isSet("QUIT-AT-EOF");
+        less.quitAtSecondEof = opt.isSet("quit-at-eof");
+        less.quiet = opt.isSet("quiet");
+        less.veryQuiet = opt.isSet("QUIET");
+        less.chopLongLines = opt.isSet("chop-long-lines");
+        less.ignoreCaseAlways = opt.isSet("IGNORE-CASE");
+        less.ignoreCaseCond = opt.isSet("ignore-case");
+        if (opt.isSet("tabs")) {
+            less.tabs = opt.getNumber("tabs");
+        }
+        less.printLineNumbers = opt.isSet("LINE-NUMBERS");
+        if (hasExtendedOptions) {
+            Less.class.getField("quitIfOneScreen").set(less, opt.isSet("quit-if-one-screen"));
+            Less.class.getField("noInit").set(less, opt.isSet("no-init"));
+            Less.class.getField("noKeypad").set(less, opt.isSet("no-keypad"));
+        }
+        less.run(sources);
+    }
+
+    protected void sort(CommandSession session, Process process, String[] argv) throws Exception {
+        final String[] usage = {
+                "sort -  writes sorted standard input to standard output.",
+                "Usage: sort [OPTIONS] [FILES]",
+                "  -? --help                    show help",
+                "  -f --ignore-case             fold lower case to upper case characters",
+                "  -r --reverse                 reverse the result of comparisons",
+                "  -u --unique                  output only the first of an equal run",
+                "  -t --field-separator=SEP     use SEP instead of non-blank to blank transition",
+                "  -b --ignore-leading-blanks   ignore leading blancks",
+                "     --numeric-sort            compare according to string numerical value",
+                "  -k --key=KEY                 fields to use for sorting separated by whitespaces"};
+
+        Options opt = parseOptions(session, usage, argv);
+
+        List<String> args = opt.args();
+
+        List<String> lines = new ArrayList<>();
+        if (!args.isEmpty()) {
+            for (String filename : args) {
+                try (BufferedReader reader = new BufferedReader(new InputStreamReader(
+                        session.currentDir().toUri().resolve(filename).toURL().openStream()))) {
+                    read(reader, lines);
+                }
+            }
+        } else {
+            BufferedReader r = new BufferedReader(new InputStreamReader(process.in()));
+            read(r, lines);
+        }
+
+        String separator = opt.get("field-separator");
+        boolean caseInsensitive = opt.isSet("ignore-case");
+        boolean reverse = opt.isSet("reverse");
+        boolean ignoreBlanks = opt.isSet("ignore-leading-blanks");
+        boolean numeric = opt.isSet("numeric-sort");
+        boolean unique = opt.isSet("unique");
+        List<String> sortFields = opt.getList("key");
+
+        char sep = (separator == null || separator.length() == 0) ? '\0' : separator.charAt(0);
+        lines.sort(new SortComparator(caseInsensitive, reverse, ignoreBlanks, numeric, sep, sortFields));
+        String last = null;
+        for (String s : lines) {
+            if (!unique || last == null || !s.equals(last)) {
+                process.out().println(s);
+            }
+            last = s;
+        }
+    }
+
+    protected void pwd(CommandSession session, Process process, String[] argv) throws Exception {
+        final String[] usage = {
+                "pwd - get current directory",
+                "Usage: pwd [OPTIONS]",
+                "  -? --help                show help"
+        };
+        Options opt = parseOptions(session, usage, argv);
+        if (!opt.args().isEmpty()) {
+            throw new IllegalArgumentException("usage: pwd");
+        }
+        process.out().println(session.currentDir());
+    }
+
+    protected void cd(CommandSession session, Process process, String[] argv) throws Exception {
+        final String[] usage = {
+                "cd - get current directory",
+                "Usage: cd [OPTIONS] DIRECTORY",
+                "  -? --help                show help"
+        };
+        Options opt = parseOptions(session, usage, argv);
+        if (opt.args().size() != 1) {
+            throw new IllegalArgumentException("usage: cd DIRECTORY");
+        }
+        Path cwd = session.currentDir();
+        cwd = cwd.resolve(opt.args().get(0)).toAbsolutePath();
+        if (!Files.exists(cwd)) {
+            throw new IOException("no such file or directory: " + opt.args().get(0));
+        } else if (!Files.isDirectory(cwd)) {
+            throw new IOException("not a directory: " + opt.args().get(0));
+        }
+        session.currentDir(cwd);
+    }
+
+    protected void ls(CommandSession session, Process process, String[] argv) throws Exception {
+        final String[] usage = {
+                "ls - list files",
+                "Usage: ls [OPTIONS] [PATTERNS...]",
+                "  -? --help                show help",
+                "  -1                       list one entry per line",
+                "  -C                       multi-column output",
+                "     --color=WHEN          colorize the output, may be `always', `never' or `auto'",
+                "  -a                       list entries starting with .",
+                "  -F                       append file type indicators",
+                "  -m                       comma separated",
+                "  -l                       long listing",
+                "  -S                       sort by size",
+                "  -f                       output is not sorted",
+                "  -r                       reverse sort order",
+                "  -t                       sort by modification time",
+                "  -x                       sort horizontally",
+                "  -L                       list referenced file for links",
+                "  -h                       print sizes in human readable form"
+        };
+        Options opt = parseOptions(session, usage, argv);
+        String color = opt.isSet("color") ? opt.get("color") : "auto";
+        boolean colored;
+        switch (color) {
+            case "always":
+            case "yes":
+            case "force":
+                colored = true;
+                break;
+            case "never":
+            case "no":
+            case "none":
+                colored = false;
+                break;
+            case "auto":
+            case "tty":
+            case "if-tty":
+                colored = process.isTty(1);
+                break;
+            default:
+                throw new IllegalArgumentException("invalid argument ‘" + color + "’ for ‘--color’");
+        }
+        Map<String, String> colors = colored ? getLsColorMap(session) : Collections.emptyMap();
+
+        class PathEntry implements Comparable<PathEntry> {
+            final Path abs;
+            final Path path;
+            final Map<String, Object> attributes;
+
+            public PathEntry(Path abs, Path root) {
+                this.abs = abs;
+                this.path = abs.startsWith(root) ? root.relativize(abs) : abs;
+                this.attributes = readAttributes(abs);
+            }
+
+            @Override
+            public int compareTo(PathEntry o) {
+                int c = doCompare(o);
+                return opt.isSet("r") ? -c : c;
+            }
+
+            private int doCompare(PathEntry o) {
+                if (opt.isSet("f")) {
+                    return -1;
+                }
+                if (opt.isSet("S")) {
+                    long s0 = attributes.get("size") != null ? ((Number) attributes.get("size")).longValue() : 0L;
+                    long s1 = o.attributes.get("size") != null ? ((Number) o.attributes.get("size")).longValue() : 0L;
+                    return s0 > s1 ? -1 : s0 < s1 ? 1 : path.toString().compareTo(o.path.toString());
+                }
+                if (opt.isSet("t")) {
+                    long t0 = attributes.get("lastModifiedTime") != null ? ((FileTime) attributes.get("lastModifiedTime")).toMillis() : 0L;
+                    long t1 = o.attributes.get("lastModifiedTime") != null ? ((FileTime) o.attributes.get("lastModifiedTime")).toMillis() : 0L;
+                    return t0 > t1 ? -1 : t0 < t1 ? 1 : path.toString().compareTo(o.path.toString());
+                }
+                return path.toString().compareTo(o.path.toString());
+            }
+
+            boolean isNotDirectory() {
+                return is("isRegularFile") || is("isSymbolicLink") || is("isOther");
+            }
+
+            boolean isDirectory() {
+                return is("isDirectory");
+            }
+
+            private boolean is(String attr) {
+                Object d = attributes.get(attr);
+                return d instanceof Boolean && (Boolean) d;
+            }
+
+            String display() {
+                String type;
+                String suffix;
+                String link = "";
+                if (is("isSymbolicLink")) {
+                    type = "sl";
+                    suffix = "@";
+                    try {
+                        Path l = Files.readSymbolicLink(abs);
+                        link = " -> " + l.toString();
+                    } catch (IOException e) {
+                        // ignore
+                    }
+                } else if (is("isDirectory")) {
+                    type = "dr";
+                    suffix = "/";
+                } else if (is("isExecutable")) {
+                    type = "ex";
+                    suffix = "*";
+                } else if (is("isOther")) {
+                    type = "ot";
+                    suffix = "";
+                } else {
+                    type = "";
+                    suffix = "";
+                }
+                boolean addSuffix = opt.isSet("F");
+                return applyStyle(path.toString(), colors, type)
+                        + (addSuffix ? suffix : "") + link;
+            }
+
+            String longDisplay() {
+                String username;
+                if (attributes.containsKey("owner")) {
+                    username = Objects.toString(attributes.get("owner"), null);
+                } else {
+                    username = "owner";
+                }
+                if (username.length() > 8) {
+                    username = username.substring(0, 8);
+                } else {
+                    for (int i = username.length(); i < 8; i++) {
+                        username = username + " ";
+                    }
+                }
+                String group;
+                if (attributes.containsKey("group")) {
+                    group = Objects.toString(attributes.get("group"), null);
+                } else {
+                    group = "group";
+                }
+                if (group.length() > 8) {
+                    group = group.substring(0, 8);
+                } else {
+                    for (int i = group.length(); i < 8; i++) {
+                        group = group + " ";
+                    }
+                }
+                Number length = (Number) attributes.get("size");
+                if (length == null) {
+                    length = 0L;
+                }
+                String lengthString;
+                if (opt.isSet("h")) {
+                    double l = length.longValue();
+                    String unit = "B";
+                    if (l >= 1000) {
+                         l /= 1024;
+                        unit = "K";
+                        if (l >= 1000) {
+                            l /= 1024;
+                            unit = "M";
+                            if (l >= 1000) {
+                                l /= 1024;
+                                unit = "T";
+                            }
+                        }
+                    }
+                    if (l < 10 && length.longValue() > 1000) {
+                        lengthString = String.format("%.1f", l) + unit;
+                    } else {
+                        lengthString = String.format("%3.0f", l) + unit;
+                    }
+                } else {
+                    lengthString = String.format("%1$8s", length);
+                }
+                @SuppressWarnings("unchecked")
+                Set<PosixFilePermission> perms = (Set<PosixFilePermission>) attributes.get("permissions");
+                if (perms == null) {
+                    perms = EnumSet.noneOf(PosixFilePermission.class);
+                }
+                // TODO: all fields should be padded to align
+                return (is("isDirectory") ? "d" : (is("isSymbolicLink") ? "l" : (is("isOther") ? "o" : "-")))
+                        + PosixFilePermissions.toString(perms) + " "
+                        + String.format("%3s", (attributes.containsKey("nlink") ? attributes.get("nlink").toString() : "1"))
+                        + " " + username + " " + group + " " + lengthString + " "
+                        + toString((FileTime) attributes.get("lastModifiedTime"))
+                        + " " + display();
+            }
+
+            protected String toString(FileTime time) {
+                long millis = (time != null) ? time.toMillis() : -1L;
+                if (millis < 0L) {
+                    return "------------";
+                }
+                ZonedDateTime dt = Instant.ofEpochMilli(millis).atZone(ZoneId.systemDefault());
+                // Less than six months
+                if (System.currentTimeMillis() - millis < 183L * 24L * 60L * 60L * 1000L) {
+                    return DateTimeFormatter.ofPattern("MMM ppd HH:mm").format(dt);
+                }
+                // Older than six months
+                else {
+                    return DateTimeFormatter.ofPattern("MMM ppd  yyyy").format(dt);
+                }
+            }
+
+            protected Map<String, Object> readAttributes(Path path) {
+                Map<String, Object>  attrs = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
+                for (String view : path.getFileSystem().supportedFileAttributeViews()) {
+                    try {
+                        Map<String, Object> ta = Files.readAttributes(path, view + ":*",
+                                getLinkOptions(opt.isSet("L")));
+                        ta.forEach(attrs::putIfAbsent);
+                    } catch (IOException e) {
+                        // Ignore
+                    }
+                }
+                attrs.computeIfAbsent("isExecutable", s -> Files.isExecutable(path));
+                attrs.computeIfAbsent("permissions", s -> getPermissionsFromFile(path.toFile()));
+                return attrs;
+            }
+        }
+
+        Path currentDir = session.currentDir();
+        // Listing
+        List<Path> expanded = new ArrayList<>();
+        if (opt.args().isEmpty()) {
+            expanded.add(currentDir);
+        } else {
+            opt.args().forEach(s -> expanded.add(currentDir.resolve(s)));
+        }
+        boolean listAll = opt.isSet("a");
+        Predicate<Path> filter = p -> listAll || !p.getFileName().toString().startsWith(".");
+        List<PathEntry> all = expanded.stream()
+                .filter(filter)
+                .map(p -> new PathEntry(p, currentDir))
+                .sorted()
+                .collect(Collectors.toList());
+        // Print files first
+        List<PathEntry> files = all.stream()
+                .filter(PathEntry::isNotDirectory)
+                .collect(Collectors.toList());
+        PrintStream out = process.out();
+        Consumer<Stream<PathEntry>> display = s -> {
+            boolean optLine  = opt.isSet("1");
+            boolean optComma = opt.isSet("m");
+            boolean optLong  = opt.isSet("l");
+            boolean optCol   = opt.isSet("C");
+            if (!optLine && !optComma && !optLong && !optCol) {
+                if (process.isTty(1)) {
+                    optCol = true;
+                }
+                else {
+                    optLine = true;
+                }
+            }
+            // One entry per line
+            if (optLine) {
+                s.map(PathEntry::display).forEach(out::println);
+            }
+            // Comma separated list
+            else if (optComma) {
+                out.println(s.map(PathEntry::display).collect(Collectors.joining(", ")));
+            }
+            // Long listing
+            else if (optLong) {
+                s.map(PathEntry::longDisplay).forEach(out::println);
+            }
+            // Column listing
+            else if (optCol) {
+                toColumn(session, process, out, s.map(PathEntry::display), opt.isSet("x"));
+            }
+        };
+        boolean space = false;
+        if (!files.isEmpty()) {
+            display.accept(files.stream());
+            space = true;
+        }
+        // Print directories
+        List<PathEntry> directories = all.stream()
+                .filter(PathEntry::isDirectory)
+                .collect(Collectors.toList());
+        for (PathEntry entry : directories) {
+            if (space) {
+                out.println();
+            }
+            space = true;
+            Path path = currentDir.resolve(entry.path);
+            if (expanded.size() > 1) {
+                out.println(currentDir.relativize(path).toString() + ":");
+            }
+            display.accept(Stream.concat(Stream.of(".", "..").map(path::resolve), Files.list(path))
+                            .filter(filter)
+                            .map(p -> new PathEntry(p, path))
+                            .sorted()
+            );
+        }
+    }
+
+    private void toColumn(CommandSession session, Process process, PrintStream out, Stream<String> ansi, boolean horizontal) {
+        Terminal terminal = Shell.getTerminal(session);
+        int width = process.isTty(1) ? terminal.getWidth() : 80;
+        List<AttributedString> strings = ansi.map(AttributedString::fromAnsi).collect(Collectors.toList());
+        if (!strings.isEmpty()) {
+            int max = strings.stream().mapToInt(AttributedString::columnLength).max().getAsInt();
+            int c = Math.max(1, width / max);
+            while (c > 1 && c * max + (c - 1) >= width) {
+                c--;
+            }
+            int columns = c;
+            int lines = (strings.size() + columns - 1) / columns;
+            IntBinaryOperator index;
+            if (horizontal) {
+                index = (i, j) -> i * columns + j;
+            } else {
+                index = (i, j) -> j * lines + i;
+            }
+            AttributedStringBuilder sb = new AttributedStringBuilder();
+            for (int i = 0; i < lines; i++) {
+                for (int j = 0; j < columns; j++) {
+                    int idx = index.applyAsInt(i, j);
+                    if (idx < strings.size()) {
+                        AttributedString str = strings.get(idx);
+                        boolean hasRightItem = j < columns - 1 && index.applyAsInt(i, j + 1) < strings.size();
+                        sb.append(str);
+                        if (hasRightItem) {
+                            for (int k = 0; k <= max - str.length(); k++) {
+                                sb.append(' ');
+                            }
+                        }
+                    }
+                }
+                sb.append('\n');
+            }
+            out.print(sb.toAnsi(terminal));
+        }
+    }
+
+    protected void cat(CommandSession session, Process process, String[] argv) throws Exception {
+        final String[] usage = {
+                "cat - concatenate and print FILES",
+                "Usage: cat [OPTIONS] [FILES]",
+                "  -? --help                show help",
+                "  -n                       number the output lines, starting at 1"
+        };
+        Options opt = parseOptions(session, usage, argv);
+        List<String> args = opt.args();
+        if (args.isEmpty()) {
+            args = Collections.singletonList("-");
+        }
+        Path cwd = session.currentDir();
+        for (String arg : args) {
+            InputStream is;
+            if ("-".equals(arg)) {
+                is = process.in();
+            } else {
+                is = cwd.toUri().resolve(arg).toURL().openStream();
+            }
+            cat(process, new BufferedReader(new InputStreamReader(is)), opt.isSet("n"));
+        }
+    }
+
+    protected void echo(CommandSession session, Process process, Object[] argv) throws Exception {
+        final String[] usage = {
+                "echo - echoes or prints ARGUMENT to standard output",
+                "Usage: echo [OPTIONS] [ARGUMENTS]",
+                "  -? --help                show help",
+                "  -n                       no trailing new line"
+        };
+        Options opt = parseOptions(session, usage, argv);
+        List<String> args = opt.args();
+        StringBuilder buf = new StringBuilder();
+        if (args != null) {
+            for (String arg : args) {
+                if (buf.length() > 0)
+                    buf.append(' ');
+                for (int i = 0; i < arg.length(); i++) {
+                    int c = arg.charAt(i);
+                    int ch;
+                    if (c == '\\') {
+                        c = i < arg.length() - 1 ? arg.charAt(++i) : '\\';
+                        switch (c) {
+                            case 'a':
+                                buf.append('\u0007');
+                                break;
+                            case 'n':
+                                buf.append('\n');
+                                break;
+                            case 't':
+                                buf.append('\t');
+                                break;
+                            case 'r':
+                                buf.append('\r');
+                                break;
+                            case '\\':
+                                buf.append('\\');
+                                break;
+                            case '0':
+                            case '1':
+                            case '2':
+                            case '3':
+                            case '4':
+                            case '5':
+                            case '6':
+                            case '7':
+                            case '8':
+                            case '9':
+                                ch = 0;
+                                for (int j = 0; j < 3; j++) {
+                                    c = i < arg.length() - 1 ? arg.charAt(++i) : -1;
+                                    if (c >= 0) {
+                                        ch = ch * 8 + (c - '0');
+                                    }
+                                }
+                                buf.append((char) ch);
+                                break;
+                            case 'u':
+                                ch = 0;
+                                for (int j = 0; j < 4; j++) {
+                                    c = i < arg.length() - 1 ? arg.charAt(++i) : -1;
+                                    if (c >= 0) {
+                                        if (c >= 'A' && c <= 'Z') {
+                                            ch = ch * 16 + (c - 'A' + 10);
+                                        } else if (c >= 'a' && c <= 'z') {
+                                            ch = ch * 16 + (c - 'a' + 10);
+                                        } else if (c >= '0' && c <= '9') {
+                                            ch = ch * 16 + (c - '0');
+                                        } else {
+                                            break;
+                                        }
+                                    }
+                                }
+                                buf.append((char) ch);
+                                break;
+                            default:
+                                buf.append((char) c);
+                                break;
+                        }
+                    } else {
+                        buf.append((char) c);
+                    }
+                }
+            }
+        }
+        if (opt.isSet("n")) {
+            process.out().print(buf);
+        } else {
+            process.out().println(buf);
+        }
+    }
+
+    protected void grep(CommandSession session, Process process, String[] argv) throws Exception {
+        final String[] usage = {
+                "grep -  search for PATTERN in each FILE or standard input.",
+                "Usage: grep [OPTIONS] PATTERN [FILES]",
+                "  -? --help                Show help",
+                "  -i --ignore-case         Ignore case distinctions",
+                "  -n --line-number         Prefix each line with line number within its input file",
+                "  -q --quiet, --silent     Suppress all normal output",
+                "  -v --invert-match        Select non-matching lines",
+                "  -w --word-regexp         Select only whole words",
+                "  -x --line-regexp         Select only whole lines",
+                "  -c --count               Only print a count of matching lines per file",
+                "     --color=WHEN          Use markers to distinguish the matching string, may be `always', `never' or `auto'",
+                "  -B --before-context=NUM  Print NUM lines of leading context before matching lines",
+                "  -A --after-context=NUM   Print NUM lines of trailing context after matching lines",
+                "  -C --context=NUM         Print NUM lines of output context",
+                "     --pad-lines           Pad line numbers"
+        };
+        Options opt = parseOptions(session, usage, argv);
+        List<String> args = opt.args();
+        if (args.isEmpty()) {
+            throw new IllegalArgumentException("no pattern supplied");
+        }
+
+        String regex = args.remove(0);
+        String regexp = regex;
+        if (opt.isSet("word-regexp")) {
+            regexp = "\\b" + regexp + "\\b";
+        }
+        if (opt.isSet("line-regexp")) {
+            regexp = "^" + regexp + "$";
+        } else {
+            regexp = ".*" + regexp + ".*";
+        }
+        Pattern p;
+        Pattern p2;
+        if (opt.isSet("ignore-case")) {
+            p = Pattern.compile(regexp, Pattern.CASE_INSENSITIVE);
+            p2 = Pattern.compile(regex, Pattern.CASE_INSENSITIVE);
+        } else {
+            p = Pattern.compile(regexp);
+            p2 = Pattern.compile(regex);
+        }
+        int after = opt.isSet("after-context") ? opt.getNumber("after-context") : -1;
+        int before = opt.isSet("before-context") ? opt.getNumber("before-context") : -1;
+        int context = opt.isSet("context") ? opt.getNumber("context") : 0;
+        String lineFmt = opt.isSet("pad-lines") ? "%6d" : "%d";
+        if (after < 0) {
+            after = context;
+        }
+        if (before < 0) {
+            before = context;
+        }
+        List<String> lines = new ArrayList<>();
+        boolean invertMatch = opt.isSet("invert-match");
+        boolean lineNumber = opt.isSet("line-number");
+        boolean count = opt.isSet("count");
+        String color = opt.isSet("color") ? opt.get("color") : "auto";
+        boolean colored;
+        switch (color) {
+            case "always":
+            case "yes":
+            case "force":
+                colored = true;
+                break;
+            case "never":
+            case "no":
+            case "none":
+                colored = false;
+                break;
+            case "auto":
+            case "tty":
+            case "if-tty":
+                colored = process.isTty(1);
+                break;
+            default:
+                throw new IllegalArgumentException("invalid argument ‘" + color + "’ for ‘--color’");
+        }
+        Map<String, String> colors = colored ? getColorMap(session, "GREP", DEFAULT_GREP_COLORS) : Collections.emptyMap();
+
+        List<Source> sources = new ArrayList<>();
+        if (opt.args().isEmpty()) {
+            opt.args().add("-");
+        }
+        for (String arg : opt.args()) {
+            if ("-".equals(arg)) {
+                sources.add(new StdInSource(process));
+            } else {
+                sources.add(new PathSource(session.currentDir().resolve(arg), arg));
+            }
+        }
+        boolean match = false;
+        for (Source source : sources) {
+            boolean firstPrint = true;
+            int nb = 0;
+            int lineno = 1;
+            String line;
+            int lineMatch = 0;
+            try (BufferedReader r = new BufferedReader(new InputStreamReader(source.read()))) {
+                while ((line = r.readLine()) != null) {
+                    if (line.length() == 1 && line.charAt(0) == '\n') {
+                        break;
+                    }
+                    boolean matches = p.matcher(line).matches();
+                    AttributedStringBuilder sbl = new AttributedStringBuilder();
+                    if (!count) {
+                        if (sources.size() > 1) {
+                            if (colored) {
+                                applyStyle(sbl, colors, "fn");
+                            }
+                            sbl.append(source.getName());
+                            if (colored) {
+                                applyStyle(sbl, colors, "se");
+                            }
+                            sbl.append(":");
+                        }
+                        if (lineNumber) {
+                            if (colored) {
+                                applyStyle(sbl, colors, "ln");
+                            }
+                            sbl.append(String.format(lineFmt, lineno));
+                            if (colored) {
+                                applyStyle(sbl, colors, "se");
+                            }
+                            sbl.append((matches ^ invertMatch) ? ":" : "-");
+                        }
+                        String style = matches ^ invertMatch ^ (invertMatch && colors.containsKey("rv"))
+                                ? "sl" : "cx";
+                        if (colored) {
+                            applyStyle(sbl, colors, style);
+                        }
+                        AttributedString aLine = AttributedString.fromAnsi(line);
+                        Matcher matcher2 = p2.matcher(aLine.toString());
+                        int cur = 0;
+                        while (matcher2.find()) {
+                            int index = matcher2.start(0);
+                            AttributedString prefix = aLine.subSequence(cur, index);
+                            sbl.append(prefix);
+                            cur = matcher2.end();
+                            if (colored) {
+                                applyStyle(sbl, colors, invertMatch ? "mc" : "ms", "mt");
+                            }
+                            sbl.append(aLine.subSequence(index, cur));
+                            if (colored) {
+                                applyStyle(sbl, colors, style);
+                            }
+                            nb++;
+                        }
+                        sbl.append(aLine.subSequence(cur, aLine.length()));
+                    }
+                    if (matches ^ invertMatch) {
+                        lines.add(sbl.toAnsi(Shell.getTerminal(session)));
+                        lineMatch = lines.size();
+                    } else {
+                        if (lineMatch != 0 & lineMatch + after + before <= lines.size()) {
+                            if (!count) {
+                                if (!firstPrint && before + after > 0) {
+                                    AttributedStringBuilder sbl2 = new AttributedStringBuilder();
+                                    if (colored) {
+                                        applyStyle(sbl2, colors, "se");
+                                    }
+                                    sbl2.append("--");
+                                    process.out().println(sbl2.toAnsi(Shell.getTerminal(session)));
+                                } else {
+                                    firstPrint = false;
+                                }
+                                for (int i = 0; i < lineMatch + after; i++) {
+                                    process.out().println(lines.get(i));
+                                }
+                            }
+                            while (lines.size() > before) {
+                                lines.remove(0);
+                            }
+                            lineMatch = 0;
+                        }
+                        lines.add(sbl.toAnsi(Shell.getTerminal(session)));
+                        while (lineMatch == 0 && lines.size() > before) {
+                            lines.remove(0);
+                        }
+                    }
+                    lineno++;
+                }
+                if (!count && lineMatch > 0) {
+                    if (!firstPrint && before + after > 0) {
+                        AttributedStringBuilder sbl2 = new AttributedStringBuilder();
+                        if (colored) {
+                            applyStyle(sbl2, colors, "se");
+                        }
+                        sbl2.append("--");
+                        process.out().println(sbl2.toAnsi(Shell.getTerminal(session)));
+                    } else {
+                        firstPrint = false;
+                    }
+                    for (int i = 0; i < lineMatch + after && i < lines.size(); i++) {
+                        process.out().println(lines.get(i));
+                    }
+                }
+                if (count) {
+                    process.out().println(nb);
+                }
+                match |= nb > 0;
+            }
+        }
+        Process.Utils.current().error(match ? 0 : 1);
+    }
+
+    protected void sleep(CommandSession session, Process process, String[] argv) throws Exception {
+        final String[] usage = {
+                "sleep -  suspend execution for an interval of time",
+                "Usage: sleep seconds",
+                "  -? --help                    show help"};
+
+        Options opt = parseOptions(session, usage, argv);
+        List<String> args = opt.args();
+        if (args.size() != 1) {
+            throw new IllegalArgumentException("usage: sleep seconds");
+        } else {
+            int s = Integer.parseInt(args.get(0));
+            Thread.sleep(s * 1000);
+        }
+    }
+
+    protected static void read(BufferedReader r, List<String> lines) throws IOException {
+        for (String s = r.readLine(); s != null; s = r.readLine()) {
+            lines.add(s);
+        }
+    }
+
+    private static void cat(Process process, final BufferedReader reader, boolean displayLineNumbers) throws IOException {
+        String line;
+        int lineno = 1;
+        try {
+            while ((line = reader.readLine()) != null) {
+                if (displayLineNumbers) {
+                    process.out().print(String.format("%6d  ", lineno++));
+                }
+                process.out().println(line);
+            }
+        } finally {
+            reader.close();
+        }
+    }
+
+    public static class SortComparator implements Comparator<String> {
+
+        private static Pattern fpPattern;
+
+        static {
+            final String Digits = "(\\p{Digit}+)";
+            final String HexDigits = "(\\p{XDigit}+)";
+            final String Exp = "[eE][+-]?" + Digits;
+            final String fpRegex = "([\\x00-\\x20]*[+-]?(NaN|Infinity|(((" + Digits + "(\\.)?(" + Digits + "?)(" + Exp + ")?)|(\\.(" + Digits + ")(" + Exp + ")?)|(((0[xX]" + HexDigits + "(\\.)?)|(0[xX]" + HexDigits + "?(\\.)" + HexDigits + "))[pP][+-]?" + Digits + "))" + "[fFdD]?))[\\x00-\\x20]*)(.*)";
+            fpPattern = Pattern.compile(fpRegex);
+        }
+
+        private boolean caseInsensitive;
+        private boolean reverse;
+        private boolean ignoreBlanks;
+        private boolean numeric;
+        private char separator;
+        private List<Key> sortKeys;
+
+        public SortComparator(boolean caseInsensitive,
+                              boolean reverse,
+                              boolean ignoreBlanks,
+                              boolean numeric,
+                              char separator,
+                              List<String> sortFields) {
+            this.caseInsensitive = caseInsensitive;
+            this.reverse = reverse;
+            this.separator = separator;
+            this.ignoreBlanks = ignoreBlanks;
+            this.numeric = numeric;
+            if (sortFields == null || sortFields.size() == 0) {
+                sortFields = new ArrayList<>();
+                sortFields.add("1");
+            }
+            sortKeys = sortFields.stream().map(Key::new).collect(Collectors.toList());
+        }
+
+        public int compare(String o1, String o2) {
+            int res = 0;
+
+            List<Integer> fi1 = getFieldIndexes(o1);
+            List<Integer> fi2 = getFieldIndexes(o2);
+            for (Key key : sortKeys) {
+                int[] k1 = getSortKey(o1, fi1, key);
+                int[] k2 = getSortKey(o2, fi2, key);
+                if (key.numeric) {
+                    Double d1 = getDouble(o1, k1[0], k1[1]);
+                    Double d2 = getDouble(o2, k2[0], k2[1]);
+                    res = d1.compareTo(d2);
+                } else {
+                    res = compareRegion(o1, k1[0], k1[1], o2, k2[0], k2[1], key.caseInsensitive);
+                }
+                if (res != 0) {
+                    if (key.reverse) {
+                        res = -res;
+                    }
+                    break;
+                }
+            }
+            return res;
+        }
+
+        protected Double getDouble(String s, int start, int end) {
+            Matcher m = fpPattern.matcher(s.substring(start, end));
+            m.find();
+            return new Double(s.substring(0, m.end(1)));
+        }
+
+        protected int compareRegion(String s1, int start1, int end1, String s2, int start2, int end2, boolean caseInsensitive) {
+            for (int i1 = start1, i2 = start2; i1 < end1 && i2 < end2; i1++, i2++) {
+                char c1 = s1.charAt(i1);
+                char c2 = s2.charAt(i2);
+                if (c1 != c2) {
+                    if (caseInsensitive) {
+                        c1 = Character.toUpperCase(c1);
+                        c2 = Character.toUpperCase(c2);
+                        if (c1 != c2) {
+                            c1 = Character.toLowerCase(c1);
+                            c2 = Character.toLowerCase(c2);
+                            if (c1 != c2) {
+                                return c1 - c2;
+                            }
+                        }
+                    } else {
+                        return c1 - c2;
+                    }
+                }
+            }
+            return end1 - end2;
+        }
+
+        protected int[] getSortKey(String str, List<Integer> fields, Key key) {
+            int start;
+            int end;
+            if (key.startField * 2 <= fields.size()) {
+                start = fields.get((key.startField - 1) * 2);
+                if (key.ignoreBlanksStart) {
+                    while (start < fields.get((key.startField - 1) * 2 + 1) && Character.isWhitespace(str.charAt(start))) {
+                        start++;
+                    }
+                }
+                if (key.startChar > 0) {
+                    start = Math.min(start + key.startChar - 1, fields.get((key.startField - 1) * 2 + 1));
+                }
+            } else {
+                start = 0;
+            }
+            if (key.endField > 0 && key.endField * 2 <= fields.size()) {
+                end = fields.get((key.endField - 1) * 2);
+                if (key.ignoreBlanksEnd) {
+                    while (end < fields.get((key.endField - 1) * 2 + 1) && Character.isWhitespace(str.charAt(end))) {
+                        end++;
+                    }
+                }
+                if (key.endChar > 0) {
+                    end = Math.min(end + key.endChar - 1, fields.get((key.endField - 1) * 2 + 1));
+                }
+            } else {
+                end = str.length();
+            }
+            return new int[]{start, end};
+        }
+
+        protected List<Integer> getFieldIndexes(String o) {
+            List<Integer> fields = new ArrayList<>();
+            if (o.length() > 0) {
+                if (separator == '\0') {
+                    fields.add(0);
+                    for (int idx = 1; idx < o.length(); idx++) {
+                        if (Character.isWhitespace(o.charAt(idx)) && !Character.isWhitespace(o.charAt(idx - 1))) {
+                            fields.add(idx - 1);
+                            fields.add(idx);
+                        }
+                    }
+                    fields.add(o.length() - 1);
+                } else {
+                    int last = -1;
+                    for (int idx = o.indexOf(separator); idx >= 0; idx = o.indexOf(separator, idx + 1)) {
+                        if (last >= 0) {
+                            fields.add(last);
+                            fields.add(idx - 1);
+                        } else if (idx > 0) {
+                            fields.add(0);
+                            fields.add(idx - 1);
+                        }
+                        last = idx + 1;
+                    }
+                    if (last < o.length()) {
+                        fields.add(last < 0 ? 0 : last);
+                        fields.add(o.length() - 1);
+                    }
+                }
+            }
+            return fields;
+        }
+
+        public class Key {
+            int startField;
+            int startChar;
+            int endField;
+            int endChar;
+            boolean ignoreBlanksStart;
+            boolean ignoreBlanksEnd;
+            boolean caseInsensitive;
+            boolean reverse;
+            boolean numeric;
+
+            public Key(String str) {
+                boolean modifiers = false;
+                boolean startPart = true;
+                boolean inField = true;
+                boolean inChar = false;
+                for (char c : str.toCharArray()) {
+                    switch (c) {
+                        case '0':
+                        case '1':
+                        case '2':
+                        case '3':
+                        case '4':
+                        case '5':
+                        case '6':
+                        case '7':
+                        case '8':
+                        case '9':
+                            if (!inField && !inChar) {
+                                throw new IllegalArgumentException("Bad field syntax: " + str);
+                            }
+                            if (startPart) {
+                                if (inChar) {
+                                    startChar = startChar * 10 + (c - '0');
+                                } else {
+                                    startField = startField * 10 + (c - '0');
+                                }
+                            } else {
+                                if (inChar) {
+                                    endChar = endChar * 10 + (c - '0');
+                                } else {
+                                    endField = endField * 10 + (c - '0');
+                                }
+                            }
+                            break;
+                        case '.':
+                            if (!inField) {
+                                throw new IllegalArgumentException("Bad field syntax: " + str);
+                            }
+                            inField = false;
+                            inChar = true;
+                            break;
+                        case 'n':
+                            inField = false;
+                            inChar = false;
+                            modifiers = true;
+                            numeric = true;
+                            break;
+                        case 'f':
+                            inField = false;
+                            inChar = false;
+                            modifiers = true;
+                            caseInsensitive = true;
+                            break;
+                        case 'r':
+                            inField = false;
+                            inChar = false;
+                            modifiers = true;
+                            reverse = true;
+                            break;
+                        case 'b':
+                            inField = false;
+                            inChar = false;
+                            modifiers = true;
+                            if (startPart) {
+                                ignoreBlanksStart = true;
+                            } else {
+                                ignoreBlanksEnd = true;
+                            }
+                            break;
+                        case ',':
+                            inField = true;
+                            inChar = false;
+                            startPart = false;
+                            break;
+                        default:
+                            throw new IllegalArgumentException("Bad field syntax: " + str);
+                    }
+                }
+                if (!modifiers) {
+                    ignoreBlanksStart = ignoreBlanksEnd = SortComparator.this.ignoreBlanks;
+                    reverse = SortComparator.this.reverse;
+                    caseInsensitive = SortComparator.this.caseInsensitive;
+                    numeric = SortComparator.this.numeric;
+                }
+                if (startField < 1) {
+                    throw new IllegalArgumentException("Bad field syntax: " + str);
+                }
+            }
+        }
+    }
+
+    private static LinkOption[] getLinkOptions(boolean followLinks) {
+        if (followLinks) {
+            return EMPTY_LINK_OPTIONS;
+        } else {    // return a clone that modifications to the array will not affect others
+            return NO_FOLLOW_OPTIONS.clone();
+        }
+    }
+
+    /**
+     * @param fileName The file name to be evaluated - ignored if {@code null}/empty
+     * @return {@code true} if the file ends in one of the {@link #WINDOWS_EXECUTABLE_EXTENSIONS}
+     */
+    private static boolean isWindowsExecutable(String fileName) {
+        if ((fileName == null) || (fileName.length() <= 0)) {
+            return false;
+        }
+        for (String suffix : WINDOWS_EXECUTABLE_EXTENSIONS) {
+            if (fileName.endsWith(suffix)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * @param f The {@link File} to be checked
+     * @return A {@link Set} of {@link PosixFilePermission}s based on whether
+     * the file is readable/writable/executable. If so, then <U>all</U> the
+     * relevant permissions are set (i.e., owner, group and others)
+     */
+    private static Set<PosixFilePermission> getPermissionsFromFile(File f) {
+        Set<PosixFilePermission> perms = EnumSet.noneOf(PosixFilePermission.class);
+        if (f.canRead()) {
+            perms.add(PosixFilePermission.OWNER_READ);
+            perms.add(PosixFilePermission.GROUP_READ);
+            perms.add(PosixFilePermission.OTHERS_READ);
+        }
+
+        if (f.canWrite()) {
+            perms.add(PosixFilePermission.OWNER_WRITE);
+            perms.add(PosixFilePermission.GROUP_WRITE);
+            perms.add(PosixFilePermission.OTHERS_WRITE);
+        }
+
+        if (f.canExecute() || (OSUtils.IS_WINDOWS && isWindowsExecutable(f.getName()))) {
+            perms.add(PosixFilePermission.OWNER_EXECUTE);
+            perms.add(PosixFilePermission.GROUP_EXECUTE);
+            perms.add(PosixFilePermission.OTHERS_EXECUTE);
+        }
+
+        return perms;
+    }
+
+    public static Map<String, String> getLsColorMap(CommandSession session) {
+        return getColorMap(session, "LS", DEFAULT_LS_COLORS);
+    }
+
+    public static Map<String, String> getColorMap(CommandSession session, String name, String def) {
+        Object obj = session.get(name + "_COLORS");
+        String str = obj != null ? obj.toString() : null;
+        if (str == null) {
+            str = def;
+        }
+        String sep = str.matches("[a-z]{2}=[0-9]*(;[0-9]+)*(:[a-z]{2}=[0-9]*(;[0-9]+)*)*") ? ":" : " ";
+        return Arrays.stream(str.split(sep))
+                .collect(Collectors.toMap(s -> s.substring(0, s.indexOf('=')),
+                                          s -> s.substring(s.indexOf('=') + 1)));
+    }
+
+    static String applyStyle(String text, Map<String, String> colors, String... types) {
+        String t = null;
+        for (String type : types) {
+            if (colors.get(type) != null) {
+                t = type;
+                break;
+            }
+        }
+        return new AttributedString(text, new StyleResolver(colors::get).resolve("." + t))
+                .toAnsi();
+    }
+
+    static void applyStyle(AttributedStringBuilder sb, Map<String, String> colors, String... types) {
+        String t = null;
+        for (String type : types) {
+            if (colors.get(type) != null) {
+                t = type;
+                break;
+            }
+        }
+        sb.style(new StyleResolver(colors::get).resolve("." + t));
+    }
+
+    private static class StdInSource implements Source {
+
+        private final Process process;
+
+        StdInSource(Process process) {
+            this.process = process;
+        }
+
+        @Override
+        public String getName() {
+            return null;
+        }
+
+        @Override
+        public InputStream read() {
+            return process.in();
+        }
+    }
+}
diff --git a/jline/src/main/java/org/apache/felix/gogo/jline/Procedural.java b/jline/src/main/java/org/apache/felix/gogo/jline/Procedural.java
new file mode 100644
index 0000000..eece61a
--- /dev/null
+++ b/jline/src/main/java/org/apache/felix/gogo/jline/Procedural.java
@@ -0,0 +1,604 @@
+/*
+ * 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.felix.gogo.jline;
+
+import java.io.ByteArrayOutputStream;
+import java.io.PrintStream;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+
+import org.apache.felix.service.command.CommandSession;
+import org.apache.felix.service.command.Function;
+import org.apache.felix.service.command.Process;
+import org.jline.builtins.Options;
+
+public class Procedural {
+
+    static final String[] functions = {"each", "if", "not", "throw", "try", "until", "while", "break", "continue"};
+
+    public Object _main(CommandSession session, Object[] argv) throws Throwable {
+        if (argv == null || argv.length < 1) {
+            throw new IllegalArgumentException();
+        }
+        Process process = Process.Utils.current();
+        try {
+            return run(session, process, argv);
+        } catch (OptionException e) {
+            process.err().println(e.getMessage());
+            process.error(2);
+        } catch (HelpException e) {
+            process.err().println(e.getMessage());
+            process.error(0);
+        } catch (ThrownException e) {
+            process.error(1);
+            throw e.getCause();
+        }
+        return null;
+    }
+
+    @SuppressWarnings("serial")
+    protected static class OptionException extends Exception {
+        public OptionException(String message, Throwable cause) {
+            super(message, cause);
+        }
+    }
+
+    @SuppressWarnings("serial")
+    protected static class HelpException extends Exception {
+        public HelpException(String message) {
+            super(message);
+        }
+    }
+
+    @SuppressWarnings("serial")
+    protected static class ThrownException extends Exception {
+        public ThrownException(Throwable cause) {
+            super(cause);
+        }
+    }
+
+    @SuppressWarnings("serial")
+    protected static class BreakException extends Exception {
+    }
+
+    @SuppressWarnings("serial")
+    protected static class ContinueException extends Exception {
+    }
+
+    protected Options parseOptions(CommandSession session, String[] usage, Object[] argv) throws HelpException, OptionException {
+        try {
+            Options opt = Options.compile(usage, s -> get(session, s)).parse(argv, true);
+            if (opt.isSet("help")) {
+                ByteArrayOutputStream baos = new ByteArrayOutputStream();
+                opt.usage(new PrintStream(baos));
+                throw new HelpException(baos.toString());
+            }
+            return opt;
+        } catch (IllegalArgumentException e) {
+            throw new OptionException(e.getMessage(), e);
+        }
+    }
+
+    protected String get(CommandSession session, String name) {
+        Object o = session.get(name);
+        return o != null ? o.toString() : null;
+    }
+
+    protected Object run(CommandSession session, Process process, Object[] argv) throws Throwable {
+        switch (argv[0].toString()) {
+            case "each":
+                return doEach(session, process, argv);
+            case "if":
+                return doIf(session, process, argv);
+            case "not":
+                return doNot(session, process, argv);
+            case "throw":
+                return doThrow(session, process, argv);
+            case "try":
+                return doTry(session, process, argv);
+            case "until":
+                return doUntil(session, process, argv);
+            case "while":
+                return doWhile(session, process, argv);
+            case "break":
+                return doBreak(session, process, argv);
+            case "continue":
+                return doContinue(session, process, argv);
+            default:
+                throw new UnsupportedOperationException();
+        }
+    }
+
+    protected List<Object> doEach(CommandSession session,
+                                  Process process,
+                                  Object[] argv) throws Exception {
+        String[] usage = {
+                "each -  loop over the elements",
+                "Usage: each [-r] elements [do] { closure }",
+                "         elements              an array to iterate on",
+                "         closure               a closure to call",
+                "  -? --help                    Show help",
+                "  -r --result                  Return a list containing each iteration result",
+        };
+        Options opt = parseOptions(session, usage, argv);
+
+        Collection<Object> elements = getElements(opt);
+        if (opt.argObjects().size() > 0 && "do".equals(opt.argObjects().get(0))) {
+            opt.argObjects().remove(0);
+        }
+        List<Function> functions = getFunctions(opt);
+
+
+        if (elements == null || functions == null || functions.size() != 1) {
+            process.err().println("usage: each elements [do] { closure }");
+            process.err().println("       elements: an array to iterate on");
+            process.err().println("       closure: a function or closure to call");
+            process.error(2);
+            return null;
+        }
+
+        List<Object> args = new ArrayList<>();
+        List<Object> results = new ArrayList<>();
+        args.add(null);
+
+        for (Object x : elements) {
+            checkInterrupt();
+            args.set(0, x);
+            try {
+                results.add(functions.get(0).execute(session, args));
+            } catch (BreakException b) {
+                break;
+            } catch (ContinueException c) {
+                // Ignore
+            }
+        }
+
+        return opt.isSet("result") ? results : null;
+    }
+
+    protected Object doIf(CommandSession session, Process process, Object[] argv) throws Exception {
+        String[] usage = {
+                "if -  if / then / else construct",
+                "Usage: if {condition} [then] {if-action} [elif {cond} [then] {elif-action}]... [else] {else-action}",
+                "  -? --help                    Show help",
+        };
+        Options opt = parseOptions(session, usage, argv);
+
+        List<Function> conditions = new ArrayList<>();
+        List<Function> actions = new ArrayList<>();
+        Function elseFunction = null;
+        int step = 0;
+        boolean error = false;
+        for (Object obj : opt.argObjects()) {
+            switch (step) {
+                case 0:
+                    if (obj instanceof Function) {
+                        conditions.add((Function) obj);
+                    } else {
+                        error = true;
+                    }
+                    step = 1;
+                    break;
+                case 1:
+                    if ("then".equals(obj)) {
+                        step = 2;
+                        break;
+                    }
+                case 2:
+                    if (obj instanceof Function) {
+                        actions.add((Function) obj);
+                        step = 3;
+                    } else {
+                        error = true;
+                    }
+                    break;
+                case 3:
+                    if ("elif".equals(obj)) {
+                        step = 4;
+                    } else if ("else".equals(obj)) {
+                        step = 7;
+                    } else if (obj instanceof Function) {
+                        elseFunction = (Function) obj;
+                        step = 8;
+                    } else {
+                        error = true;
+                    }
+                    break;
+                case 4:
+                    if (obj instanceof Function) {
+                        conditions.add((Function) obj);
+                    } else {
+                        error = true;
+                    }
+                    step = 5;
+                    break;
+                case 5:
+                    if ("then".equals(obj)) {
+                        step = 6;
+                        break;
+                    }
+                case 6:
+                    if (obj instanceof Function) {
+                        actions.add((Function) obj);
+                        step = 3;
+                    } else {
+                        error = true;
+                    }
+                    break;
+                case 7:
+                    if (obj instanceof Function) {
+                        elseFunction = (Function) obj;
+                        step = 8;
+                    } else {
+                        error = true;
+                    }
+                    break;
+                case 8:
+                    error = true;
+                    break;
+            }
+            if (error) {
+                break;
+            }
+        }
+        error |= conditions.isEmpty();
+        error |= conditions.size() != actions.size();
+
+        if (error) {
+            process.err().println("usage: if {condition} [then] {if-action} [elif {elif-action}]... [else] {else-action}");
+            process.error(2);
+            return null;
+        }
+        for (int i = 0, length = conditions.size(); i < length; ++i) {
+            if (isTrue(session, conditions.get(i))) {
+                return actions.get(i).execute(session, null);
+            }
+        }
+        if (elseFunction != null) {
+            return elseFunction.execute(session, null);
+        }
+        return null;
+    }
+
+    protected Boolean doNot(CommandSession session, Process process, Object[] argv) throws Exception {
+        String[] usage = {
+                "not -  return the opposite condition",
+                "Usage: not { condition }",
+                "  -? --help                    Show help",
+        };
+        Options opt = parseOptions(session, usage, argv);
+        List<Function> functions = getFunctions(opt);
+        if (functions == null || functions.size() != 1) {
+            process.err().println("usage: not { condition }");
+            process.error(2);
+            return null;
+        }
+        return !isTrue(session, functions.get(0));
+
+    }
+
+    protected Object doThrow(CommandSession session, Process process, Object[] argv) throws ThrownException, HelpException, OptionException {
+        String[] usage = {
+                "throw -  throw an exception",
+                "Usage: throw [ message [ cause ] ]",
+                "       throw exception",
+                "       throw",
+                "  -? --help                    Show help",
+        };
+        Options opt = parseOptions(session, usage, argv);
+        if (opt.argObjects().size() == 0) {
+            Object exception = session.get("exception");
+            if (exception instanceof Throwable)
+                throw new ThrownException((Throwable) exception);
+            else
+                throw new ThrownException(new Exception());
+        }
+        else if (opt.argObjects().size() == 1 && opt.argObjects().get(0) instanceof Throwable) {
+            throw new ThrownException((Throwable) opt.argObjects().get(0));
+        }
+        else {
+            String message = opt.argObjects().get(0).toString();
+            Throwable cause = null;
+            if (opt.argObjects().size() > 1) {
+                if (opt.argObjects().get(1) instanceof Throwable) {
+                    cause = (Throwable) opt.argObjects().get(1);
+                }
+            }
+            throw new ThrownException(new Exception(message, cause));
+        }
+    }
+
+    protected Object doTry(CommandSession session, Process process, Object[] argv) throws Exception {
+        String[] usage = {
+                "try -  try / catch / finally construct",
+                "Usage: try { try-action } [ [catch] { catch-action } [ [finally] { finally-action } ]  ]",
+                "  -? --help                    Show help",
+        };
+        Options opt = parseOptions(session, usage, argv);
+        Function tryAction = null;
+        Function catchFunction = null;
+        Function finallyFunction = null;
+        int step = 0;
+        boolean error = false;
+        for (Object obj : opt.argObjects()) {
+            if (tryAction == null) {
+                if (obj instanceof Function) {
+                    tryAction = (Function) obj;
+                } else {
+                    error = true;
+                    break;
+                }
+                step = 1;
+            } else if ("catch".equals(obj)) {
+                if (step != 1) {
+                    error = true;
+                    break;
+                }
+                step = 2;
+            } else if ("finally".equals(obj)) {
+                if (step != 1 && step != 3) {
+                    error = true;
+                    break;
+                }
+                step = 4;
+            } else if (step == 1 || step == 2) {
+                if (obj instanceof Function) {
+                    catchFunction = (Function) obj;
+                } else {
+                    error = true;
+                    break;
+                }
+                step = 3;
+            } else if (step == 3 || step == 4) {
+                if (obj instanceof Function) {
+                    finallyFunction = (Function) obj;
+                } else {
+                    error = true;
+                    break;
+                }
+                step = 5;
+            } else {
+                error = true;
+                break;
+            }
+        }
+        error |= tryAction == null;
+        error |= catchFunction == null && finallyFunction == null;
+
+        if (error) {
+            process.err().println("usage: try { try-action } [ [catch] { catch-action } [ [finally] { finally-action } ] ]");
+            process.error(2);
+            return null;
+        }
+        try {
+            return tryAction.execute(session, null);
+        } catch (BreakException b) {
+            throw b;
+        } catch (Exception e) {
+            session.put("exception", e);
+            if (catchFunction != null) {
+                catchFunction.execute(session, null);
+            }
+            return null;
+        } finally {
+            if (finallyFunction != null) {
+                finallyFunction.execute(session, null);
+            }
+        }
+    }
+
+    protected Object doWhile(CommandSession session, Process process, Object[] argv) throws Exception {
+        String[] usage = {
+                "while -  while loop",
+                "Usage: while { condition } [do] { action }",
+                "  -? --help                    Show help",
+        };
+        Options opt = parseOptions(session, usage, argv);
+        Function condition = null;
+        Function action = null;
+        int step = 0;
+        boolean error = false;
+        for (Object obj : opt.argObjects()) {
+            if (condition == null) {
+                if (obj instanceof Function) {
+                    condition = (Function) obj;
+                } else {
+                    error = true;
+                    break;
+                }
+                step = 1;
+            } else if ("do".equals(obj)) {
+                if (step != 1) {
+                    error = true;
+                    break;
+                }
+                step = 2;
+            } else if (step == 1 || step == 2) {
+                if (obj instanceof Function) {
+                    action = (Function) obj;
+                } else {
+                    error = true;
+                    break;
+                }
+                step = 3;
+            } else {
+                error = true;
+                break;
+            }
+        }
+        error |= condition == null;
+        error |= action == null;
+
+        if (error) {
+            process.err().println("usage: while { condition } [do] { action }");
+            process.error(2);
+            return null;
+        }
+        while (isTrue(session, condition)) {
+            try {
+                action.execute(session, null);
+            } catch (BreakException b) {
+                break;
+            } catch (ContinueException c) {
+                // Ignore
+            }
+        }
+        return null;
+    }
+
+    protected Object doUntil(CommandSession session, Process process, Object[] argv) throws Exception {
+        String[] usage = {
+                "until -  until loop",
+                "Usage: until { condition } [do] { action }",
+                "  -? --help                    Show help",
+        };
+        Options opt = parseOptions(session, usage, argv);
+        Function condition = null;
+        Function action = null;
+        int step = 0;
+        boolean error = false;
+        for (Object obj : opt.argObjects()) {
+            if (condition == null) {
+                if (obj instanceof Function) {
+                    condition = (Function) obj;
+                } else {
+                    error = true;
+                    break;
+                }
+                step = 1;
+            } else if ("do".equals(obj)) {
+                if (step != 1) {
+                    error = true;
+                    break;
+                }
+                step = 2;
+            } else if (step == 1 || step == 2) {
+                if (obj instanceof Function) {
+                    action = (Function) obj;
+                } else {
+                    error = true;
+                    break;
+                }
+                step = 3;
+            } else {
+                error = true;
+                break;
+            }
+        }
+        error |= condition == null;
+        error |= action == null;
+
+        if (error) {
+            process.err().println("usage: until { condition } [do] { action }");
+            process.error(2);
+            return null;
+        }
+        while (!isTrue(session, condition)) {
+            try {
+                action.execute(session, null);
+            } catch (BreakException e) {
+                break;
+            } catch (ContinueException c) {
+                // Ignore
+            }
+        }
+        return null;
+    }
+
+    protected Object doBreak(CommandSession session, Process process, Object[] argv) throws Exception {
+        String[] usage = {
+                "break -  break from loop",
+                "Usage: break",
+                "  -? --help                    Show help",
+        };
+        parseOptions(session, usage, argv);
+        throw new BreakException();
+    }
+
+    protected Object doContinue(CommandSession session, Process process, Object[] argv) throws Exception {
+        String[] usage = {
+                "continue -  continue loop",
+                "Usage: continue",
+                "  -? --help                    Show help",
+        };
+        parseOptions(session, usage, argv);
+        throw new ContinueException();
+    }
+
+    private boolean isTrue(CommandSession session, Function function) throws Exception {
+        checkInterrupt();
+        return isTrue(function.execute(session, null));
+    }
+
+    private boolean isTrue(Object result) throws InterruptedException {
+        checkInterrupt();
+
+        if (result == null)
+            return false;
+
+        if (result instanceof Boolean)
+            return (Boolean) result;
+
+        if (result instanceof Number) {
+            if (0 == ((Number) result).intValue())
+                return false;
+        }
+
+        if ("".equals(result))
+            return false;
+
+        return !"0".equals(result);
+    }
+
+    private void checkInterrupt() throws InterruptedException {
+        if (Thread.currentThread().isInterrupted())
+            throw new InterruptedException("interrupted");
+    }
+
+    @SuppressWarnings("unchecked")
+    private Collection<Object> getElements(Options opt) {
+        Collection<Object> elements = null;
+        if (opt.argObjects().size() > 0) {
+            Object o = opt.argObjects().remove(0);
+            if (o instanceof Collection) {
+                elements = (Collection<Object>) o;
+            } else if (o != null && o.getClass().isArray()) {
+                elements = Arrays.asList((Object[]) o);
+            }
+        }
+        return elements;
+    }
+
+    private List<Function> getFunctions(Options opt) {
+        List<Function> functions = new ArrayList<>();
+        for (Object o : opt.argObjects()) {
+            if (o instanceof Function) {
+                functions.add((Function) o);
+            }
+            else {
+                functions = null;
+                break;
+            }
+        }
+        return functions;
+    }
+
+}
diff --git a/jline/src/main/java/org/apache/felix/gogo/jline/Shell.java b/jline/src/main/java/org/apache/felix/gogo/jline/Shell.java
new file mode 100644
index 0000000..eaf0901
--- /dev/null
+++ b/jline/src/main/java/org/apache/felix/gogo/jline/Shell.java
@@ -0,0 +1,728 @@
+/*
+ * 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.felix.gogo.jline;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.io.PrintStream;
+import java.io.PrintWriter;
+import java.lang.annotation.Annotation;
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+import java.net.HttpURLConnection;
+import java.net.URI;
+import java.net.URL;
+import java.net.URLConnection;
+import java.nio.CharBuffer;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+import java.util.TreeMap;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import org.apache.felix.gogo.runtime.Closure;
+import org.apache.felix.gogo.runtime.CommandProxy;
+import org.apache.felix.gogo.runtime.CommandSessionImpl;
+import org.apache.felix.service.command.Job;
+import org.apache.felix.service.command.Job.Status;
+import org.apache.felix.gogo.runtime.Reflective;
+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.Descriptor;
+import org.apache.felix.service.command.Function;
+import org.apache.felix.service.command.Parameter;
+import org.apache.felix.service.threadio.ThreadIO;
+import org.jline.builtins.Completers.CompletionData;
+import org.jline.builtins.Completers.CompletionEnvironment;
+import org.jline.builtins.Options;
+import org.jline.reader.EndOfFileException;
+import org.jline.reader.LineReader;
+import org.jline.reader.LineReaderBuilder;
+import org.jline.reader.ParsedLine;
+import org.jline.reader.UserInterruptException;
+import org.jline.terminal.Terminal;
+import org.jline.terminal.Terminal.Signal;
+import org.jline.terminal.Terminal.SignalHandler;
+import org.jline.utils.AttributedStringBuilder;
+import org.jline.utils.AttributedStyle;
+
+public class Shell {
+
+    public static final String VAR_COMPLETIONS = ".completions";
+    public static final String VAR_COMMAND_LINE = ".commandLine";
+    public static final String VAR_READER = ".reader";
+    public static final String VAR_SESSION = ".session";
+    public static final String VAR_PROCESSOR = ".processor";
+    public static final String VAR_TERMINAL = ".terminal";
+    public static final String VAR_EXCEPTION = "exception";
+    public static final String VAR_RESULT = "_";
+    public static final String VAR_LOCATION = ".location";
+    public static final String VAR_PROMPT = "prompt";
+    public static final String VAR_RPROMPT = "rprompt";
+    public static final String VAR_SCOPE = "SCOPE";
+    public static final String VAR_CONTEXT = org.apache.felix.gogo.runtime.activator.Activator.CONTEXT;
+
+    static final String[] functions = {"gosh", "sh", "source", "help"};
+
+    private final URI baseURI;
+    private final String profile;
+    private final Context context;
+    private final CommandProcessor processor;
+    private final ThreadIO tio;
+
+    private AtomicBoolean stopping = new AtomicBoolean();
+
+    public Shell(Context context, CommandProcessor processor) {
+        this(context, processor, null);
+    }
+
+    public Shell(Context context, CommandProcessor processor, String profile) {
+        this(context, processor, null, profile);
+    }
+
+    public Shell(Context context, CommandProcessor processor, ThreadIO tio, String profile) {
+        this.context = context;
+        this.processor = processor;
+        this.tio = tio != null ? tio : getThreadIO(processor);
+        String baseDir = context.getProperty("gosh.home");
+        baseDir = (baseDir == null) ? context.getProperty("user.dir") : baseDir;
+        this.baseURI = new File(baseDir).toURI();
+        this.profile = profile != null ? profile : "gosh_profile";
+    }
+
+    private ThreadIO getThreadIO(CommandProcessor processor) {
+        try {
+            Field field = processor.getClass().getDeclaredField("threadIO");
+            field.setAccessible(true);
+            return (ThreadIO) field.get(processor);
+        } catch (Throwable t) {
+            return null;
+        }
+    }
+
+    public Context getContext() {
+        return context;
+    }
+
+    public static Terminal getTerminal(CommandSession session) {
+        return (Terminal) session.get(VAR_TERMINAL);
+    }
+
+    public static LineReader getReader(CommandSession session) {
+        return (LineReader) session.get(VAR_READER);
+    }
+
+    public static CommandProcessor getProcessor(CommandSession session) {
+        return (CommandProcessor) session.get(VAR_PROCESSOR);
+    }
+
+    @SuppressWarnings("unchecked")
+    public static Map<String, List<CompletionData>> getCompletions(CommandSession session) {
+        return (Map<String, List<CompletionData>>) session.get(VAR_COMPLETIONS);
+    }
+
+    @SuppressWarnings("unchecked")
+    public static Set<String> getCommands(CommandSession session) {
+        return (Set<String>) session.get(CommandSessionImpl.COMMANDS);
+    }
+
+    public static ParsedLine getParsedLine(CommandSession session) {
+        return (ParsedLine) session.get(VAR_COMMAND_LINE);
+    }
+
+    public static String getPrompt(CommandSession session) {
+        return expand(session, VAR_PROMPT, "gl! ");
+    }
+
+    public static String getRPrompt(CommandSession session) {
+        return expand(session, VAR_RPROMPT, null);
+    }
+
+    public static String expand(CommandSession session, String name, String def) {
+        Object prompt = session.get(name);
+        if (prompt != null) {
+            try {
+                Object o = org.apache.felix.gogo.runtime.Expander.expand(
+                        prompt.toString(),
+                        new Closure((CommandSessionImpl) session, null, null));
+                if (o != null) {
+                    return o.toString();
+                }
+            } catch (Exception e) {
+                // ignore
+            }
+        }
+        return def;
+    }
+
+    public static String resolve(CommandSession session, String command) {
+        String resolved = command;
+        if (command.indexOf(':') < 0) {
+            Set<String> commands = getCommands(session);
+            Object path = session.get(VAR_SCOPE);
+            String scopePath = (null == path ? "*" : path.toString());
+            for (String scope : scopePath.split(":")) {
+                for (String entry : commands) {
+                    if ("*".equals(scope) && entry.endsWith(":" + command)
+                            || entry.equals(scope + ":" + command)) {
+                        resolved = entry;
+                        break;
+                    }
+                }
+            }
+        }
+        return resolved;
+    }
+
+    public static CharSequence readScript(URI script) throws Exception {
+        CharBuffer buf = CharBuffer.allocate(4096);
+        StringBuilder sb = new StringBuilder();
+
+        URLConnection conn = script.toURL().openConnection();
+
+        try (InputStreamReader in = new InputStreamReader(conn.getInputStream()))
+        {
+            while (in.read(buf) > 0)
+            {
+                buf.flip();
+                sb.append(buf);
+                buf.clear();
+            }
+        }
+        finally
+        {
+            if (conn instanceof HttpURLConnection)
+            {
+                ((HttpURLConnection) conn).disconnect();
+            }
+        }
+
+        return sb;
+    }
+
+    @SuppressWarnings("unchecked")
+    static Set<String> getVariables(CommandSession session) {
+        return (Set<String>) session.get(".variables");
+    }
+
+    private static <T extends Annotation> T findAnnotation(Annotation[] anns,
+                                                           Class<T> clazz) {
+        for (int i = 0; (anns != null) && (i < anns.length); i++) {
+            if (clazz.isInstance(anns[i])) {
+                return clazz.cast(anns[i]);
+            }
+        }
+        return null;
+    }
+
+    public void stop() {
+        stopping.set(true);
+    }
+
+    public Object gosh(CommandSession currentSession, String[] argv) throws Exception {
+        final String[] usage = {
+                "gosh - execute script with arguments in a new session",
+                "  args are available as session variables $1..$9 and $args.",
+                "Usage: gosh [OPTIONS] [script-file [args..]]",
+                "  -c --command             pass all remaining args to sub-shell",
+                "     --nointeractive       don't start interactive session",
+                "     --nohistory           don't save the command history",
+                "     --login               login shell (same session, reads etc/gosh_profile)",
+                "  -s --noshutdown          don't shutdown framework when script completes",
+                "  -x --xtrace              echo commands before execution",
+                "  -? --help                show help",
+                "If no script-file, an interactive shell is started, type $D to exit."};
+
+        Options opt = Options.compile(usage).setOptionsFirst(true).parse(argv);
+        List<String> args = opt.args();
+
+        boolean login = opt.isSet("login");
+        boolean interactive = !opt.isSet("nointeractive");
+
+        if (opt.isSet("help")) {
+            opt.usage(System.err);
+            if (login && !opt.isSet("noshutdown")) {
+                shutdown();
+            }
+            return null;
+        }
+
+        if (opt.isSet("command") && args.isEmpty()) {
+            throw opt.usageError("option --command requires argument(s)");
+        }
+
+        CommandSession session;
+        if (login) {
+            session = currentSession;
+        } else {
+            session = createChildSession(currentSession);
+        }
+
+        if (opt.isSet("xtrace")) {
+            session.put("echo", true);
+        }
+
+        Terminal terminal = getTerminal(session);
+        session.put(Shell.VAR_CONTEXT, context);
+        session.put(Shell.VAR_PROCESSOR, processor);
+        session.put(Shell.VAR_SESSION, session);
+        session.put("#TERM", (Function) (s, arguments) -> terminal.getType());
+        session.put("#COLUMNS", (Function) (s, arguments) -> terminal.getWidth());
+        session.put("#LINES", (Function) (s, arguments) -> terminal.getHeight());
+        session.put("#PWD", (Function) (s, arguments) -> s.currentDir().toString());
+        if (!opt.isSet("nohistory")) {
+            session.put(LineReader.HISTORY_FILE, Paths.get(System.getProperty("user.home"), ".gogo.history"));
+        }
+
+        if (tio != null) {
+            PrintWriter writer = terminal.writer();
+            PrintStream out = new PrintStream(new OutputStream() {
+                @Override
+                public void write(int b) throws IOException {
+                    write(new byte[]{(byte) b}, 0, 1);
+                }
+                public void write(byte b[], int off, int len) {
+                    writer.write(new String(b, off, len));
+                }
+                public void flush() {
+                    writer.flush();
+                }
+                public void close() {
+                    writer.close();
+                }
+            });
+            tio.setStreams(terminal.input(), out, out);
+        }
+
+        try {
+            LineReader reader;
+            if (args.isEmpty() && interactive) {
+                CompletionEnvironment completionEnvironment = new CompletionEnvironment() {
+                    @Override
+                    public Map<String, List<CompletionData>> getCompletions() {
+                        return Shell.getCompletions(session);
+                    }
+
+                    @Override
+                    public Set<String> getCommands() {
+                        return Shell.getCommands(session);
+                    }
+
+                    @Override
+                    public String resolveCommand(String command) {
+                        return Shell.resolve(session, command);
+                    }
+
+                    @Override
+                    public String commandName(String command) {
+                        int idx = command.indexOf(':');
+                        return idx >= 0 ? command.substring(idx + 1) : command;
+                    }
+
+                    @Override
+                    public Object evaluate(LineReader reader, ParsedLine line, String func) throws Exception {
+                        session.put(Shell.VAR_COMMAND_LINE, line);
+                        return session.execute(func);
+                    }
+                };
+                reader = LineReaderBuilder.builder()
+                        .terminal(terminal)
+                        .variables(((CommandSessionImpl) session).getVariables())
+                        .completer(new org.jline.builtins.Completers.Completer(completionEnvironment))
+                        .highlighter(new Highlighter(session))
+                        .parser(new Parser())
+                        .expander(new Expander(session))
+                        .build();
+                reader.setOpt(LineReader.Option.AUTO_FRESH_LINE);
+                session.put(Shell.VAR_READER, reader);
+                session.put(Shell.VAR_COMPLETIONS, new HashMap<>());
+            } else {
+                reader = null;
+            }
+
+            if (login || interactive) {
+                URI uri = baseURI.resolve("etc/" + profile);
+                if (!new File(uri).exists()) {
+                    URL url = getClass().getResource("/ext/" + profile);
+                    if (url == null) {
+                        url = getClass().getResource("/" + profile);
+                    }
+                    uri = (url == null) ? null : url.toURI();
+                }
+                if (uri != null) {
+                    source(session, uri.toString());
+                }
+            }
+
+            Object result = null;
+
+            if (args.isEmpty()) {
+                if (interactive) {
+                    result = runShell(session, terminal, reader);
+                }
+            } else {
+                CharSequence program;
+
+                if (opt.isSet("command")) {
+                    StringBuilder buf = new StringBuilder();
+                    for (String arg : args) {
+                        if (buf.length() > 0) {
+                            buf.append(' ');
+                        }
+                        buf.append(arg);
+                    }
+                    program = buf;
+                } else {
+                    URI script = session.currentDir().toUri().resolve(args.remove(0));
+
+                    // set script arguments
+                    session.put("0", script);
+                    session.put("args", args);
+
+                    for (int i = 0; i < args.size(); ++i) {
+                        session.put(String.valueOf(i + 1), args.get(i));
+                    }
+
+                    program = readScript(script);
+                }
+
+                result = session.execute(program);
+            }
+
+            if (login && interactive && !opt.isSet("noshutdown")) {
+                if (terminal != null) {
+                    terminal.writer().println("gosh: stopping framework");
+                    terminal.flush();
+                }
+                shutdown();
+            }
+
+            return result;
+        } finally {
+            if (tio != null) {
+                tio.close();
+            }
+        }
+    }
+
+    private CommandSession createChildSession(CommandSession parent) {
+        CommandSession session = processor.createSession(parent);
+        getVariables(parent).stream()
+                .filter(key -> key.matches("[.]?[A-Z].*"))
+                .forEach(key -> session.put(key, parent.get(key)));
+        session.put(Shell.VAR_TERMINAL, getTerminal(parent));
+        return session;
+    }
+
+    private Object runShell(final CommandSession session, Terminal terminal,
+                            LineReader reader) throws InterruptedException {
+        AtomicBoolean reading = new AtomicBoolean();
+        session.setJobListener((job, previous, current) -> {
+            if (previous == Status.Background || current == Status.Background
+                    || previous == Status.Suspended || current == Status.Suspended) {
+                int width = terminal.getWidth();
+                String status = current.name().toLowerCase();
+                terminal.writer().write(getStatusLine(job, width, status));
+                terminal.flush();
+                if (reading.get() && !stopping.get()) {
+                    reader.callWidget(LineReader.REDRAW_LINE);
+                    reader.callWidget(LineReader.REDISPLAY);
+                }
+            }
+        });
+        SignalHandler intHandler = terminal.handle(Signal.INT, s -> {
+            Job current = session.foregroundJob();
+            if (current != null) {
+                current.interrupt();
+            }
+        });
+        SignalHandler suspHandler = terminal.handle(Signal.TSTP, s -> {
+            Job current = session.foregroundJob();
+            if (current != null) {
+                current.suspend();
+            }
+        });
+        Object result = null;
+        try {
+            while (!stopping.get()) {
+                try {
+                    reading.set(true);
+                    try {
+                        String prompt = Shell.getPrompt(session);
+                        String rprompt = Shell.getRPrompt(session);
+                        if (stopping.get()) {
+                            break;
+                        }
+                        reader.readLine(prompt, rprompt, (Character) null, null);
+                    } finally {
+                        reading.set(false);
+                    }
+                    ParsedLine parsedLine = reader.getParsedLine();
+                    if (parsedLine == null) {
+                        throw new EndOfFileException();
+                    }
+                    try {
+                        result = session.execute(((ParsedLineImpl) parsedLine).program());
+                        session.put(Shell.VAR_RESULT, result); // set $_ to last result
+
+                        if (result != null && !Boolean.FALSE.equals(session.get(".Gogo.format"))) {
+                            System.out.println(session.format(result, Converter.INSPECT));
+                        }
+                    } catch (Exception e) {
+                        AttributedStringBuilder sb = new AttributedStringBuilder();
+                        sb.style(sb.style().foreground(AttributedStyle.RED));
+                        sb.append(e.toString());
+                        sb.style(sb.style().foregroundDefault());
+                        terminal.writer().println(sb.toAnsi(terminal));
+                        terminal.flush();
+                        session.put(Shell.VAR_EXCEPTION, e);
+                    }
+
+                    waitJobCompletion(session);
+
+                } catch (UserInterruptException e) {
+                    // continue;
+                } catch (EndOfFileException e) {
+                    try {
+                        reader.getHistory().save();
+                    } catch (IOException e1) {
+                        // ignore
+                    }
+                    break;
+                }
+            }
+        } finally {
+            terminal.handle(Signal.INT, intHandler);
+            terminal.handle(Signal.TSTP, suspHandler);
+        }
+        return result;
+    }
+
+    private void waitJobCompletion(final CommandSession session) throws InterruptedException {
+        while (true) {
+            Job job = session.foregroundJob();
+            if (job != null) {
+                //noinspection SynchronizationOnLocalVariableOrMethodParameter
+                synchronized (job) {
+                    if (job.status() == Status.Foreground) {
+                        job.wait();
+                    }
+                }
+            } else {
+                break;
+            }
+        }
+    }
+
+    private String getStatusLine(Job job, int width, String status) {
+        StringBuilder sb = new StringBuilder();
+        for (int i = 0; i < width - 1; i++) {
+            sb.append(' ');
+        }
+        sb.append('\r');
+        sb.append("[").append(job.id()).append("]  ");
+        sb.append(status);
+        for (int i = status.length(); i < "background".length(); i++) {
+            sb.append(' ');
+        }
+        sb.append("  ").append(job.command()).append("\n");
+        return sb.toString();
+    }
+
+    @Descriptor("start a new shell")
+    public Object sh(final CommandSession session, String[] argv) throws Exception {
+        return gosh(session, argv);
+    }
+
+    private void shutdown() throws Exception {
+        context.exit();
+    }
+
+    @Descriptor("Evaluates contents of file")
+    public Object source(CommandSession session, String script) throws Exception {
+        URI uri = session.currentDir().toUri().resolve(script);
+        session.put("0", uri);
+        try {
+            return session.execute(readScript(uri));
+        } finally {
+            session.put("0", null); // API doesn't support remove
+        }
+    }
+
+    private Map<String, List<Method>> getReflectionCommands(CommandSession session) {
+        Map<String, List<Method>> commands = new TreeMap<>();
+        Set<String> names = getCommands(session);
+        for (String name : names) {
+            Function function = (Function) session.get(name);
+            if (function instanceof CommandProxy) {
+                Object target = ((CommandProxy) function).getTarget();
+                List<Method> methods = new ArrayList<>();
+                String func = name.substring(name.indexOf(':') + 1).toLowerCase();
+                List<String> funcs = new ArrayList<>();
+                funcs.add("is" + func);
+                funcs.add("get" + func);
+                funcs.add("set" + func);
+                if (Reflective.KEYWORDS.contains(func)) {
+                    funcs.add("_" + func);
+                } else {
+                    funcs.add(func);
+                }
+                for (Method method : target.getClass().getMethods()) {
+                    if (funcs.contains(method.getName().toLowerCase())) {
+                        methods.add(method);
+                    }
+                }
+                commands.put(name, methods);
+                ((CommandProxy) function).ungetTarget();
+            }
+        }
+        return commands;
+    }
+
+    @Descriptor("displays available commands")
+    public void help(CommandSession session) {
+        Map<String, List<Method>> commands = getReflectionCommands(session);
+        commands.keySet().forEach(System.out::println);
+    }
+
+    @Descriptor("displays information about a specific command")
+    public void help(CommandSession session, @Descriptor("target command") String name) {
+        Map<String, List<Method>> commands = getReflectionCommands(session);
+
+        List<Method> methods = null;
+
+        // If the specified command doesn't have a scope, then
+        // search for matching methods by ignoring the scope.
+        int scopeIdx = name.indexOf(':');
+        if (scopeIdx < 0) {
+            for (Entry<String, List<Method>> entry : commands.entrySet()) {
+                String k = entry.getKey().substring(entry.getKey().indexOf(':') + 1);
+                if (name.equals(k)) {
+                    name = entry.getKey();
+                    methods = entry.getValue();
+                    break;
+                }
+            }
+        }
+        // Otherwise directly look up matching methods.
+        else {
+            methods = commands.get(name);
+        }
+
+        if ((methods != null) && (methods.size() > 0)) {
+            for (Method m : methods) {
+                Descriptor d = m.getAnnotation(Descriptor.class);
+                if (d == null) {
+                    System.out.println("\n" + m.getName());
+                } else {
+                    System.out.println("\n" + m.getName() + " - " + d.value());
+                }
+
+                System.out.println("   scope: " + name.substring(0, name.indexOf(':')));
+
+                // Get flags and options.
+                Class<?>[] paramTypes = m.getParameterTypes();
+                Map<String, Parameter> flags = new TreeMap<>();
+                Map<String, String> flagDescs = new TreeMap<>();
+                Map<String, Parameter> options = new TreeMap<>();
+                Map<String, String> optionDescs = new TreeMap<>();
+                List<String> params = new ArrayList<>();
+                Annotation[][] anns = m.getParameterAnnotations();
+                for (int paramIdx = 0; paramIdx < anns.length; paramIdx++) {
+                    Class<?> paramType = m.getParameterTypes()[paramIdx];
+                    if (paramType == CommandSession.class) {
+                        /* Do not bother the user with a CommandSession. */
+                        continue;
+                    }
+                    Parameter p = findAnnotation(anns[paramIdx], Parameter.class);
+                    d = findAnnotation(anns[paramIdx], Descriptor.class);
+                    if (p != null) {
+                        if (p.presentValue().equals(Parameter.UNSPECIFIED)) {
+                            options.put(p.names()[0], p);
+                            if (d != null) {
+                                optionDescs.put(p.names()[0], d.value());
+                            }
+                        } else {
+                            flags.put(p.names()[0], p);
+                            if (d != null) {
+                                flagDescs.put(p.names()[0], d.value());
+                            }
+                        }
+                    } else if (d != null) {
+                        params.add(paramTypes[paramIdx].getSimpleName());
+                        params.add(d.value());
+                    } else {
+                        params.add(paramTypes[paramIdx].getSimpleName());
+                        params.add("");
+                    }
+                }
+
+                // Print flags and options.
+                if (flags.size() > 0) {
+                    System.out.println("   flags:");
+                    for (Entry<String, Parameter> entry : flags.entrySet()) {
+                        // Print all aliases.
+                        String[] names = entry.getValue().names();
+                        System.out.print("      " + names[0]);
+                        for (int aliasIdx = 1; aliasIdx < names.length; aliasIdx++) {
+                            System.out.print(", " + names[aliasIdx]);
+                        }
+                        System.out.println("   " + flagDescs.get(entry.getKey()));
+                    }
+                }
+                if (options.size() > 0) {
+                    System.out.println("   options:");
+                    for (Entry<String, Parameter> entry : options.entrySet()) {
+                        // Print all aliases.
+                        String[] names = entry.getValue().names();
+                        System.out.print("      " + names[0]);
+                        for (int aliasIdx = 1; aliasIdx < names.length; aliasIdx++) {
+                            System.out.print(", " + names[aliasIdx]);
+                        }
+                        System.out.println("   "
+                                + optionDescs.get(entry.getKey())
+                                + ((entry.getValue().absentValue() == null) ? ""
+                                : " [optional]"));
+                    }
+                }
+                if (params.size() > 0) {
+                    System.out.println("   parameters:");
+                    for (Iterator<String> it = params.iterator(); it.hasNext(); ) {
+                        System.out.println("      " + it.next() + "   " + it.next());
+                    }
+                }
+            }
+        }
+    }
+
+    public interface Context {
+        String getProperty(String name);
+
+        void exit() throws Exception;
+    }
+
+}
diff --git a/jline/src/main/java/org/apache/felix/gogo/jline/SingleServiceTracker.java b/jline/src/main/java/org/apache/felix/gogo/jline/SingleServiceTracker.java
new file mode 100644
index 0000000..c9e4be8
--- /dev/null
+++ b/jline/src/main/java/org/apache/felix/gogo/jline/SingleServiceTracker.java
@@ -0,0 +1,191 @@
+/*
+ * 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.felix.gogo.jline;
+
+import java.util.Arrays;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicReference;
+
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.Constants;
+import org.osgi.framework.Filter;
+import org.osgi.framework.InvalidSyntaxException;
+import org.osgi.framework.ServiceEvent;
+import org.osgi.framework.ServiceListener;
+import org.osgi.framework.ServiceReference;
+
+//This is from aries util
+public final class SingleServiceTracker<T> implements ServiceListener {
+
+    public interface SingleServiceListener {
+        void serviceFound();
+
+        void serviceLost();
+
+        void serviceReplaced();
+    }
+
+    private final BundleContext ctx;
+    private final String className;
+    private final AtomicReference<T> service = new AtomicReference<>();
+    private final AtomicReference<ServiceReference<?>> ref = new AtomicReference<>();
+    private final AtomicBoolean open = new AtomicBoolean(false);
+    private final SingleServiceListener serviceListener;
+    private final String filterString;
+    private final Filter filter;
+
+    public SingleServiceTracker(BundleContext context, Class<T> clazz, SingleServiceListener sl) throws InvalidSyntaxException {
+        this(context, clazz, null, sl);
+    }
+
+    public SingleServiceTracker(BundleContext context, Class<T> clazz, String filterString, SingleServiceListener sl) throws InvalidSyntaxException {
+        this(context, clazz.getName(), filterString, sl);
+    }
+
+    public SingleServiceTracker(BundleContext context, String className, String filterString, SingleServiceListener sl) throws InvalidSyntaxException {
+        this.ctx = context;
+        this.className = className;
+        this.serviceListener = sl;
+        if (filterString == null || filterString.isEmpty()) {
+            this.filterString = null;
+            this.filter = null;
+        } else {
+            this.filterString = filterString;
+            this.filter = context.createFilter(filterString);
+        }
+    }
+
+    public T getService() {
+        return service.get();
+    }
+
+    public ServiceReference<?> getServiceReference() {
+        return ref.get();
+    }
+
+    public void open() {
+        if (open.compareAndSet(false, true)) {
+            try {
+                String filterString = '(' + Constants.OBJECTCLASS + '=' + className + ')';
+                if (filter != null) filterString = "(&" + filterString + filter + ')';
+                ctx.addServiceListener(this, filterString);
+                findMatchingReference(null);
+            } catch (InvalidSyntaxException e) {
+                // this can never happen. (famous last words :)
+            }
+        }
+    }
+
+    public void serviceChanged(ServiceEvent event) {
+        if (open.get()) {
+            if (event.getType() == ServiceEvent.UNREGISTERING) {
+                ServiceReference<?> deadRef = event.getServiceReference();
+                if (deadRef.equals(ref.get())) {
+                    findMatchingReference(deadRef);
+                }
+            } else if (event.getType() == ServiceEvent.REGISTERED && ref.get() == null) {
+                findMatchingReference(null);
+            }
+        }
+    }
+
+    private void findMatchingReference(ServiceReference<?> original) {
+        try {
+            boolean clear = true;
+            ServiceReference<?>[] refs = ctx.getServiceReferences(className, filterString);
+            if (refs != null && refs.length > 0) {
+                if (refs.length > 1) {
+                    Arrays.sort(refs);
+                }
+                @SuppressWarnings("unchecked")
+                T service = (T) ctx.getService(refs[0]);
+                if (service != null) {
+                    clear = false;
+
+                    // We do the unget out of the lock so we don't exit this class while holding a lock.
+                    if (!update(original, refs[0], service)) {
+                        ctx.ungetService(refs[0]);
+                    }
+                }
+            } else if (original == null) {
+                clear = false;
+            }
+
+            if (clear) {
+                update(original, null, null);
+            }
+        } catch (InvalidSyntaxException e) {
+            // this can never happen. (famous last words :)
+        }
+    }
+
+    private boolean update(ServiceReference<?> deadRef, ServiceReference<?> newRef, T service) {
+        boolean result = false;
+        int foundLostReplaced = -1;
+
+        // Make sure we don't try to get a lock on null
+        Object lock;
+
+        // we have to choose our lock.
+        if (newRef != null) lock = newRef;
+        else if (deadRef != null) lock = deadRef;
+        else lock = this;
+
+        // This lock is here to ensure that no two threads can set the ref and service
+        // at the same time.
+        synchronized (lock) {
+            if (open.get()) {
+                result = this.ref.compareAndSet(deadRef, newRef);
+                if (result) {
+                    this.service.set(service);
+
+                    if (deadRef == null && newRef != null) foundLostReplaced = 0;
+                    if (deadRef != null && newRef == null) foundLostReplaced = 1;
+                    if (deadRef != null && newRef != null) foundLostReplaced = 2;
+                }
+            }
+        }
+
+        if (serviceListener != null) {
+            if (foundLostReplaced == 0) serviceListener.serviceFound();
+            else if (foundLostReplaced == 1) serviceListener.serviceLost();
+            else if (foundLostReplaced == 2) serviceListener.serviceReplaced();
+        }
+
+        return result;
+    }
+
+    public void close() {
+        if (open.compareAndSet(true, false)) {
+            ctx.removeServiceListener(this);
+
+            ServiceReference<?> deadRef;
+            synchronized (this) {
+                deadRef = ref.getAndSet(null);
+                service.set(null);
+            }
+            if (deadRef != null) {
+                serviceListener.serviceLost();
+                ctx.ungetService(deadRef);
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/jline/src/main/java/org/apache/felix/gogo/jline/package-info.java b/jline/src/main/java/org/apache/felix/gogo/jline/package-info.java
new file mode 100644
index 0000000..7a33720
--- /dev/null
+++ b/jline/src/main/java/org/apache/felix/gogo/jline/package-info.java
@@ -0,0 +1,34 @@
+/*
+ * 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.
+ */
+
+@org.osgi.annotation.bundle.Capability(
+    attribute = "implementation.name=gogo.jline",
+    namespace = "org.apache.felix.gogo",
+    name = "shell.implementation",
+    version = "1.0.0"
+)
+@Requirement(
+    effective = "active",
+    namespace = "org.apache.felix.gogo",
+    name = "command.implementation",
+    version = "1.0.0"
+)
+package org.apache.felix.gogo.jline;
+
+import org.osgi.annotation.bundle.Requirement;
diff --git a/jline/src/main/resources/gosh_profile b/jline/src/main/resources/gosh_profile
new file mode 100644
index 0000000..64061d8
--- /dev/null
+++ b/jline/src/main/resources/gosh_profile
@@ -0,0 +1,344 @@
+#
+# 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.
+#
+# default gosh_profile
+# only read if etc/gosh_profile doesn't exist relative to the System property
+# gosh.home or failing that the current directory.
+
+# catch all exceptions from this script to avoid it aborting startup
+try {
+
+  # ensure gogo commands are found first
+  SCOPE = 'gogo:*'
+
+  # add methods on BundleContext object as commands
+  addcommand context ${.context}
+
+  # add methods on System object as commands
+  addcommand system (((${.context} getClass) getClassLoader) loadClass java.lang.System)
+
+  # alias to print full stack trace
+  e = { $exception printStackTrace }
+
+  ## disable console auto-formatting of each result
+  #  you will then need to explicitly use the 'format' command
+  #  to print the result of commands that don't write to stdout.
+  #.Gogo.format = false
+
+  ## disable printing the formatted result of a command into pipelines
+  #.Format.Pipe = false
+
+  # set prompt
+  prompt = 'g! '
+  \#rprompt = { (new java.text.SimpleDateFormat \'$'\u001B\\[90m'\'HH:mm:ss) format (new Date) }
+  secondary-prompt-pattern = '%M%P > '
+  # could also be written
+  #  \#rprompt = { ${(qq)$(date +$'\u001B\[90m%T')} }
+
+
+  __option_not_present = {
+      res = true
+      opts = $argv
+      each $opts {
+          arg = $it
+          each ($.commandLine words) {
+              if { ($it toString) equals ($arg toString) } {
+                  res = false
+              }
+          }
+      }
+      $res
+  }
+
+  __load_class = {
+      (($.reader class) classLoader) loadClass $1
+  }
+
+  __set_unset_arguments = {
+      is_setopt = (($.commandLine words) get 0) equals "setopt"
+      enums = ((__load_class 'org.jline.reader.LineReader$Option') enumConstants)
+      candidates = new ArrayList
+      each $enums {
+          name = ${(GL)it/_/-}
+          is_set = ($.reader isSet $it)
+          neg = %(( if(is_setopt, is_set, not(is_set)) ))
+          if { $neg } {
+              name = "no-${name}"
+          }
+          if { not  { (($.commandLine words) subList 1 ($.commandLine wordIndex)) contains $name } } {
+              $candidates add (new org.jline.reader.Candidate $name $name (if { $neg } { "unset" } { "set" }) null null null true)
+          }
+      }
+      $candidates
+  }
+
+  setopt group
+
+  complete -c gogo:complete -e
+  complete -c gogo:complete -d "Edit command specific completions"
+  complete -c gogo:complete -s c -l command --description "Command to add completion to" -n '__option_not_present -c --command' -a '$.commands'
+  complete -c gogo:complete -s s -l short-option --description "Posix-style option to complete" -n '__option_not_present -s --short-option'
+  complete -c gogo:complete -s l -l long-option --description "GNU-style option to complete" -n '__option_not_present -l --long-option'
+  complete -c gogo:complete -s a -l arguments --description "A list of possible arguments" -n '__option_not_present -a --argument'
+  complete -c gogo:complete -s d -l description --description "Description of this completions" -n '__option_not_present -d --description'
+  complete -c gogo:complete -s h -l help --description "Display help and exit" -n '__option_not_present -h --help'
+  complete -c gogo:complete -s n -l condition --description "The completion should only be used if the specified command has a zero exit status"  -n '__option_not_present -n --condition'
+  complete -c gogo:complete -s e -l erase --description "Remove completion"  -n '__option_not_present -e --erase'
+
+  complete -c gogo:history -e
+  complete -c gogo:history -d "Show and manipulate command history"
+  complete -c gogo:history -l clear --description "Clear history" -n '__option_not_present --clear'
+  complete -c gogo:history -l save --description "Save history" -n '__option_not_present --save'
+
+  complete -c gogo:setopt -e
+  complete -c gogo:setopt -d "Set or view set shell options"
+  complete -c gogo:setopt -a '__set_unset_arguments'
+
+  complete -c gogo:unsetopt -e
+  complete -c gogo:unsetopt -d "Unset or view unset shell options"
+  complete -c gogo:unsetopt -a '__set_unset_arguments'
+
+  complete -c gogo:cat -e
+  complete -c gogo:cat -d "Concatenate and print files"
+  complete -c gogo:cat -s n "Number the output lines, starting at 1"
+  complete -c gogo:cat -a '__files'
+
+  complete -c gogo:pwd -e
+  complete -c gogo:pwd -d "Get current directory"
+
+  complete -c gogo:ls -e
+  complete -c gogo:ls -d "List files"
+
+  complete -c gogo:cd -e
+  complete -c gogo:cd -d "Change current directory"
+  complete -c gogo:cd -a 'wi = ($.commandLine wordIndex); if { %(wi==1) } { __directories } { [ ] }'
+
+  complete -c gogo:sleep -e
+  complete -c gogo:sleep -d "Pause execution for the specified amount of time"
+
+  complete -c gogo:echo -e
+  complete -c gogo:echo -d "Write arguments to the standard output"
+  complete -c gogo:echo -s n -d "No trailing new line"
+
+  complete -c gogo:grep -e
+  complete -c gogo:grep -d "File pattern searcher"
+  # TODO
+
+  complete -c gogo:sort -e
+  complete -c gogo:sort -d "Sort lines of text files"
+  # TODO
+
+  complete -c gogo:gosh -e
+  complete -c gogo:gosh -d "Execute script with arguments in a new session"
+  # TODO
+
+  complete -c gogo:sh -e
+  complete -c gogo:sh -d "Execute script with arguments in a new session"
+  # TODO
+
+  complete -c gogo:source -e
+  complete -c gogo:source -d "Execute script with arguments"
+  # TODO
+
+  # TODO: format getopt new set tac type addcommand removeCommand eval
+
+  complete -c gogo:each -e
+  complete -c gogo:each -d "Loop and execute script on the specified elements"
+
+  complete -c gogo:if -e
+  complete -c gogo:if -d "Conditionaly execute a script"
+
+  complete -c gogo:not -e
+  complete -c gogo:not -d "Negates the result of a script"
+
+  complete -c gogo:throw -e
+  complete -c gogo:throw -d "Throws an exception"
+
+  complete -c gogo:try -e
+  complete -c gogo:try -d "Try executing a script and catch any exception"
+
+  complete -c gogo:until -e
+  complete -c gogo:until -d "Loop and execute script until a condition is satisfied"
+
+  complete -c gogo:while -e
+  complete -c gogo:while -d "Loop and execute script while a condition is satisfied"
+
+  complete -c gogo:less -e
+  complete -c gogo:less -d "File pager"
+  complete -c gogo:less -s e -l quit-at-eof --description "Exit on second EOF"
+  complete -c gogo:less -s E -l QUIT-AT-EOF --description "Exit on EOF"
+  complete -c gogo:less -s q -l quiet -l silent --description "Silent mode"
+  complete -c gogo:less -s Q -l QUIET -l SILENT --description "Completely  silent"
+  complete -c gogo:less -s S -l chop-long-lines --description "Do not fold long lines"
+  complete -c gogo:less -s i -l ignore-case --description "Search ignores lowercase case"
+  complete -c gogo:less -s I -l IGNORE-CASE --description "Search ignores all case"
+  complete -c gogo:less -s x -l tabs --description "Set tab stops"
+  complete -c gogo:less -s N -l LINE-NUMBERS --description "Display line number for each line"
+  complete -c gogo:less -a '__files'
+
+  complete -c gogo:nano -e
+  complete -c gogo:nano -d "File editor"
+  complete -c gogo:nano -a '__files'
+
+  complete -c gogo:keymap -e
+  complete -c gogo:keymap -d "Manipulate keymaps"
+  complete -c gogo:keymap -s N --description "Create a new keymap" -n '__option_not_present -N -d -D -l -r -s -A'
+  complete -c gogo:keymap -s d --description "Delete existing keymaps and reset to default state" -n '__option_not_present -N -d -D -l -r -s -A'
+  complete -c gogo:keymap -s D --description "Delete named keymaps" -n '__option_not_present -N -d -D -l -r -s -A'
+  complete -c gogo:keymap -s l --description "List existing keymap names" -n '__option_not_present -N -d -D -l -r -s -A'
+  complete -c gogo:keymap -s r --description "Unbind specified in-strings" -n '__option_not_present -N -d -D -l -r -s -A'
+  complete -c gogo:keymap -s s --description "Bind each in-string to each out-string" -n '__option_not_present -N -d -D -l -r -s -A'
+  complete -c gogo:keymap -s A --description "Create alias to keymap" -n '__option_not_present -N -d -D -l -r -s -A'
+  complete -c gogo:keymap -s e --description "Select emacs keymap and bind it to main" -n '__option_not_present -e -a -v -M'
+  complete -c gogo:keymap -s v --description "Select viins keymap and bind it to main" -n '__option_not_present -e -a -v -M'
+  complete -c gogo:keymap -s a --description "Select vicmd keymap" -n '__option_not_present -e -a -v -M'
+  complete -c gogo:keymap -s M --description "Specify keymap to select" -n '__option_not_present -e -a -v -M' -a '(keymap -l | tac) split " "'
+  complete -c gogo:keymap -s R --description "Interpret in-strings as ranges"
+  complete -c gogo:keymap -s p --description "List bindings which have given key sequence as a a prefix"
+  complete -c gogo:keymap -s L --description "Output in form of keymap commands"
+
+  complete -c gogo:widget -e
+  complete -c gogo:widget -d "Manipulate widgets"
+  complete -c gogo:widget -s N --description "Create a new widget" -n '__option_not_present -N -A -D -U -l'
+  complete -c gogo:widget -s A --description "Create alias to widget" -n '__option_not_present -N -A -D -U -l'
+  complete -c gogo:widget -s D --description "Delete widgets" -n '__option_not_present -N -A -D -U -l'
+  complete -c gogo:widget -s U --description "Push characters to the stack" -n '__option_not_present -N -A -D -U -l'
+  complete -c gogo:widget -s l --description "List user-defined widgets" -n '__option_not_present -N -A -D -U -l'
+  complete -c gogo:widget -s a --description "With -l, list all widgets" -n '__option_not_present -l'
+
+  complete -c gogo:telnetd -e
+  complete -c gogo:telnetd -d "Telnet daemon"
+  complete -c gogo:telnetd -s i -l ip --description "Listening IP interface" -n '__option_not_present -i --ip'
+  complete -c gogo:telnetd -s p -l port --description "Listening IP port" -n '__option_not_present -p --port'
+  complete -c gogo:telnetd -a '[start stop status]'
+
+  complete -c gogo:sshd -e
+  complete -c gogo:sshd -d "SSH daemon"
+  complete -c gogo:sshd -s i -l ip --description "Listening IP interface" -n '__option_not_present -i --ip'
+  complete -c gogo:sshd -s p -l port --description "Listening IP port" -n '__option_not_present -p --port'
+  complete -c gogo:sshd -a '[start stop status]'
+
+  complete -c gogo:tmux -e
+  complete -c gogo:tmux -d "Terminal multiplexer"
+
+  complete -c gogo:bg -e
+  complete -c gogo:bg -d "Put job in background"
+
+  complete -c gogo:fg -e
+  complete -c gogo:fg -d "Put job in foreground"
+
+  complete -c gogo:jobs -e
+  complete -c gogo:jobs -d "List jobs"
+
+  complete -c gogo:clear -e
+  complete -c gogo:clear -d "Clear screen"
+
+  complete -c gogo:head -e
+  complete -c gogo:head -d "Displays first lines of file"
+  complete -c gogo:head -s n -l lines --description "Print line counts"
+  complete -c gogo:head -s c -l bytes --description "Print byte counts"
+  complete -c gogo:head -a '__files'
+
+  complete -c gogo:tail -e
+  complete -c gogo:tail -d "Displays last lines of file"
+  complete -c gogo:tail -s q -l quiet --description "Suppress headers when printing multiple sources"
+  complete -c gogo:tail -s f -l follow --description "Do not stop at end of file"
+  complete -c gogo:tail -s F -l FOLLOW --description "Follow and check for file renaming or rotation"
+  complete -c gogo:tail -s n -l lines --description "Number of lines to print"
+  complete -c gogo:tail -s c -l bytes --description "Number of bytes to print"
+  complete -c gogo:tail -a '__files'
+
+  complete -c gogo:date -e
+  complete -c gogo:date -d "Display date and time"
+  complete -c gogo:date -s u --description "Use UTC"
+  complete -c gogo:date -s r --description "Print the date represented by 'seconds' since January 1, 1970"
+  complete -c gogo:date -s v --description "Adjust date"
+  complete -c gogo:date -s f --description "Use 'input_fmt' to parse 'new_date'"
+
+  complete -c gogo:wc -e
+  complete -c gogo:wc -d "Word, line, character, and byte count"
+  complete -c gogo:wc -s n -l lines --description "Print line count"
+  complete -c gogo:wc -s c -l bytes --description "Print byte count"
+  complete -c gogo:wc -s m -l chars --description "Print character count"
+  complete -c gogo:wc -s w -l words --description "Print word count"
+  complete -c gogo:wc -a '__files'
+
+  __get_scr_components = {
+    list = [ ]
+    scrref = ($.context getServiceReference org.osgi.service.component.runtime.ServiceComponentRuntime)
+    scr = ($.context getService $scrref)
+    each ($scr getComponentDescriptionDTOs ($.context bundles)) {
+      $list add ((($it getClass) getField "name") get $it)
+    }
+    $.context ungetService $scrref
+    $list
+  }
+  __get_bundles_with_scr_components = {
+    list = [ ]
+    scrref = ($.context getServiceReference org.osgi.service.component.runtime.ServiceComponentRuntime)
+    scr = ($.context getService $scrref)
+    each ($.context bundles) {
+      if { ($scr getComponentDescriptionDTOs $it) isEmpty } { } {
+        $list add ($it symbolicName)
+      }
+    }
+    $.context ungetService $scrref
+    $list
+  }
+
+  complete -c scr:config -e
+  complete -c scr:config -d "Show the current SCR configuration"
+
+  complete -c scr:disable -e
+  complete -c scr:disable -d "Disable an enabled component"
+  complete -c scr:disable -a '__get_scr_components'
+
+  complete -c scr:enable -e
+  complete -c scr:enable -d "Enable an disabled component"
+  complete -c scr:enable -a '__get_scr_components'
+
+  complete -c scr:info -e
+  complete -c scr:info -d "Dump information of a component or component configuration"
+  complete -c scr:info -a '__get_scr_components'
+
+  complete -c scr:list -e
+  complete -c scr:list -d "List component configurations of a specific bundle"
+  complete -c scr:list -a '__get_bundles_with_scr_components'
+
+  # print welcome message
+  __resolve_uri = {
+    uri = $1
+    path = $2
+    if { "$uri" startsWith "jar:" } /* then */ {
+      idx = ("$uri" indexOf "!")
+      p1 = ("$uri" substring 0 $idx)
+      p2 = "!"
+      p3 = (new java.net.URI ("$uri" substring %(idx+1))) resolve $path
+      "$p1$p2$p3"
+    } /* else */ {
+      $uri resolve $path
+    }
+  }
+
+  # print welcome message, unless we're explicitly told not to...
+  if { $.gosh_quiet } { } { cat (new java.net.URL ($0 toURL) motd) }
+} {
+    echo "$0: ERROR: $exception"
+}
+
+# end
diff --git a/jline/src/main/resources/motd b/jline/src/main/resources/motd
new file mode 100644
index 0000000..01954f9
--- /dev/null
+++ b/jline/src/main/resources/motd
@@ -0,0 +1,3 @@
+____________________________
+Welcome to Apache Felix Gogo
+
diff --git a/jline/src/test/java/org/apache/felix/gogo/jline/AbstractParserTest.java b/jline/src/test/java/org/apache/felix/gogo/jline/AbstractParserTest.java
new file mode 100644
index 0000000..a785c2e
--- /dev/null
+++ b/jline/src/test/java/org/apache/felix/gogo/jline/AbstractParserTest.java
@@ -0,0 +1,75 @@
+/*
+ * 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.felix.gogo.jline;
+
+import java.io.FilterInputStream;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.PrintStream;
+
+import org.apache.felix.gogo.runtime.threadio.ThreadIOImpl;
+import org.junit.After;
+import org.junit.Before;
+
+public abstract class AbstractParserTest {
+
+    private ThreadIOImpl threadIO;
+    private InputStream sin;
+    private PrintStream sout;
+    private PrintStream serr;
+
+    @Before
+    public void setUp() {
+        sin = new NoCloseInputStream(System.in);
+        sout = new NoClosePrintStream(System.out);
+        serr = new NoClosePrintStream(System.err);
+        threadIO = new ThreadIOImpl();
+        threadIO.start();
+    }
+
+    @After
+    public void tearDown() {
+        threadIO.stop();
+    }
+
+    public class Context extends org.apache.felix.gogo.jline.Context {
+        public Context() {
+            super(AbstractParserTest.this.threadIO, sin, sout, serr);
+        }
+    }
+
+    private static class NoCloseInputStream extends FilterInputStream {
+        public NoCloseInputStream(InputStream in) {
+            super(in);
+        }
+        @Override
+        public void close() {
+        }
+    }
+
+    private static class NoClosePrintStream extends PrintStream {
+        public NoClosePrintStream(OutputStream out) {
+            super(out);
+        }
+        @Override
+        public void close() {
+        }
+    }
+
+}
diff --git a/jline/src/test/java/org/apache/felix/gogo/jline/BaseConvertersTest.java b/jline/src/test/java/org/apache/felix/gogo/jline/BaseConvertersTest.java
new file mode 100644
index 0000000..cae7a44
--- /dev/null
+++ b/jline/src/test/java/org/apache/felix/gogo/jline/BaseConvertersTest.java
@@ -0,0 +1,61 @@
+/*
+ * 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.felix.gogo.jline;
+
+import org.apache.felix.service.command.CommandSession;
+import org.apache.felix.service.command.Function;
+import org.junit.Test;
+
+import java.util.List;
+
+import static org.junit.Assert.assertEquals;
+
+public class BaseConvertersTest {
+
+    @Test
+    public void testFunctionProxy() throws Exception {
+        Function function = new Function() {
+            @Override
+            public Object execute(CommandSession session, List<Object> arguments) {
+                return "Hello ";
+            }
+            public String toString() {
+                return "MyFunction";
+            }
+        };
+        MyType myType = (MyType) new BaseConverters().convert(MyType.class, function);
+        assertEquals("MyFunction", myType.toString());
+        assertEquals("Hello ", myType.run(null));
+        assertEquals("World !", myType.hello());
+    }
+
+    @FunctionalInterface
+    public interface MyType {
+
+        String toString();
+
+        Object run(List<Object> args);
+
+        default String hello() {
+            return "World !";
+        }
+
+    }
+
+}
diff --git a/jline/src/test/java/org/apache/felix/gogo/jline/Context.java b/jline/src/test/java/org/apache/felix/gogo/jline/Context.java
new file mode 100644
index 0000000..45040dc
--- /dev/null
+++ b/jline/src/test/java/org/apache/felix/gogo/jline/Context.java
@@ -0,0 +1,103 @@
+/*
+ * 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.felix.gogo.jline;
+
+import java.io.InputStream;
+import java.io.PrintStream;
+import java.nio.file.Path;
+
+import org.apache.felix.gogo.runtime.CommandProcessorImpl;
+import org.apache.felix.service.command.CommandSession;
+import org.apache.felix.service.threadio.ThreadIO;
+
+public class Context extends CommandProcessorImpl
+{
+    public static final String EMPTY = "";
+    
+    private final CommandSession session;
+
+    public Context(ThreadIO threadio, InputStream in, PrintStream out, PrintStream err)
+    {
+        super(threadio);
+        Shell shell = new Shell(new MyContext(), this);
+        addCommand("gogo", this, "addCommand");
+        addCommand("gogo", this, "removeCommand");
+        addCommand("gogo", this, "eval");
+        register(this, new Builtin(), Builtin.functions);
+        register(this, new Procedural(), Procedural.functions);
+        register(this, new Posix(this), Posix.functions);
+        register(this, shell, Shell.functions);
+        session = createSession(in, out, err);
+    }
+
+    static void register(CommandProcessorImpl processor, Object target, String[] functions) {
+        for (String function : functions) {
+            processor.addCommand("gogo", target, function);
+        }
+    }
+
+    private static class MyContext implements Shell.Context {
+
+        public String getProperty(String name) {
+            return System.getProperty(name);
+        }
+
+        public void exit() {
+            System.exit(0);
+        }
+    }
+
+    public Object execute(CharSequence source) throws Exception
+    {
+        Object result = new Exception();
+        try
+        {
+            return result = session.execute(source);
+        }
+        finally
+        {
+            System.err.println("execute<" + source + "> = ("
+                + (null == result ? "Null" : result.getClass().getSimpleName()) + ")("
+                + result + ")\n");
+        }
+    }
+
+    public void addCommand(String function, Object target)
+    {
+        addCommand("test", target, function);
+    }
+
+    public Object set(String name, Object value)
+    {
+        return session.put(name, value);
+    }
+
+    public Object get(String name)
+    {
+        return session.get(name);
+    }
+
+    public void currentDir(Path path) {
+        session.currentDir(path);
+    }
+
+    public Path currentDir() {
+        return session.currentDir();
+    }
+}
diff --git a/jline/src/test/java/org/apache/felix/gogo/jline/ParserTest.java b/jline/src/test/java/org/apache/felix/gogo/jline/ParserTest.java
new file mode 100644
index 0000000..5d19905
--- /dev/null
+++ b/jline/src/test/java/org/apache/felix/gogo/jline/ParserTest.java
@@ -0,0 +1,59 @@
+/*
+ * 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.felix.gogo.jline;
+
+import org.jline.reader.CompletingParsedLine;
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+
+public class ParserTest {
+
+    @Test
+    public void testEscapedWord() {
+        Parser parser = new Parser();
+        CompletingParsedLine line = (CompletingParsedLine) parser.parse("foo second\\ param \"quoted param\"", 15);
+        assertNotNull(line);
+        assertNotNull(line.words());
+        assertEquals("foo second\\ param \"quoted param\"", line.line());
+        assertEquals(15, line.cursor());
+        assertEquals(3, line.words().size());
+        assertEquals("second param", line.word());
+        assertEquals(10, line.wordCursor());
+        assertEquals(11, line.rawWordCursor());
+        assertEquals(13, line.rawWordLength());
+    }
+
+    @Test
+    public void testQuotedWord() {
+        Parser parser = new Parser();
+        CompletingParsedLine line = (CompletingParsedLine) parser.parse("foo second\\ param \"quoted param\"", 20);
+        assertNotNull(line);
+        assertNotNull(line.words());
+        assertEquals("foo second\\ param \"quoted param\"", line.line());
+        assertEquals(20, line.cursor());
+        assertEquals(3, line.words().size());
+        assertEquals("quoted param", line.word());
+        assertEquals(1, line.wordCursor());
+        assertEquals(2, line.rawWordCursor());
+        assertEquals(14, line.rawWordLength());
+    }
+
+}
diff --git a/jline/src/test/java/org/apache/felix/gogo/jline/PosixTest.java b/jline/src/test/java/org/apache/felix/gogo/jline/PosixTest.java
new file mode 100644
index 0000000..6e3034f
--- /dev/null
+++ b/jline/src/test/java/org/apache/felix/gogo/jline/PosixTest.java
@@ -0,0 +1,53 @@
+/*
+ * 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.felix.gogo.jline;
+
+import org.junit.Test;
+
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.Reader;
+import java.io.StringWriter;
+
+import static org.junit.Assert.assertEquals;
+
+public class PosixTest extends AbstractParserTest {
+
+    @Test
+    public void testGrepWithColoredInput() throws Exception {
+        Context context = new Context();
+        context.addCommand("echo", new Posix(context));
+        context.addCommand("grep", new Posix(context));
+        context.addCommand("tac", this);
+
+        Object res = context.execute("echo \"  \\u001b[1mbold\\u001b[0m  la\" | grep la | tac");
+        assertEquals("  \u001b[1mbold\u001b[0m  la", res);
+    }
+
+    public String tac() throws IOException {
+        StringWriter sw = new StringWriter();
+        Reader rdr = new InputStreamReader(System.in);
+        char[] buf = new char[1024];
+        int len;
+        while ((len = rdr.read(buf)) >= 0) {
+            sw.write(buf, 0, len);
+        }
+        return sw.toString();
+    }
+}
diff --git a/jline/src/test/java/org/apache/felix/gogo/jline/ShellTest.java b/jline/src/test/java/org/apache/felix/gogo/jline/ShellTest.java
new file mode 100644
index 0000000..74a3127
--- /dev/null
+++ b/jline/src/test/java/org/apache/felix/gogo/jline/ShellTest.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.felix.gogo.jline;
+
+import java.util.Arrays;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+public class ShellTest extends AbstractParserTest {
+
+    @Test
+    public void testAssignmentWithEcho() throws Exception {
+        Context context = new Context();
+        context.execute("a = \"foo\"");
+        Assert.assertEquals("foo", context.get("a"));
+        context.execute("a = $(echo bar)");
+        Assert.assertEquals("bar", context.get("a"));
+    }
+
+    @Test
+    public void testLoopBreak() throws Exception {
+        Context context = new Context();
+        Object result = context.execute("$(each {1..10} { i = $it; if { %(i >= 5) } { break } ; echo $i })");
+        Assert.assertEquals("1\n2\n3\n4", result);
+    }
+
+    @Test
+    public void testJobIds() throws Exception {
+        Context context = new Context();
+        // TODO: not than in zsh, the same thing is achieved using
+        // TODO:     ${${${(@f)"$(jobs)"}%]*}#*\[}
+//        Object result = context.execute("sleep 1 & sleep 1 & ${${${(f)\"$(jobs)\"}%']*'}#'*\\['}");
+        Object result = context.execute("sleep 1 & sleep 1 & ${${${(f)$(jobs)}%\\]*}#*\\[}");
+        Assert.assertEquals(Arrays.asList("1", "2"), result);
+    }
+
+}
diff --git a/jline/src/test/java/org/apache/felix/gogo/jline/ssh/ShellCommand.java b/jline/src/test/java/org/apache/felix/gogo/jline/ssh/ShellCommand.java
new file mode 100644
index 0000000..736cb4b
--- /dev/null
+++ b/jline/src/test/java/org/apache/felix/gogo/jline/ssh/ShellCommand.java
@@ -0,0 +1,136 @@
+/*
+ * 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.felix.gogo.jline.ssh;
+
+import java.io.CharArrayWriter;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.io.PrintStream;
+import java.io.Reader;
+import java.util.Map;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import org.apache.felix.service.command.CommandProcessor;
+import org.apache.felix.service.command.CommandSession;
+import org.apache.sshd.server.Command;
+import org.apache.sshd.server.Environment;
+import org.apache.sshd.server.ExitCallback;
+import org.apache.sshd.server.SessionAware;
+import org.apache.sshd.server.session.ServerSession;
+
+public class ShellCommand implements Command, Runnable, SessionAware {
+
+    public static final String SHELL_INIT_SCRIPT = "karaf.shell.init.script";
+    public static final String EXEC_INIT_SCRIPT = "karaf.exec.init.script";
+
+    private static final Logger LOGGER = Logger.getLogger(ShellCommand.class.getName());
+
+    private String command;
+    private InputStream in;
+    private OutputStream out;
+    private OutputStream err;
+    private ExitCallback callback;
+    @SuppressWarnings("unused")
+    private ServerSession session;
+    private CommandProcessor processor;
+    private Environment env;
+
+    public ShellCommand(CommandProcessor processor, String command) {
+        this.processor = processor;
+        this.command = command;
+    }
+
+    public void setInputStream(InputStream in) {
+        this.in = in;
+    }
+
+    public void setOutputStream(OutputStream out) {
+        this.out = out;
+    }
+
+    public void setErrorStream(OutputStream err) {
+        this.err = err;
+    }
+
+    public void setExitCallback(ExitCallback callback) {
+        this.callback = callback;
+    }
+
+    public void setSession(ServerSession session) {
+        this.session = session;
+    }
+
+    public void start(final Environment env) {
+        this.env = env;
+        new Thread(this).start();
+    }
+
+    public void run() {
+        int exitStatus = 0;
+        try {
+            final CommandSession session = processor.createSession(in, new PrintStream(out), new PrintStream(err));
+            for (Map.Entry<String, String> e : env.getEnv().entrySet()) {
+                session.put(e.getKey(), e.getValue());
+            }
+            try {
+                String scriptFileName = System.getProperty(EXEC_INIT_SCRIPT);
+                if (scriptFileName == null) {
+                    scriptFileName = System.getProperty(SHELL_INIT_SCRIPT);
+                }
+                executeScript(scriptFileName, session);
+                session.execute(command);
+            } catch (Throwable t) {
+                exitStatus = 1;
+                t.printStackTrace();
+            }
+        } catch (Exception e) {
+            exitStatus = 1;
+            LOGGER.log(Level.SEVERE, "Unable to start shell", e);
+        } finally {
+            ShellFactoryImpl.close(in, out, err);
+            callback.onExit(exitStatus);
+        }
+    }
+
+    public void destroy() {
+    }
+
+    private void executeScript(String scriptFileName, CommandSession session) {
+        if (scriptFileName != null) {
+            File scriptFile = new File(scriptFileName);
+            try (Reader 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.log(Level.FINE, "Error in initialization script", e);
+            }
+            // Ignore
+        }
+    }
+
+}
diff --git a/jline/src/test/java/org/apache/felix/gogo/jline/ssh/ShellCommandFactory.java b/jline/src/test/java/org/apache/felix/gogo/jline/ssh/ShellCommandFactory.java
new file mode 100644
index 0000000..d0a0a45
--- /dev/null
+++ b/jline/src/test/java/org/apache/felix/gogo/jline/ssh/ShellCommandFactory.java
@@ -0,0 +1,37 @@
+/*
+ * 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.felix.gogo.jline.ssh;
+
+import org.apache.felix.service.command.CommandProcessor;
+import org.apache.sshd.server.Command;
+import org.apache.sshd.server.CommandFactory;
+
+public class ShellCommandFactory implements CommandFactory {
+
+    private CommandProcessor processor;
+
+    public ShellCommandFactory(CommandProcessor processor) {
+        this.processor = processor;
+    }
+
+    public Command createCommand(String command) {
+        return new ShellCommand(processor, command);
+    }
+
+}
diff --git a/jline/src/test/java/org/apache/felix/gogo/jline/ssh/ShellFactoryImpl.java b/jline/src/test/java/org/apache/felix/gogo/jline/ssh/ShellFactoryImpl.java
new file mode 100644
index 0000000..91c40e7
--- /dev/null
+++ b/jline/src/test/java/org/apache/felix/gogo/jline/ssh/ShellFactoryImpl.java
@@ -0,0 +1,274 @@
+/*
+ * 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.felix.gogo.jline.ssh;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.PrintStream;
+import java.util.Map;
+
+import org.apache.felix.gogo.jline.Shell;
+import org.apache.felix.gogo.jline.Shell.Context;
+import org.apache.felix.service.command.CommandProcessor;
+import org.apache.felix.service.command.CommandSession;
+import org.apache.sshd.common.Factory;
+import org.apache.sshd.common.channel.PtyMode;
+import org.apache.sshd.server.Command;
+import org.apache.sshd.server.Environment;
+import org.apache.sshd.server.ExitCallback;
+import org.apache.sshd.server.SessionAware;
+import org.apache.sshd.server.Signal;
+import org.apache.sshd.server.session.ServerSession;
+import org.jline.terminal.Attributes;
+import org.jline.terminal.Attributes.ControlChar;
+import org.jline.terminal.Attributes.InputFlag;
+import org.jline.terminal.Attributes.LocalFlag;
+import org.jline.terminal.Attributes.OutputFlag;
+import org.jline.terminal.Size;
+import org.jline.terminal.Terminal;
+import org.jline.terminal.TerminalBuilder;
+
+/**
+ * SSHD {@link org.apache.sshd.server.Command} factory which provides access to
+ * Shell.
+ */
+public class ShellFactoryImpl implements Factory<Command> {
+    private final CommandProcessor processor;
+
+    public ShellFactoryImpl(CommandProcessor processor) {
+        this.processor = processor;
+    }
+
+    private static void flush(OutputStream... streams) {
+        for (OutputStream s : streams) {
+            try {
+                s.flush();
+            } catch (IOException e) {
+                // Ignore
+            }
+        }
+    }
+
+    static void close(Closeable... closeables) {
+        for (Closeable c : closeables) {
+            try {
+                c.close();
+            } catch (IOException e) {
+                // Ignore
+            }
+        }
+    }
+
+    public Command create() {
+        return new ShellImpl();
+    }
+
+    public class ShellImpl implements Command, SessionAware {
+        private InputStream in;
+
+        private OutputStream out;
+
+        private OutputStream err;
+
+        private ExitCallback callback;
+
+        @SuppressWarnings("unused")
+        private ServerSession session;
+
+        private boolean closed;
+
+        public void setInputStream(final InputStream in) {
+            this.in = in;
+        }
+
+        public void setOutputStream(final OutputStream out) {
+            this.out = out;
+        }
+
+        public void setErrorStream(final OutputStream err) {
+            this.err = err;
+        }
+
+        public void setExitCallback(ExitCallback callback) {
+            this.callback = callback;
+        }
+
+        public void setSession(ServerSession session) {
+            this.session = session;
+        }
+
+        public void start(final Environment env) throws IOException {
+            try {
+                new Thread(() -> {
+                    try {
+                        ShellImpl.this.run(env);
+                    } catch (Throwable t) {
+                        t.printStackTrace();
+                    }
+                }).start();
+            } catch (Exception e) {
+                throw (IOException) new IOException("Unable to start shell", e);
+            }
+        }
+
+        public void run(Environment env) {
+            try {
+                Terminal terminal = TerminalBuilder.builder()
+                        .name("gogo")
+                        .type(env.getEnv().get("TERM"))
+                        .system(false)
+                        .streams(in, out)
+                        .build();
+                terminal.setSize(new Size(Integer.parseInt(env.getEnv().get("COLUMNS")),
+                        Integer.parseInt(env.getEnv().get("LINES"))));
+                Attributes attr = terminal.getAttributes();
+                for (Map.Entry<PtyMode, Integer> e : env.getPtyModes().entrySet()) {
+                    switch (e.getKey()) {
+                        case VINTR:
+                            attr.setControlChar(ControlChar.VINTR, e.getValue());
+                            break;
+                        case VQUIT:
+                            attr.setControlChar(ControlChar.VQUIT, e.getValue());
+                            break;
+                        case VERASE:
+                            attr.setControlChar(ControlChar.VERASE, e.getValue());
+                            break;
+                        case VKILL:
+                            attr.setControlChar(ControlChar.VKILL, e.getValue());
+                            break;
+                        case VEOF:
+                            attr.setControlChar(ControlChar.VEOF, e.getValue());
+                            break;
+                        case VEOL:
+                            attr.setControlChar(ControlChar.VEOL, e.getValue());
+                            break;
+                        case VEOL2:
+                            attr.setControlChar(ControlChar.VEOL2, e.getValue());
+                            break;
+                        case VSTART:
+                            attr.setControlChar(ControlChar.VSTART, e.getValue());
+                            break;
+                        case VSTOP:
+                            attr.setControlChar(ControlChar.VSTOP, e.getValue());
+                            break;
+                        case VSUSP:
+                            attr.setControlChar(ControlChar.VSUSP, e.getValue());
+                            break;
+                        case VDSUSP:
+                            attr.setControlChar(ControlChar.VDSUSP, e.getValue());
+                            break;
+                        case VREPRINT:
+                            attr.setControlChar(ControlChar.VREPRINT, e.getValue());
+                            break;
+                        case VWERASE:
+                            attr.setControlChar(ControlChar.VWERASE, e.getValue());
+                            break;
+                        case VLNEXT:
+                            attr.setControlChar(ControlChar.VLNEXT, e.getValue());
+                            break;
+                        /*
+                        case VFLUSH:
+                            attr.setControlChar(ControlChar.VMIN, e.getValue());
+                            break;
+                        case VSWTCH:
+                            attr.setControlChar(ControlChar.VTIME, e.getValue());
+                            break;
+                        */
+                        case VSTATUS:
+                            attr.setControlChar(ControlChar.VSTATUS, e.getValue());
+                            break;
+                        case VDISCARD:
+                            attr.setControlChar(ControlChar.VDISCARD, e.getValue());
+                            break;
+                        case ECHO:
+                            attr.setLocalFlag(LocalFlag.ECHO, e.getValue() != 0);
+                            break;
+                        case ICANON:
+                            attr.setLocalFlag(LocalFlag.ICANON, e.getValue() != 0);
+                            break;
+                        case ISIG:
+                            attr.setLocalFlag(LocalFlag.ISIG, e.getValue() != 0);
+                            break;
+                        case ICRNL:
+                            attr.setInputFlag(InputFlag.ICRNL, e.getValue() != 0);
+                            break;
+                        case INLCR:
+                            attr.setInputFlag(InputFlag.INLCR, e.getValue() != 0);
+                            break;
+                        case IGNCR:
+                            attr.setInputFlag(InputFlag.IGNCR, e.getValue() != 0);
+                            break;
+                        case OCRNL:
+                            attr.setOutputFlag(OutputFlag.OCRNL, e.getValue() != 0);
+                            break;
+                        case ONLCR:
+                            attr.setOutputFlag(OutputFlag.ONLCR, e.getValue() != 0);
+                            break;
+                        case ONLRET:
+                            attr.setOutputFlag(OutputFlag.ONLRET, e.getValue() != 0);
+                            break;
+                        case OPOST:
+                            attr.setOutputFlag(OutputFlag.OPOST, e.getValue() != 0);
+                            break;
+                        default:
+                    }
+                }
+                terminal.setAttributes(attr);
+                PrintStream pout = new PrintStream(terminal.output());
+                final CommandSession session = processor.createSession(terminal.input(), pout, pout);
+                session.put(Shell.VAR_TERMINAL, terminal);
+                for (Map.Entry<String, String> e : env.getEnv().entrySet()) {
+                    session.put(e.getKey(), e.getValue());
+                }
+                env.addSignalListener(signals -> {
+                    terminal.setSize(new Size(Integer.parseInt(env.getEnv().get("COLUMNS")),
+                                                Integer.parseInt(env.getEnv().get("LINES"))));
+                    terminal.raise(Terminal.Signal.WINCH);
+                }, Signal.WINCH);
+                Context context = new Context() {
+                    @Override
+                    public String getProperty(String name) {
+                        return System.getProperty(name);
+                    }
+
+                    @Override
+                    public void exit() {
+                        destroy();
+                    }
+                };
+                new Shell(context, processor).gosh(session, new String[]{"--login"});
+            } catch (Throwable t) {
+                t.printStackTrace();
+            }
+        }
+
+        public void destroy() {
+            if (!closed) {
+                closed = true;
+                ShellFactoryImpl.flush(out, err);
+                ShellFactoryImpl.close(in, out, err);
+                callback.onExit(0);
+            }
+        }
+
+    }
+
+}
diff --git a/jline/src/test/java/org/apache/felix/gogo/jline/ssh/Ssh.java b/jline/src/test/java/org/apache/felix/gogo/jline/ssh/Ssh.java
new file mode 100644
index 0000000..9fa10ae
--- /dev/null
+++ b/jline/src/test/java/org/apache/felix/gogo/jline/ssh/Ssh.java
@@ -0,0 +1,114 @@
+/*
+ * 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.felix.gogo.jline.ssh;
+
+import java.io.IOException;
+import java.util.Collections;
+import java.util.List;
+
+import org.apache.felix.service.command.CommandProcessor;
+import org.apache.felix.service.command.CommandSession;
+import org.apache.sshd.server.ServerBuilder;
+import org.apache.sshd.server.SshServer;
+import org.apache.sshd.server.keyprovider.SimpleGeneratorHostKeyProvider;
+import org.apache.sshd.server.scp.ScpCommandFactory;
+import org.apache.sshd.server.subsystem.sftp.SftpSubsystemFactory;
+import org.jline.builtins.Options;
+
+public class Ssh {
+
+    public static final String[] functions = {"sshd"};
+
+    private static final int defaultPort = 2022;
+
+    private final CommandProcessor processor;
+    private SshServer server;
+    @SuppressWarnings("unused")
+    private Object context;
+    private int port;
+    private String ip;
+
+    public Ssh(CommandProcessor processor) {
+        this.processor = processor;
+    }
+
+    public void sshd(CommandSession session, String[] argv) throws IOException {
+        final String[] usage = {"sshd - start an ssh server",
+                "Usage: sshd [-i ip] [-p port] start | stop | status",
+                "  -i --ip=INTERFACE        listen interface (default=127.0.0.1)",
+                "  -p --port=PORT           listen port (default=" + defaultPort + ")",
+                "  -? --help                show help"};
+
+        Options opt = Options.compile(usage).parse(argv);
+        List<String> args = opt.args();
+
+        if (opt.isSet("help") || args.isEmpty()) {
+            opt.usage(System.err);
+            return;
+        }
+
+        String command = args.get(0);
+
+        if ("start".equals(command)) {
+            if (server != null) {
+                throw new IllegalStateException("sshd is already running on port " + port);
+            }
+            ip = opt.get("ip");
+            port = opt.getNumber("port");
+            context = session.get(org.apache.felix.gogo.runtime.activator.Activator.CONTEXT);
+            start();
+            status();
+        } else if ("stop".equals(command)) {
+            if (server == null) {
+                throw new IllegalStateException("sshd is not running.");
+            }
+            stop();
+        } else if ("status".equals(command)) {
+            status();
+        } else {
+            throw opt.usageError("bad command: " + command);
+        }
+
+    }
+
+    private void status() {
+        if (server != null) {
+            System.out.println("sshd is running on " + ip + ":" + port);
+        } else {
+            System.out.println("sshd is not running.");
+        }
+    }
+
+    private void start() throws IOException {
+        server = ServerBuilder.builder().build();
+        server.setPort(port);
+        server.setHost(ip);
+        server.setShellFactory(new ShellFactoryImpl(processor));
+        server.setCommandFactory(new ScpCommandFactory.Builder().withDelegate(new ShellCommandFactory(processor)).build());
+        server.setSubsystemFactories(Collections.singletonList(
+                new SftpSubsystemFactory.Builder().build()
+        ));
+        server.setKeyPairProvider(new SimpleGeneratorHostKeyProvider());
+        server.start();
+    }
+
+    private void stop() throws IOException {
+        server.stop();
+    }
+}
diff --git a/jline/src/test/java/org/apache/felix/gogo/jline/telnet/BootException.java b/jline/src/test/java/org/apache/felix/gogo/jline/telnet/BootException.java
new file mode 100644
index 0000000..ee1f053
--- /dev/null
+++ b/jline/src/test/java/org/apache/felix/gogo/jline/telnet/BootException.java
@@ -0,0 +1,74 @@
+/*
+ * 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.
+ */
+
+/***
+ * Java TelnetD library (embeddable telnet daemon)
+ * Copyright (c) 2000-2005 Dieter Wimberger
+ * All rights reserved.
+ * <p/>
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ * Redistributions of source code must retain the above copyright notice,
+ * this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * <p/>
+ * Neither the name of the author nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ * <p/>
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDER AND CONTRIBUTORS ``AS
+ * IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+ * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ ***/
+
+package org.apache.felix.gogo.jline.telnet;
+
+/**
+ * Class that implements a BootException.<br>
+ * This exception will flag a broken boot process,
+ * which expresses startup failure and unavailabilty
+ * of telnet service for the container application.
+ *
+ * @author Dieter Wimberger
+ * @version 2.0 (16/07/2006)
+ */
+@SuppressWarnings("serial")
+public class BootException extends Exception {
+
+    /**
+     * Constructor method for a BootException.<br>
+     *
+     * @param msg String that contains an understandable failure message.
+     */
+    public BootException(String msg) {
+        super(msg);
+    }//constructor
+
+}//class BootException
\ No newline at end of file
diff --git a/jline/src/test/java/org/apache/felix/gogo/jline/telnet/Connection.java b/jline/src/test/java/org/apache/felix/gogo/jline/telnet/Connection.java
new file mode 100644
index 0000000..a6c87d7
--- /dev/null
+++ b/jline/src/test/java/org/apache/felix/gogo/jline/telnet/Connection.java
@@ -0,0 +1,255 @@
+/*
+ * 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.
+ */
+
+/***
+ * Java TelnetD library (embeddable telnet daemon)
+ * Copyright (c) 2000-2005 Dieter Wimberger
+ * All rights reserved.
+ * <p/>
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ * Redistributions of source code must retain the above copyright notice,
+ * this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * <p/>
+ * Neither the name of the author nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ * <p/>
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDER AND CONTRIBUTORS ``AS
+ * IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+ * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ ***/
+
+package org.apache.felix.gogo.jline.telnet;
+
+import java.util.List;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * Class that implements a connection with this telnet daemon.<br>
+ * It is derived from java.lang.Thread, which reflects the architecture
+ * constraint of one thread per connection. This might seem a waste of
+ * resources, but as a matter of fact sharing threads would require a
+ * far more complex imlementation, due to the fact that telnet is not a
+ * stateless protocol (i.e. alive throughout a session of multiple requests
+ * and responses).<br>
+ * Each Connection instance is created by the listeners ConnectionManager
+ * instance, making it part of a threadgroup and passing in an associated
+ * ConnectionData instance, that holds vital information about the connection.
+ * Be sure to take a look at their documention.<br>
+ * <p/>
+ * Once the thread has started and is running, it will get a login
+ * shell instance from the ShellManager and run passing its own reference.
+ *
+ * @author Dieter Wimberger
+ * @version 2.0 (16/07/2006)
+ * @see ConnectionManager
+ * @see ConnectionData
+ */
+public abstract class Connection
+        extends Thread {
+
+    private static final Logger LOG = Logger.getLogger(Connection.class.getName());
+    private static int number;            //unique number for a thread in the thread group
+    private boolean dead;
+    private List<ConnectionListener> listeners;
+
+    //Associations
+    private ConnectionData connectionData;    //associated information
+
+    /**
+     * Constructs a TelnetConnection by invoking its parent constructor
+     * and setting of various members.<br>
+     * Subsequently instantiates the whole i/o subsystem, negotiating
+     * telnet protocol level options etc.<br>
+     *
+     * @param tcg ThreadGroup that this instance is running in.
+     * @param cd  ConnectionData instance containing all vital information
+     *            of this connection.
+     * @see ConnectionData
+     */
+    public Connection(ThreadGroup tcg, ConnectionData cd) {
+        super(tcg, ("Connection" + (++number)));
+
+        connectionData = cd;
+        //init the connection listeners for events
+        //(there should actually be only one or two)
+        listeners = new CopyOnWriteArrayList<>();
+        dead = false;
+    }//constructor
+
+    /**
+     * Method overloaded to implement following behaviour:
+     * <ol>
+     * <li> On first entry, retrieve an instance of the configured
+     * login shell from the ShellManager and run it.
+     * <li> Handle a shell switch or close down disgracefully when
+     * problems (i.e. unhandled unchecked exceptions) occur in the
+     * running shell.
+     * </ol>
+     */
+    public void run() {
+        try {
+            doRun();
+
+        } catch (Exception ex) {
+            LOG.log(Level.SEVERE, "run()", ex); //Handle properly
+        } finally {
+            //call close if not dead already
+            if (!dead) {
+                close();
+            }
+        }
+        LOG.log(Level.FINE, "run():: Returning from " + this.toString());
+    }//run
+
+    protected abstract void doRun() throws Exception;
+
+    protected abstract void doClose() throws Exception;
+
+    /**
+     * Method to access the associated connection data.
+     *
+     * @return ConnectionData associated with the Connection instance.
+     * @see ConnectionData
+     */
+    public ConnectionData getConnectionData() {
+        return connectionData;
+    }//getConnectionData
+
+    /**
+     * Closes the connection and its underlying i/o and network
+     * resources.<br>
+     */
+    public synchronized void close() {
+        if (!dead) {
+            try {
+                //connection dead
+                dead = true;
+                //close i/o
+                doClose();
+            } catch (Exception ex) {
+                LOG.log(Level.SEVERE, "close()", ex);
+                //handle
+            }
+            try {
+                //close socket
+                connectionData.getSocket().close();
+            } catch (Exception ex) {
+                LOG.log(Level.SEVERE, "close()", ex);
+                //handle
+            }
+            try {
+                //register closed connection in ConnectionManager
+                connectionData.getManager().registerClosedConnection(this);
+            } catch (Exception ex) {
+                LOG.log(Level.SEVERE, "close()", ex);
+                //handle
+            }
+            try {
+                //try to interrupt it
+                interrupt();
+            } catch (Exception ex) {
+                LOG.log(Level.SEVERE, "close()", ex);
+                //handle
+            }
+
+
+            LOG.log(Level.FINE, "Closed " + this.toString() + " and inactive.");
+        }
+    }//close
+
+    /**
+     * Returns if a connection has been closed.<br>
+     *
+     * @return the state of the connection.
+     */
+    public boolean isActive() {
+        return !dead;
+    }//isClosed
+
+    /****** Event handling ****************/
+
+    /**
+     * Method that registers a ConnectionListener with the
+     * Connection instance.
+     *
+     * @param cl ConnectionListener to be registered.
+     * @see ConnectionListener
+     */
+    public void addConnectionListener(ConnectionListener cl) {
+        listeners.add(cl);
+    }//addConnectionListener
+
+    /**
+     * Method that removes a ConnectionListener from the
+     * Connection instance.
+     *
+     * @param cl ConnectionListener to be removed.
+     * @see ConnectionListener
+     */
+    public void removeConnectionListener(ConnectionListener cl) {
+        listeners.remove(cl);
+    }//removeConnectionListener
+
+
+    /**
+     * Method called by the io subsystem to pass on a
+     * "low-level" event. It will be properly delegated to
+     * all registered listeners.
+     *
+     * @param ce ConnectionEvent to be processed.
+     * @see ConnectionEvent
+     */
+    public void processConnectionEvent(ConnectionEvent ce) {
+        for (ConnectionListener cl : listeners) {
+            switch (ce.getType()) {
+                case CONNECTION_IDLE:
+                    cl.connectionIdle(ce);
+                    break;
+                case CONNECTION_TIMEDOUT:
+                    cl.connectionTimedOut(ce);
+                    break;
+                case CONNECTION_LOGOUTREQUEST:
+                    cl.connectionLogoutRequest(ce);
+                    break;
+                case CONNECTION_BREAK:
+                    cl.connectionSentBreak(ce);
+                    break;
+                case CONNECTION_TERMINAL_GEOMETRY_CHANGED:
+                    cl.connectionTerminalGeometryChanged(ce);
+            }
+        }
+    }//processConnectionEvent
+
+}//class Connection
diff --git a/jline/src/test/java/org/apache/felix/gogo/jline/telnet/ConnectionData.java b/jline/src/test/java/org/apache/felix/gogo/jline/telnet/ConnectionData.java
new file mode 100644
index 0000000..48ee8e4
--- /dev/null
+++ b/jline/src/test/java/org/apache/felix/gogo/jline/telnet/ConnectionData.java
@@ -0,0 +1,464 @@
+/*
+ * 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.
+ */
+
+/***
+ * Java TelnetD library (embeddable telnet daemon)
+ * Copyright (c) 2000-2005 Dieter Wimberger
+ * All rights reserved.
+ * <p/>
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ * Redistributions of source code must retain the above copyright notice,
+ * this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * <p/>
+ * Neither the name of the author nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ * <p/>
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDER AND CONTRIBUTORS ``AS
+ * IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+ * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ ***/
+
+package org.apache.felix.gogo.jline.telnet;
+
+import java.net.InetAddress;
+import java.net.Socket;
+import java.util.HashMap;
+import java.util.Locale;
+import java.util.Map;
+
+/**
+ * An utility class that is used to store and allow retrieval
+ * of all data associated with a connection.
+ *
+ * @author Dieter Wimberger
+ * @version 2.0 (16/07/2006)
+ * @see Connection
+ */
+public class ConnectionData {
+
+    //Associations
+    private ConnectionManager connectionManager;    //the connection's ConnectionManager
+    private Socket socket;                            //the connection's socket
+    private InetAddress address;                    //the connection's IP Address Object
+    private Map<String, String> environment;        //the environment
+
+    //Members
+    private String hostName;                        //cache for the hostname
+    private String hostAddress;                        //cache for the host ip
+    private int port;                                //port of the connection
+    private Locale locale;                            //locale of the connection
+    private long lastActivity;                        //timestamp for the last activity
+    private boolean warned;                            //warned flag
+    private String negotiatedTerminalType;            //negotiated TerminalType as String
+    private int[] terminalGeometry;                    //negotiated terminal geometry
+    private boolean terminalGeometryChanged = true;    //flag for changes in the terminal geometry
+    private String loginShell;                      //the login shell
+    private boolean lineMode = false;
+
+    /**
+     * Constructs a ConnectionData instance storing vital
+     * information about a connection.
+     *
+     * @param sock Socket of the inbound connection.
+     */
+    public ConnectionData(Socket sock, ConnectionManager cm) {
+        socket = sock;
+        connectionManager = cm;
+        address = sock.getInetAddress();
+        setHostName();
+        setHostAddress();
+        setLocale();
+        port = sock.getPort();
+        //this will set a default geometry and terminal type for the terminal
+        terminalGeometry = new int[2];
+        terminalGeometry[0] = 80;    //width
+        terminalGeometry[1] = 25;    //height
+        negotiatedTerminalType = "default";
+        environment = new HashMap<>(20);
+        //this will stamp the first activity for validity :)
+        activity();
+    }//ConnectionData
+
+
+    /**
+     * Returns a reference to the ConnectionManager the
+     * connection is associated with.
+     *
+     * @return Reference to the associated ConnectionManager.
+     * @see ConnectionManager
+     */
+    public ConnectionManager getManager() {
+        return connectionManager;
+    }//getManager
+
+    /**
+     * Returns a reference to the socket the Connection
+     * is associated with.
+     *
+     * @return Reference to the associated Socket.
+     * @see java.net.Socket
+     */
+    public Socket getSocket() {
+        return socket;
+    }//getSocket
+
+    /**
+     * Returns the remote port to which the socket is connected.
+     *
+     * @return String that contains the remote port number to which the socket is connected.
+     */
+    public int getPort() {
+        return port;
+    }//getPort
+
+    /**
+     * Returns the fully qualified host name for the connection's IP address.<br>
+     * The name is cached on creation for performance reasons. Subsequent calls
+     * will not result in resolve queries.
+     *
+     * @return String that contains the fully qualified host name for this address.
+     */
+    public String getHostName() {
+        return hostName;
+    }//getHostName
+
+    /**
+     * Returns the IP address of the connection.
+     *
+     * @return String that contains the connection's IP address.<br>
+     *         The format "%d.%d.%d.%d" is well known, where %d goes from zero to 255.
+     */
+    public String getHostAddress() {
+        return hostAddress;
+    }//getHostAddress
+
+    /**
+     * Returns the InetAddress object associated with the connection.
+     *
+     * @return InetAddress associated with the connection.
+     */
+    public InetAddress getInetAddress() {
+        return address;
+    }//getInetAddress
+
+    /**
+     * Returns the Locale object associated with the connection
+     * by carrying out a simple domain match. <br>
+     * This can either be effective, if your users are really
+     * home in the country they are connecting from,
+     * or ineffective if they are on the move getting connected
+     * from anywhere in the world.<br>
+     * <br>
+     * Yet this gives the chance of capturing a default locale
+     * and starting from some point. On application context
+     * this can be by far better handled, so be aware that
+     * it makes sense to spend some thoughts on that thing when you
+     * build your application.
+     *
+     * @return the Locale object "guessed" for the connection based
+     *         on its host name.
+     */
+    public Locale getLocale() {
+        return locale;
+    }//getLocale
+
+
+    /**
+     * Returns a timestamp of the last activity that happened on
+     * the associated connection.
+     *
+     * @return the timestamp as a long representing the difference,
+     *         measured in milliseconds, between the current time and
+     *         midnight, January 1, 1970 UTC.
+     */
+    public long getLastActivity() {
+        return lastActivity;
+    }//getLastActivity
+
+
+    /**
+     * Sets a new timestamp to the actual time in millis
+     * retrieved from the System. This will remove an idle warning
+     * flag if it has been set. Note that you can use this behaviour
+     * to implement your own complex idle timespan policies within
+     * the context of your application.<br>
+     * The check frequency of the ConnectionManager should just be set
+     * according to the lowest time to warning and time to disconnect
+     * requirements.
+     */
+    public void activity() {
+        warned = false;
+        lastActivity = System.currentTimeMillis();
+    }//setLastActivity
+
+    /**
+     * Returns the state of the idle warning flag, which
+     * will be true if a warning has been issued, and false
+     * if not.
+     *
+     * @return the state of the idle warning flag.
+     */
+    public boolean isWarned() {
+        return warned;
+    }//isWarned
+
+    /**
+     * Sets the state of the idle warning flag.<br>
+     * Note that this method will also update the
+     * the timestamp if the idle warning flag is removed,
+     * which means its kind of a second way to achieve the
+     * same thing as with the activity method.
+     *
+     * @param bool true if a warning is to be issued,
+     *             false if to be removed.
+     * @see #activity()
+     */
+    public void setWarned(boolean bool) {
+        warned = bool;
+        if (!bool) {
+            lastActivity = System.currentTimeMillis();
+        }
+    }//setWarned
+
+    /**
+     * Sets the terminal geometry data.<br>
+     * <em>This method should not be called explicitly
+     * by the application (i.e. the its here for the io subsystem).</em><br>
+     * A call will set the terminal geometry changed flag.
+     *
+     * @param width  of the terminal in columns.
+     * @param height of the terminal in rows.
+     */
+    public void setTerminalGeometry(int width, int height) {
+        terminalGeometry[0] = width;
+        terminalGeometry[1] = height;
+        terminalGeometryChanged = true;
+    }//setTerminalGeometry
+
+    /**
+     * Returns the terminal geometry in an array of two integers.
+     * <ul>
+     * <li>index 0: Width in columns.
+     * <li>index 1: Height in rows.
+     * </ul>
+     * A call will reset the terminal geometry changed flag.
+     *
+     * @return integer array containing width and height.
+     */
+    public int[] getTerminalGeometry() {
+        //we toggle the flag because the change should now be known
+        if (terminalGeometryChanged) terminalGeometryChanged = false;
+        return terminalGeometry;
+    }//getTerminalGeometry
+
+    /**
+     * Returns the width of the terminal in columns for convenience.
+     *
+     * @return the number of columns.
+     */
+    public int getTerminalColumns() {
+        return terminalGeometry[0];
+    }//getTerminalColumns
+
+    /**
+     * Returns the height of the terminal in rows for convenience.
+     *
+     * @return the number of rows.
+     */
+    public int getTerminalRows() {
+        return terminalGeometry[1];
+    }//getTerminalRows
+
+    /**
+     * Returns the state of the terminal geometry changed flag,
+     * which will be true if it has been set, and false
+     * if not.
+     *
+     * @return the state of the terminal geometry changed flag.
+     */
+    public boolean isTerminalGeometryChanged() {
+        return terminalGeometryChanged;
+    }//isTerminalGeometryChanged
+
+    /**
+     * Returns the terminal type that has been negotiated
+     * between the telnet client and the telnet server, in
+     * of a String.<br>
+     *
+     * @return the negotiated terminal type as String.
+     */
+    public String getNegotiatedTerminalType() {
+        return negotiatedTerminalType;
+    }//getNegotiatedTerminalType
+
+    /**
+     * Sets the terminal type that has been negotiated
+     * between telnet client and telnet server, in form of
+     * a String.<br>
+     * <p/>
+     * <em>This method should not be called explicitly
+     * by the application (i.e. the its here for the io subsystem).</em><br>
+     *
+     * @param termtype the negotiated terminal type as String.
+     */
+    public void setNegotiatedTerminalType(String termtype) {
+        negotiatedTerminalType = termtype;
+    }//setNegotiatedTerminalType
+
+    /**
+     * Returns the hashmap for storing and
+     * retrieving environment variables to be passed
+     * between shells.
+     *
+     * @return a <tt>HashMap</tt> instance.
+     */
+    public Map<String, String> getEnvironment() {
+        return environment;
+    }//getEnvironment
+
+    /**
+     * Returns the login shell name.
+     *
+     * @return the shell name as string.
+     */
+    public String getLoginShell() {
+        return loginShell;
+    }//getLoginShell
+
+    /**
+     * Sets the login shell name.
+     *
+     * @param s the shell name as string.
+     */
+    public void setLoginShell(String s) {
+        loginShell = s;
+    }//setLoginShell
+
+    /**
+     * Tests if in line mode.
+     *
+     * @return true if in line mode, false otherwise
+     */
+    public boolean isLineMode() {
+        return lineMode;
+    }//isLineMode
+
+    /**
+     * Sets the line mode flag for the connection.
+     * Note that the setting will only be used at
+     * startup at the moment.
+     *
+     * @param b true if to be initialized in linemode,
+     *          false otherwise.
+     */
+    public void setLineMode(boolean b) {
+        lineMode = b;
+    }//setLineMode
+
+    /**
+     * Mutator for HostName cache
+     */
+    private void setHostName() {
+        hostName = address.getHostName();
+    }//setHostName
+
+    /**
+     * Mutator for HostAddress cache
+     */
+    private void setHostAddress() {
+        hostAddress = address.getHostAddress();
+    }//setHostAddress
+
+    /**
+     * Mutator for Locale
+     * Sets a Locale derived from the hostname,
+     * or the default which is Locale.ENGLISH if something
+     * goes wrong.
+     * The localhost represents a problem for example :)
+     */
+    private void setLocale() {
+        String country = getHostName();
+        try {
+            country = country.substring(country.lastIndexOf(".") + 1);
+            switch (country) {
+                case "at":
+                    locale = new Locale("de", "AT");
+                    break;
+                case "de":
+                    locale = new Locale("de", "DE");
+                    break;
+                case "mx":
+                    locale = new Locale("es", "MX");
+                    break;
+                case "es":
+                    locale = new Locale("es", "ES");
+                    break;
+                case "it":
+                    locale = Locale.ITALY;
+                    break;
+                case "fr":
+                    locale = Locale.FRANCE;
+                    break;
+                case "uk":
+                    locale = new Locale("en", "GB");
+                    break;
+                case "arpa":
+                    locale = Locale.US;
+                    break;
+                case "com":
+                    locale = Locale.US;
+                    break;
+                case "edu":
+                    locale = Locale.US;
+                    break;
+                case "gov":
+                    locale = Locale.US;
+                    break;
+                case "org":
+                    locale = Locale.US;
+                    break;
+                case "mil":
+                    locale = Locale.US;
+                    break;
+                default:
+                    //default to english
+                    locale = Locale.ENGLISH;
+                    break;
+            }
+        } catch (Exception ex) {
+            //default to english
+            locale = Locale.ENGLISH;
+        }
+    }//setLocale
+
+}//class ConnectionData
diff --git a/jline/src/test/java/org/apache/felix/gogo/jline/telnet/ConnectionEvent.java b/jline/src/test/java/org/apache/felix/gogo/jline/telnet/ConnectionEvent.java
new file mode 100644
index 0000000..8348f58
--- /dev/null
+++ b/jline/src/test/java/org/apache/felix/gogo/jline/telnet/ConnectionEvent.java
@@ -0,0 +1,137 @@
+/*
+ * 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.
+ */
+
+/***
+ * Java TelnetD library (embeddable telnet daemon)
+ * Copyright (c) 2000-2005 Dieter Wimberger
+ * All rights reserved.
+ * <p/>
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ * Redistributions of source code must retain the above copyright notice,
+ * this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * <p/>
+ * Neither the name of the author nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ * <p/>
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDER AND CONTRIBUTORS ``AS
+ * IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+ * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ ***/
+
+package org.apache.felix.gogo.jline.telnet;
+
+/**
+ * Class implmenting a ConnectionEvent.<br>
+ * These events are used to communicate things that are
+ * supposed to be handled within the application context.
+ * These events are processed by the Connection instance
+ * calling upon its registered listeners.
+ *
+ * @author Dieter Wimberger
+ * @version 2.0 (16/07/2006)
+ * @see Connection
+ * @see ConnectionListener
+ */
+public class ConnectionEvent {
+
+    private final Connection source;
+    private final Type type;
+    /**
+     * Constructs a new instance of a ConnectionEvent
+     * with a given source (Connection) and a given type.
+     *
+     * @param source Connection that represents the source of this event.
+     * @param type int that contains one of the defined event types.
+     */
+    public ConnectionEvent(Connection source, Type type) {
+        this.type = type;
+        this.source = source;
+    }//constructor
+
+    /**
+     * Accessor method returning the source of the
+     * ConnectionEvent instance.
+     *
+     * @return Connection representing the source.
+     */
+    public Connection getSource() {
+        return source;
+    }//getSource
+
+    /**
+     * Method that helps identifying the type.
+     *
+     * @return Event type.
+     */
+    public Type getType() {
+        return type;
+    }//getType
+
+    public enum Type {
+        /**
+         * Defines the connection idle event type.<br>
+         * It occurs if a connection has been idle exceeding
+         * the configured time to warning.
+         */
+        CONNECTION_IDLE,
+
+        /**
+         * Defines the connection timed out event type.<br>
+         * It occurs if a connection has been idle exceeding
+         * the configured time to warning and the configured time
+         * to timedout.
+         */
+        CONNECTION_TIMEDOUT,
+
+        /**
+         * Defines the connection requested logout event type.<br>
+         * It occurs if a connection requested disgraceful logout by
+         * sending a <Ctrl>-<D> key combination.
+         */
+        CONNECTION_LOGOUTREQUEST,
+
+        /**
+         * Defines the connection sent break event type.<br>
+         * It occurs when the connection sent a NVT BREAK.
+         */
+        CONNECTION_BREAK,
+
+        /**
+         * Defines the connection geometry event type.
+         * It occurs when the connection sent a NAWS.
+         */
+        CONNECTION_TERMINAL_GEOMETRY_CHANGED
+    }
+
+
+}//class ConnectionEvent
\ No newline at end of file
diff --git a/jline/src/test/java/org/apache/felix/gogo/jline/telnet/ConnectionFilter.java b/jline/src/test/java/org/apache/felix/gogo/jline/telnet/ConnectionFilter.java
new file mode 100644
index 0000000..e948166
--- /dev/null
+++ b/jline/src/test/java/org/apache/felix/gogo/jline/telnet/ConnectionFilter.java
@@ -0,0 +1,82 @@
+/*
+ * 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.
+ */
+
+/***
+ * Java TelnetD library (embeddable telnet daemon)
+ * Copyright (c) 2000-2005 Dieter Wimberger
+ * All rights reserved.
+ * <p/>
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ * Redistributions of source code must retain the above copyright notice,
+ * this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * <p/>
+ * Neither the name of the author nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ * <p/>
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDER AND CONTRIBUTORS ``AS
+ * IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+ * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ ***/
+
+package org.apache.felix.gogo.jline.telnet;
+
+import java.net.InetAddress;
+
+/**
+ * Interface defining a generic IP level connection
+ * filter.<br>
+ * Due to the fact that this task depends heavily on
+ * application context, I chose a very generic way
+ * of applying IP level connection filtering.
+ * <br><br>
+ * Implementations should consider following issues:
+ * <ul>
+ * <li>performance
+ * <li>administration (maybe via an admin shell)
+ * <li>logging denials
+ * </ul>
+ *
+ * @author Dieter Wimberger
+ * @version 2.0 (16/07/2006)
+ */
+public interface ConnectionFilter {
+
+    /**
+     * Tests if a given ip address is allowed to connect.
+     *
+     * @param ip the address to be tested.
+     * @return true if allowed to connect, false otherwise.
+     */
+    boolean isAllowed(InetAddress ip);
+
+}//interface ConnectionFilter
\ No newline at end of file
diff --git a/jline/src/test/java/org/apache/felix/gogo/jline/telnet/ConnectionListener.java b/jline/src/test/java/org/apache/felix/gogo/jline/telnet/ConnectionListener.java
new file mode 100644
index 0000000..38ed90b
--- /dev/null
+++ b/jline/src/test/java/org/apache/felix/gogo/jline/telnet/ConnectionListener.java
@@ -0,0 +1,106 @@
+/*
+ * 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.
+ */
+
+/***
+ * Java TelnetD library (embeddable telnet daemon)
+ * Copyright (c) 2000-2005 Dieter Wimberger
+ * All rights reserved.
+ * <p/>
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ * Redistributions of source code must retain the above copyright notice,
+ * this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * <p/>
+ * Neither the name of the author nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ * <p/>
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDER AND CONTRIBUTORS ``AS
+ * IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+ * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ ***/
+
+package org.apache.felix.gogo.jline.telnet;
+
+
+/**
+ * Interface to be implemented if a class wants to
+ * qualify as a ConnectionListener.<br>
+ * Note that a Shell is per contract also forced to
+ * implement this interface.
+ *
+ * @author Dieter Wimberger
+ * @version 2.0 (16/07/2006)
+ * @see ConnectionEvent
+ */
+public interface ConnectionListener {
+
+    /**
+     * Called when a CONNECTION_IDLE event occured.
+     *
+     * @param ce ConnectionEvent instance.
+     * @see ConnectionEvent.Type#CONNECTION_IDLE
+     */
+    void connectionIdle(ConnectionEvent ce);
+
+    /**
+     * Called when a CONNECTION_TIMEDOUT event occured.
+     *
+     * @param ce ConnectionEvent instance.
+     * @see ConnectionEvent.Type#CONNECTION_TIMEDOUT
+     */
+    void connectionTimedOut(ConnectionEvent ce);
+
+    /**
+     * Called when a CONNECTION_LOGOUTREQUEST occured.
+     *
+     * @param ce ConnectionEvent instance.
+     * @see ConnectionEvent.Type#CONNECTION_LOGOUTREQUEST
+     */
+    void connectionLogoutRequest(ConnectionEvent ce);
+
+    /**
+     * Called when a CONNECTION_BREAK event occured.
+     *
+     * @param ce ConnectionEvent instance.
+     * @see ConnectionEvent.Type#CONNECTION_BREAK
+     */
+    void connectionSentBreak(ConnectionEvent ce);
+
+    /**
+     * Called when a CONNECTION_TERMINAL_GEOMETRY_CHANGED event occured.
+     *
+     * @param ce ConnectionEvent instance.
+     * @see ConnectionEvent.Type#CONNECTION_TERMINAL_GEOMETRY_CHANGED
+     */
+    void connectionTerminalGeometryChanged(ConnectionEvent ce);
+
+}//interface ConnectionListener
\ No newline at end of file
diff --git a/jline/src/test/java/org/apache/felix/gogo/jline/telnet/ConnectionManager.java b/jline/src/test/java/org/apache/felix/gogo/jline/telnet/ConnectionManager.java
new file mode 100644
index 0000000..d46b0e5
--- /dev/null
+++ b/jline/src/test/java/org/apache/felix/gogo/jline/telnet/ConnectionManager.java
@@ -0,0 +1,393 @@
+/*
+ * 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.
+ */
+
+/***
+ * Java TelnetD library (embeddable telnet daemon)
+ * Copyright (c) 2000-2005 Dieter Wimberger
+ * All rights reserved.
+ * <p/>
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ * Redistributions of source code must retain the above copyright notice,
+ * this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * <p/>
+ * Neither the name of the author nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ * <p/>
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDER AND CONTRIBUTORS ``AS
+ * IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+ * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ ***/
+
+package org.apache.felix.gogo.jline.telnet;
+
+import java.io.IOException;
+import java.net.InetAddress;
+import java.net.Socket;
+import java.text.MessageFormat;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Stack;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * Class that takes care for active and queued connection.
+ * Housekeeping is done also for connections that were just broken
+ * off, or exceeded their timeout. 
+ *
+ * @author Dieter Wimberger
+ * @version 2.0 (16/07/2006)
+ */
+public abstract class ConnectionManager implements Runnable {
+
+    private static Logger LOG = Logger.getLogger(ConnectionManager.class.getName());
+    private final List<Connection> openConnections;
+    private Thread thread;
+    private ThreadGroup threadGroup; //ThreadGroup all connections run in
+    private Stack<Connection> closedConnections;
+    private ConnectionFilter connectionFilter; //reference to the connection filter
+    private int maxConnections; //maximum allowed connections stored from the properties
+    private int warningTimeout; //time to idle warning
+    private int disconnectTimeout; //time to idle diconnection
+    private int housekeepingInterval; //interval for managing cleanups
+    private String loginShell;
+    private boolean lineMode = false;
+    private boolean stopping = false;
+
+    public ConnectionManager() {
+        threadGroup = new ThreadGroup(toString() + "Connections");
+        closedConnections = new Stack<>();
+        openConnections = Collections.synchronizedList(new ArrayList<>(100));
+    }
+
+    public ConnectionManager(int con, int timew, int timedis, int hoke, ConnectionFilter filter, String lsh, boolean lm) {
+        this();
+        connectionFilter = filter;
+        loginShell = lsh;
+        lineMode = lm;
+        maxConnections = con;
+        warningTimeout = timew;
+        disconnectTimeout = timedis;
+        housekeepingInterval = hoke;
+    }//constructor
+
+    /**
+     * Gets the active ConnectionFilter instance or
+     * returns null if no filter is set.
+     *
+     * @return the managers ConnectionFilter.
+     */
+    public ConnectionFilter getConnectionFilter() {
+        return connectionFilter;
+    }//getConnectionFilter
+
+    /**
+     * Set a connection filter for this
+     * ConnectionManager instance. The filter is used to handle
+     * IP level allow/deny of incoming connections.
+     *
+     * @param filter ConnectionFilter instance.
+     */
+    public void setConnectionFilter(ConnectionFilter filter) {
+        connectionFilter = filter;
+    }//setConnectionFilter
+
+    /**
+     * Returns the number of open connections.
+     * @return the number of open connections as <tt>int</tt>.
+     */
+    public int openConnectionCount() {
+        return openConnections.size();
+    }//openConnectionCount
+
+    /**
+     * Returns the {@link Connection} at the given index.
+     * @param idx
+     * @return
+     */
+    public Connection getConnection(int idx) {
+        synchronized (openConnections) {
+            return openConnections.get(idx);
+        }
+    }//getConnection
+
+    /**
+     * Get all {@link Connection} instances with the given
+     * <tt>InetAddress</tt>.
+     *
+     * @return all {@link Connection} instances with the given
+     *         <tt>InetAddress</tt>.
+     */
+    public Connection[] getConnectionsByAdddress(InetAddress addr) {
+        ArrayList<Connection> l = new ArrayList<>();
+        synchronized (openConnections) {
+            for (Connection connection : openConnections) {
+                if (connection.getConnectionData().getInetAddress().equals(addr)) {
+                    l.add(connection);
+                }
+            }
+        }
+        Connection[] conns = new Connection[l.size()];
+        return l.toArray(conns);
+    }//getConnectionsByAddress
+
+    /**
+     * Starts this <tt>ConnectionManager</tt>.
+     */
+    public void start() {
+        thread = new Thread(this);
+        thread.start();
+    }//start
+
+    /**
+     * Stops this <tt>ConnectionManager</tt>.
+     */
+    public void stop() {
+        LOG.log(Level.FINE, "stop()::" + this.toString());
+        stopping = true;
+        //wait for thread to die
+        try {
+            if (thread != null) {
+                thread.join();
+            }
+        } catch (InterruptedException iex) {
+            LOG.log(Level.SEVERE, "stop()", iex);
+        }
+        synchronized (openConnections) {
+            for (Connection tc : openConnections) {
+                try {
+                    //maybe write a disgrace to the socket?
+                    tc.close();
+                } catch (Exception exc) {
+                    LOG.log(Level.SEVERE, "stop()", exc);
+                }
+            }
+            openConnections.clear();
+        }
+        LOG.log(Level.FINE, "stop():: Stopped " + this.toString());
+    }//stop
+
+    /**
+     * Method that that tries to connect an incoming request.
+     * Properly  queueing.
+     *
+     * @param insock Socket thats representing the incoming connection.
+     */
+    public void makeConnection(Socket insock) {
+        LOG.log(Level.FINE, "makeConnection()::" + insock.toString());
+        if (connectionFilter == null || connectionFilter.isAllowed(insock.getInetAddress())) {
+            //we create the connection data object at this point to
+            //store certain information there.
+            ConnectionData newCD = new ConnectionData(insock, this);
+            newCD.setLoginShell(loginShell);
+            newCD.setLineMode(lineMode);
+            if (openConnections.size() < maxConnections) {
+                //create a new Connection instance
+                Connection con = createConnection(threadGroup, newCD);
+                //log the newly created connection
+                Object[] args = {openConnections.size() + 1};
+                LOG.info(MessageFormat.format("connection #{0,number,integer} made.", args));
+                //register it for being managed
+                synchronized (openConnections) {
+                    openConnections.add(con);
+                }
+                //start it
+                con.start();
+            }
+        } else {
+            LOG.info("makeConnection():: Active Filter blocked incoming connection.");
+            try {
+                insock.close();
+            } catch (IOException ex) {
+                //do nothing or log.
+            }
+        }
+    }//makeConnection
+
+    protected abstract Connection createConnection(ThreadGroup threadGroup, ConnectionData newCD);
+
+    /**
+     * Periodically does following work:
+     * <ul>
+     * <li> cleaning up died connections.
+     * <li> checking managed connections if they are working properly.
+     * <li> checking the open connections.
+     * </ul>
+     */
+    public void run() {
+        //housekeep connections
+        try {
+            do {
+                //clean up and close all broken connections
+                //cleanupBroken();
+                //clean up closed connections
+                cleanupClosed();
+                //check all active connections
+                checkOpenConnections();
+                //sleep interval
+                Thread.sleep(housekeepingInterval);
+            } while (!stopping);
+
+        } catch (Exception e) {
+            LOG.log(Level.SEVERE, "run()", e);
+        }
+        LOG.log(Level.FINE, "run():: Ran out " + this.toString());
+    }//run
+
+    /*
+    private void cleanupBroken() {
+      //cleanup loop
+      while (!m_BrokenConnections.isEmpty()) {
+        Connection nextOne = (Connection) m_BrokenConnections.pop();
+        log.info("cleanupBroken():: Closing broken connection " + nextOne.toString());
+        //fire logoff event for shell site cleanup , beware could hog the daemon thread
+        nextOne.processConnectionEvent(new ConnectionEvent(nextOne, ConnectionEvent.CONNECTION_BROKEN));
+        //close the connection, will be automatically registered as closed
+        nextOne.close();
+      }
+    }//cleanupBroken
+    */
+    private void cleanupClosed() {
+        if (stopping) {
+            return;
+        }
+        //cleanup loop
+        while (!closedConnections.isEmpty()) {
+            Connection nextOne = closedConnections.pop();
+            LOG.info("cleanupClosed():: Removing closed connection " + nextOne.toString());
+            synchronized (openConnections) {
+                openConnections.remove(nextOne);
+            }
+        }
+    }//cleanupBroken
+
+    private void checkOpenConnections() {
+        if (stopping) {
+            return;
+        }
+        //do routine checks on active connections
+        synchronized (openConnections) {
+            for (Connection conn : openConnections) {
+                ConnectionData cd = conn.getConnectionData();
+                //check if it is dead and remove it.
+                if (!conn.isActive()) {
+                    registerClosedConnection(conn);
+                    continue;
+                }
+                /* Timeouts check */
+                //first we caculate the inactivity time
+                long inactivity = System.currentTimeMillis() - cd.getLastActivity();
+                //now we check for warning and disconnection
+                if (inactivity > warningTimeout) {
+                    //..and for disconnect
+                    if (inactivity > (disconnectTimeout + warningTimeout)) {
+                        //this connection needs to be disconnected :)
+                        LOG.log(Level.FINE, "checkOpenConnections():" + conn.toString() + " exceeded total timeout.");
+                        //fire logoff event for shell site cleanup , beware could hog the daemon thread
+                        conn.processConnectionEvent(new ConnectionEvent(conn, ConnectionEvent.Type.CONNECTION_TIMEDOUT));
+                        //conn.close();
+                    } else {
+                        //this connection needs to be warned :)
+                        if (!cd.isWarned()) {
+                            LOG.log(Level.FINE, "checkOpenConnections():" + conn.toString() + " exceeded warning timeout.");
+                            cd.setWarned(true);
+                            //warning event is fired but beware this could hog the daemon thread!!
+                            conn.processConnectionEvent(new ConnectionEvent(conn, ConnectionEvent.Type.CONNECTION_IDLE));
+                        }
+                    }
+                }
+            }
+            /* end Timeouts check */
+        }
+    }//checkConnections
+
+    public void registerClosedConnection(Connection con) {
+        if (stopping) {
+            return;
+        }
+        if (!closedConnections.contains(con)) {
+            LOG.log(Level.FINE, "registerClosedConnection()::" + con.toString());
+            closedConnections.push(con);
+        }
+    }//unregister
+
+    public int getDisconnectTimeout() {
+        return disconnectTimeout;
+    }
+
+    public void setDisconnectTimeout(int disconnectTimeout) {
+        this.disconnectTimeout = disconnectTimeout;
+    }
+
+    public int getHousekeepingInterval() {
+        return housekeepingInterval;
+    }
+
+    public void setHousekeepingInterval(int housekeepingInterval) {
+        this.housekeepingInterval = housekeepingInterval;
+    }
+
+    public boolean isLineMode() {
+        return lineMode;
+    }
+
+    public void setLineMode(boolean lineMode) {
+        this.lineMode = lineMode;
+    }
+
+    public String getLoginShell() {
+        return loginShell;
+    }
+
+    public void setLoginShell(String loginShell) {
+        this.loginShell = loginShell;
+    }
+
+    public int getMaxConnections() {
+        return maxConnections;
+    }
+
+    public void setMaxConnections(int maxConnections) {
+        this.maxConnections = maxConnections;
+    }
+
+    public int getWarningTimeout() {
+        return warningTimeout;
+    }
+
+    public void setWarningTimeout(int warningTimeout) {
+        this.warningTimeout = warningTimeout;
+    }
+
+}//class ConnectionManager
diff --git a/jline/src/test/java/org/apache/felix/gogo/jline/telnet/PortListener.java b/jline/src/test/java/org/apache/felix/gogo/jline/telnet/PortListener.java
new file mode 100644
index 0000000..1682db0
--- /dev/null
+++ b/jline/src/test/java/org/apache/felix/gogo/jline/telnet/PortListener.java
@@ -0,0 +1,222 @@
+/*
+ * 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.
+ */
+
+/***
+ * Java TelnetD library (embeddable telnet daemon)
+ * Copyright (c) 2000-2005 Dieter Wimberger
+ * All rights reserved.
+ * <p/>
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ * Redistributions of source code must retain the above copyright notice,
+ * this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * <p/>
+ * Neither the name of the author nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ * <p/>
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDER AND CONTRIBUTORS ``AS
+ * IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+ * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ ***/
+
+package org.apache.felix.gogo.jline.telnet;
+
+import java.io.IOException;
+import java.net.ServerSocket;
+import java.net.Socket;
+import java.net.SocketException;
+import java.text.MessageFormat;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * Class that implements a <tt>PortListener</tt>.<br>
+ * If available, it accepts incoming connections and passes them
+ * to an associated <tt>ConnectionManager</tt>.
+ *
+ * @author Dieter Wimberger
+ * @version 2.0 (16/07/2006)
+ * @see ConnectionManager
+ */
+public class PortListener
+        implements Runnable {
+
+    private static final Logger LOG = Logger.getLogger(PortListener.class.getName());
+    private static final String logmsg =
+            "Listening to Port {0,number,integer} with a connectivity queue size of {1,number,integer}.";
+    private String name;
+    private int port;                                        //port number running on
+    private int floodProtection;                        //flooding protection
+    private ServerSocket serverSocket = null; //server socket
+    private Thread thread;
+    private ConnectionManager connectionManager;    //connection management thread
+    private boolean stopping = false;
+    private boolean available;                    //Flag for availability
+
+    /**
+     * Constructs a PortListener instance.<br>
+     *
+     * @param port      int that specifies the port number of the server socket.
+     * @param floodprot that specifies the server socket queue size.
+     */
+    public PortListener(String name, int port, int floodprot) {
+        this.name = name;
+        available = false;
+        this.port = port;
+        floodProtection = floodprot;
+    }//constructor
+
+    /**
+     * Returns the name of this <tt>PortListener</tt>.
+     *
+     * @return the name as <tt>String</tt>.
+     */
+    public String getName() {
+        return name;
+    }//getName
+
+    /**
+     * Tests if this <tt>PortListener</tt> is available.
+     *
+     * @return true if available, false otherwise.
+     */
+    public boolean isAvailable() {
+        return available;
+    }//isAvailable
+
+    /**
+     * Sets the availability flag of this <tt>PortListener</tt>.
+     *
+     * @param b true if to be available, false otherwise.
+     */
+    public void setAvailable(boolean b) {
+        available = b;
+    }//setAvailable
+
+    /**
+     * Starts this <tt>PortListener</tt>.
+     */
+    public void start() {
+        LOG.log(Level.FINE, "start()");
+        thread = new Thread(this);
+        thread.start();
+        available = true;
+    }//start
+
+    /**
+     * Stops this <tt>PortListener</tt>, and returns
+     * when everything was stopped successfully.
+     */
+    public void stop() {
+        LOG.log(Level.FINE, "stop()::" + this.toString());
+        //flag stop
+        stopping = true;
+        available = false;
+        //take down all connections
+        connectionManager.stop();
+
+        //close server socket
+        try {
+            serverSocket.close();
+        } catch (IOException ex) {
+            LOG.log(Level.SEVERE, "stop()", ex);
+        }
+
+        //wait for thread to die
+        try {
+            thread.join();
+        } catch (InterruptedException iex) {
+            LOG.log(Level.SEVERE, "stop()", iex);
+        }
+
+        LOG.info("stop()::Stopped " + this.toString());
+    }//stop
+
+    /**
+     * Listen constantly to a server socket and handles incoming connections
+     * through the associated {a:link ConnectionManager}.
+     *
+     * @see ConnectionManager
+     */
+    public void run() {
+        try {
+            /*
+                A server socket is opened with a connectivity queue of a size specified
+                in int floodProtection.  Concurrent login handling under normal circumstances
+                should be handled properly, but denial of service attacks via massive parallel
+                program logins should be prevented with this.
+            */
+            serverSocket = new ServerSocket(port, floodProtection);
+
+            //log entry
+            LOG.info(MessageFormat.format(logmsg, port, floodProtection));
+
+            do {
+                try {
+                    Socket s = serverSocket.accept();
+                    if (available) {
+                        connectionManager.makeConnection(s);
+                    } else {
+                        //just shut down the socket
+                        s.close();
+                    }
+                } catch (SocketException ex) {
+                    if (stopping) {
+                        //server socket was closed blocked in accept
+                        LOG.log(Level.FINE, "run(): ServerSocket closed by stop()");
+                    } else {
+                        LOG.log(Level.SEVERE, "run()", ex);
+                    }
+                }
+            } while (!stopping);
+
+        } catch (IOException e) {
+            LOG.log(Level.SEVERE, "run()", e);
+        }
+        LOG.log(Level.FINE, "run(): returning.");
+    }//run
+
+    /**
+     * Returns reference to ConnectionManager instance associated
+     * with the PortListener.
+     *
+     * @return the associated ConnectionManager.
+     */
+    public ConnectionManager getConnectionManager() {
+        return connectionManager;
+    }//getConnectionManager
+
+    public void setConnectionManager(ConnectionManager connectionManager) {
+        this.connectionManager = connectionManager;
+    }
+
+}//class PortListener
diff --git a/jline/src/test/java/org/apache/felix/gogo/jline/telnet/Telnet.java b/jline/src/test/java/org/apache/felix/gogo/jline/telnet/Telnet.java
new file mode 100644
index 0000000..1f3259b
--- /dev/null
+++ b/jline/src/test/java/org/apache/felix/gogo/jline/telnet/Telnet.java
@@ -0,0 +1,203 @@
+/*
+ * 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.felix.gogo.jline.telnet;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.PrintStream;
+import java.util.List;
+
+import org.apache.felix.gogo.jline.Shell;
+import org.apache.felix.gogo.jline.Shell.Context;
+import org.apache.felix.service.command.CommandProcessor;
+import org.apache.felix.service.command.CommandSession;
+import org.jline.builtins.Options;
+import org.jline.terminal.Size;
+import org.jline.terminal.Terminal;
+import org.jline.terminal.Terminal.Signal;
+import org.jline.terminal.TerminalBuilder;
+
+/*
+ * a very simple Telnet server.
+ * real remote access should be via ssh.
+ */
+public class Telnet {
+    public static final String[] functions = {"telnetd"};
+
+    private static final int defaultPort = 2019;
+    private final CommandProcessor processor;
+    private PortListener portListener;
+    private int port;
+    private String ip;
+
+    public Telnet(CommandProcessor procesor) {
+        this.processor = procesor;
+    }
+
+    public void telnetd(CommandSession session, String[] argv) throws IOException {
+        final String[] usage = {"telnetd - start simple telnet server",
+                "Usage: telnetd [-i ip] [-p port] start | stop | status",
+                "  -i --ip=INTERFACE        listen interface (default=127.0.0.1)",
+                "  -p --port=PORT           listen port (default=" + defaultPort + ")",
+                "  -? --help                show help"};
+
+        Options opt = Options.compile(usage).parse(argv);
+        List<String> args = opt.args();
+
+        if (opt.isSet("help") || args.isEmpty()) {
+            opt.usage(System.err);
+            return;
+        }
+
+        String command = args.get(0);
+
+        if ("start".equals(command)) {
+            if (portListener != null) {
+                throw new IllegalStateException("telnetd is already running on port " + port);
+            }
+            ip = opt.get("ip");
+            port = opt.getNumber("port");
+            start(session);
+            status();
+        } else if ("stop".equals(command)) {
+            if (portListener == null) {
+                throw new IllegalStateException("telnetd is not running.");
+            }
+            stop();
+        } else if ("status".equals(command)) {
+            status();
+        } else {
+            throw opt.usageError("bad command: " + command);
+        }
+    }
+
+    private void status() {
+        if (portListener != null) {
+            System.out.println("telnetd is running on " + ip + ":" + port);
+        } else {
+            System.out.println("telnetd is not running.");
+        }
+    }
+
+    private void start(CommandSession session) {
+        ConnectionManager connectionManager = new ConnectionManager(1000, 5 * 60 * 1000, 5 * 60 * 1000, 60 * 1000, null, null, false) {
+            @Override
+            protected Connection createConnection(ThreadGroup threadGroup, ConnectionData newCD) {
+                return new Connection(threadGroup, newCD) {
+                    TelnetIO telnetIO;
+
+                    @Override
+                    protected void doRun() throws Exception {
+                        telnetIO = new TelnetIO();
+                        telnetIO.setConnection(this);
+                        telnetIO.initIO();
+
+                        InputStream in = new InputStream() {
+                            @Override
+                            public int read() throws IOException {
+                                return telnetIO.read();
+                            }
+                            @Override
+                            public int read(byte[] b, int off, int len) throws IOException {
+                                int r = read();
+                                if (r >= 0) {
+                                    b[off] = (byte) r;
+                                    return 1;
+                                } else {
+                                    return -1;
+                                }
+                            }
+                        };
+                        PrintStream out = new PrintStream(new OutputStream() {
+                            @Override
+                            public void write(int b) throws IOException {
+                                telnetIO.write(b);
+                            }
+                            @Override
+                            public void flush() throws IOException {
+                                telnetIO.flush();
+                            }
+                        });
+                        Terminal terminal = TerminalBuilder.builder()
+                                .type(getConnectionData().getNegotiatedTerminalType().toLowerCase())
+                                .streams(in, out)
+                                .system(false)
+                                .name("telnet")
+                                .build();
+                        terminal.setSize(new Size(getConnectionData().getTerminalColumns(), getConnectionData().getTerminalRows()));
+                        terminal.setAttributes(Shell.getTerminal(session).getAttributes());
+                        addConnectionListener(new ConnectionListener() {
+                            @Override
+                            public void connectionIdle(ConnectionEvent ce) {
+                            }
+
+                            @Override
+                            public void connectionTimedOut(ConnectionEvent ce) {
+                            }
+
+                            @Override
+                            public void connectionLogoutRequest(ConnectionEvent ce) {
+                            }
+
+                            @Override
+                            public void connectionSentBreak(ConnectionEvent ce) {
+                            }
+
+                            @Override
+                            public void connectionTerminalGeometryChanged(ConnectionEvent ce) {
+                                terminal.setSize(new Size(getConnectionData().getTerminalColumns(), getConnectionData().getTerminalRows()));
+                                terminal.raise(Signal.WINCH);
+                            }
+                        });
+                        PrintStream pout = new PrintStream(terminal.output());
+                        CommandSession session = processor.createSession(terminal.input(), pout, pout);
+                        session.put(Shell.VAR_TERMINAL, terminal);
+                        Context context = new Context() {
+                            @Override
+                            public String getProperty(String name) {
+                                return System.getProperty(name);
+                            }
+                            @Override
+                            public void exit() {
+                                close();
+                            }
+                        };
+                        new Shell(context, processor).gosh(session, new String[]{"--login"});
+                    }
+
+                    @Override
+                    protected void doClose() {
+                        telnetIO.closeOutput();
+                        telnetIO.closeInput();
+                    }
+                };
+            }
+        };
+        portListener = new PortListener("gogo", port, 10);
+        portListener.setConnectionManager(connectionManager);
+        portListener.start();
+    }
+
+    private void stop() {
+        portListener.stop();
+        portListener = null;
+    }
+
+}
diff --git a/jline/src/test/java/org/apache/felix/gogo/jline/telnet/TelnetIO.java b/jline/src/test/java/org/apache/felix/gogo/jline/telnet/TelnetIO.java
new file mode 100644
index 0000000..7015319
--- /dev/null
+++ b/jline/src/test/java/org/apache/felix/gogo/jline/telnet/TelnetIO.java
@@ -0,0 +1,1532 @@
+/*
+ * 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.
+ */
+
+/***
+ * Java TelnetD library (embeddable telnet daemon)
+ * Copyright (c) 2000-2005 Dieter Wimberger
+ * All rights reserved.
+ * <p/>
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ * Redistributions of source code must retain the above copyright notice,
+ * this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * <p/>
+ * Neither the name of the author nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ * <p/>
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDER AND CONTRIBUTORS ``AS
+ * IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+ * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ ***/
+
+package org.apache.felix.gogo.jline.telnet;
+
+import java.io.BufferedOutputStream;
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.IOException;
+import java.net.InetAddress;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import org.apache.felix.gogo.jline.telnet.ConnectionEvent.Type;
+
+/**
+ * Class that represents the TelnetIO implementation. It contains
+ * an inner IACHandler class to handle the telnet protocol level
+ * communication.
+ * <p/>
+ * Although supposed to work full-duplex, we only process the telnet protocol
+ * layer communication in case of reading requests from the higher levels.
+ * This is the only way to meet the one thread per connection requirement.
+ * </p>
+ * <p/>
+ * The output is done via byte-oriented streams, definately suitable for the
+ * telnet protocol. The format of the  output is UTF-8 (Unicode), which is a
+ * standard and supported by any telnet client, including the ones included
+ * in Microsoft OS's.
+ * </p>
+ * <em>Notes:</em>
+ * <ul>
+ * <li>The underlying output is buffered, to ensure that all bytes written
+ * are send, the flush() method has to be called.
+ * <li>This low-level routines ensure nice multithreading behaviour on I/O.
+ * Neither large outputs, nor input sequences excuted by the connection thread
+ * can hog the system.
+ * </ul>
+ *
+ * @author Dieter Wimberger
+ * @version 2.0 (16/07/2006)
+ */
+public class TelnetIO {
+
+    /**
+     * Interpret As Command
+     */
+    protected static final int IAC = 255;
+    /**
+     * Go Ahead <BR> Newer Telnets do not make use of this option
+     * that allows a specific communication mode.
+     */
+    protected static final int GA = 249;
+    /**
+     * Negotiation: Will do option
+     */
+    protected static final int WILL = 251;
+    /**
+     * Negotiation: Wont do option
+     */
+    protected static final int WONT = 252;
+    /**
+     * Negotiation: Do option
+     */
+    protected static final int DO = 253;
+    /**
+     * Negotiation:  Dont do option
+     */
+    protected static final int DONT = 254;
+    /**
+     * Marks start of a subnegotiation.
+     */
+    protected static final int SB = 250;
+    /**
+     * Marks end of subnegotiation.
+     */
+    protected static final int SE = 240;
+    /**
+     * No operation
+     */
+    protected static final int NOP = 241;
+    /**
+     * Data mark its the data part of a SYNCH which helps to clean up the buffers between
+     * Telnet Server &lt;-&gt; Telnet Client. <BR>
+     * It should work like this we send a TCP urgent package and &lt;IAC&gt; &lt;DM&gt; the receiver
+     * should get the urgent package (SYNCH) and just discard everything until he receives
+     * our &lt;IAC&gt; &lt;DM&gt;.<BR>
+     * <EM>Remark</EM>:
+     * <OL>
+     * <LI>can we send a TCP urgent package?
+     * <LI>can we make use of the thing at all?
+     * </OL>
+     */
+    protected static final int DM = 242;
+    /**
+     * Break
+     */
+    protected static final int BRK = 243;
+    /**
+     * Interrupt Process
+     */
+    protected static final int IP = 244;
+    /**
+     * Abort Output
+     */
+    protected static final int AO = 245;
+
+    /**** Implementation of OutputStream ****************************************************/
+    /**
+     * Are You There
+     */
+    protected static final int AYT = 246;
+    /**
+     * Erase Char
+     */
+    protected static final int EC = 247;
+    /**
+     * Erase Line
+     */
+    protected static final int EL = 248;
+    /**
+     * Telnet Option: ECHO
+     */
+    protected static final int ECHO = 1;
+    /**
+     * Telnet Option: SUPress Go Ahead<br>
+     * This will be negotiated, all new telnet protocol implementations are
+     * recommended to do this.
+     */
+    protected static final int SUPGA = 3;
+    /**
+     * Telnet Option: Negotiate About Window Size<br>
+     * <ul>
+     * <li>Server request is IAC DO NAWS
+     * <li>Client response contains subnegotiation with data (columns, rows).
+     * </ul>
+     */
+    protected static final int NAWS = 31;
+    /**
+     * Telnet Option: Terminal TYPE <br>
+     * <ul>
+     * <li>Server request contains subnegotiation SEND
+     * <li>Client response contains subnegotiation with data IS,terminal type string
+     * </ul>
+     */
+    protected static final int TTYPE = 24;
+    /**
+     * TTYPE subnegotiation: IS
+     */
+    protected static final int IS = 0;
+    /**
+     * TTYPE subnegotiation: SEND
+     */
+    protected static final int SEND = 1;
+
+    /**** End implementation of OutputStream ***********************************************/
+
+
+    /**** Implementation of InputStream ****************************************************/
+    /**
+     * Telnet Option: Logout<br>
+     * This allows nice goodbye to time-outed or unwanted clients.
+     */
+    protected static final int LOGOUT = 18;
+    /**
+     * Telnet Option: Linemode
+     * <p/>
+     * The infamous line mode option.
+     */
+    protected static final int LINEMODE = 34;
+    protected static final int LM_MODE = 1;
+    protected static final int LM_EDIT = 1;
+    protected static final int LM_TRAPSIG = 2;
+
+    /**** Implementation of InputStream ****************************************************/
+
+
+    /****
+     * Following methods implement init/request/answer procedures for telnet
+     * protocol level communication.
+     */
+    protected static final int LM_MODEACK = 4;
+    protected static final int LM_FORWARDMASK = 2;
+    protected static final int LM_SLC = 3;
+    protected static final int LM_SLC_NOSUPPORT = 0;
+    protected static final int LM_SLC_DEFAULT = 3;
+
+
+    /**** End telnet protocol level communication methods *******************************/
+    protected static final int LM_SLC_VALUE = 2;
+
+
+    /** Constants declaration ***********************************************/
+
+//Telnet Protocoll Constants
+    protected static final int LM_SLC_CANTCHANGE = 1;
+    protected static final int LM_SLC_LEVELBITS = 3;
+    protected static final int LM_SLC_ACK = 128;
+    protected static final int LM_SLC_FLUSHIN = 64;
+    protected static final int LM_SLC_FLUSHOUT = 32;
+    protected static final int LM_SLC_SYNCH = 1;
+    protected static final int LM_SLC_BRK = 2;
+    protected static final int LM_SLC_IP = 3;
+    protected static final int LM_SLC_AO = 4;
+    protected static final int LM_SLC_AYT = 5;
+    protected static final int LM_SLC_EOR = 6;
+
+    /**
+     * The following implement the NVT (network virtual terminal) which offers the concept
+     * of a simple "printer". They are the basical meanings of control possibilities
+     * on a standard telnet implementation.
+     */
+    protected static final int LM_SLC_ABORT = 7;
+    protected static final int LM_SLC_EOF = 8;
+    protected static final int LM_SLC_SUSP = 9;
+    /**
+     * Telnet Option: Environment
+     */
+    protected static final int NEWENV = 39;
+    protected static final int NE_INFO = 2;
+
+    /**
+     * The following are constants for supported options,
+     * which can be negotiated based upon the telnet protocol
+     * specification.
+     */
+    protected static final int NE_VAR = 0;
+    protected static final int NE_VALUE = 1;
+
+    /**
+     * The following options are options for which we also support subnegotiation
+     * based upon the telnet protocol specification.
+     */
+    protected static final int NE_ESC = 2;
+    protected static final int NE_USERVAR = 3;
+    protected static final int NE_VAR_OK = 2;
+    protected static final int NE_VAR_DEFINED = 1;
+    protected static final int NE_VAR_DEFINED_EMPTY = 0;
+    protected static final int NE_VAR_UNDEFINED = -1;
+    protected static final int NE_IN_ERROR = -2;
+    protected static final int NE_IN_END = -3;
+    protected static final int NE_VAR_NAME_MAXLENGTH = 50;
+    protected static final int NE_VAR_VALUE_MAXLENGTH = 1000;
+    /**
+     * Unused
+     */
+    protected static final int EXT_ASCII = 17;        //Defines Extended ASCII
+    protected static final int SEND_LOC = 23;        //Defines Send Location
+    protected static final int AUTHENTICATION = 37;    //Defines Authentication
+    protected static final int ENCRYPT = 38;            //Defines Encryption
+    private static final Logger LOG = Logger.getLogger(TelnetIO.class.getName());
+    /**
+     * Window Size Constants
+     */
+    private static final int SMALLEST_BELIEVABLE_WIDTH = 20;
+    private static final int SMALLEST_BELIEVABLE_HEIGHT = 6;
+    private static final int DEFAULT_WIDTH = 80;
+    private static final int DEFAULT_HEIGHT = 25;
+    private Connection connection;                    //a reference to the connection this instance works for
+    private ConnectionData connectionData;            //holds all important information of the connection
+    private DataOutputStream out;                    //the byte oriented outputstream
+    private DataInputStream in;                        //the byte oriented input stream
+    //Aggregations
+    private IACHandler iacHandler;                    //holds a reference to the aggregated IACHandler
+    //Members
+    private InetAddress localAddress;                //address of the host the telnetd is running on
+    private boolean noIac = false;                    //describes if IAC was found and if its just processed
+    @SuppressWarnings("unused")
+    private boolean initializing;
+    private boolean crFlag;
+    /**
+     * Creates a TelnetIO object for the given connection.<br>
+     * Input- and OutputStreams are properly set and the primary telnet
+     * protocol initialization is carried out by the inner IACHandler class.<BR>
+     */
+    public TelnetIO() {
+    }//constructor
+
+    public void initIO() throws IOException {
+        //we make an instance of our inner class
+        iacHandler = new IACHandler();
+        //we setup underlying byte oriented streams
+        in = new DataInputStream(connectionData.getSocket().getInputStream());
+        out = new DataOutputStream(new BufferedOutputStream(connectionData.getSocket().getOutputStream()));
+
+        //we save the local address (necessary?)
+        localAddress = connectionData.getSocket().getLocalAddress();
+        crFlag = false;
+        //bootstrap telnet communication
+        initTelnetCommunication();
+    }//initIO
+
+    public void setConnection(Connection con) {
+        connection = con;
+        connectionData = connection.getConnectionData();
+    }//setConnection
+
+    /**
+     * Method to output a byte. Ensures that CR(\r) is never send
+     * alone,but CRLF(\r\n), which is a rule of the telnet protocol.
+     *
+     * @param b Byte to be written.
+     */
+    public void write(byte b) throws IOException {
+        //ensure CRLF(\r\n) is written for LF(\n) to adhere
+        //to the telnet protocol.
+        if (!crFlag && b == 10) {
+            out.write(13);
+        }
+
+        out.write(b);
+
+        crFlag = b == 13;
+    }//write(byte)
+
+    /**
+     * Method to output an int.
+     *
+     * @param i Integer to be written.
+     */
+    public void write(int i)
+            throws IOException {
+        write((byte) i);
+    }//write(int)
+
+    /**
+     * Method to write an array of bytes.
+     *
+     * @param sequence byte[] to be written.
+     */
+    public void write(byte[] sequence) throws IOException {
+        for (byte b : sequence) {
+            write(b);
+        }
+    }//write(byte[])
+
+    /**
+     * Method to output an array of int' s.
+     *
+     * @param sequence int [] to write
+     */
+    public void write(int[] sequence) throws IOException {
+        for (int i : sequence) {
+            write((byte) i);
+        }
+    }//write(int[])
+
+    /**
+     * Method to write a char.
+     *
+     * @param ch char to be written.
+     */
+    public void write(char ch) throws IOException {
+        write((byte) ch);
+    }//write(char)
+
+    /**
+     * Method to output a string.
+     *
+     * @param str String to be written.
+     */
+    public void write(String str) throws IOException {
+        write(str.getBytes());
+    }//write(String)
+
+    /**
+     * Method to flush all buffered output.
+     */
+    public void flush() throws IOException {
+        out.flush();
+    }//flush
+
+    /**
+     * Method to close the underlying output stream to free system resources.<br>
+     * Most likely only to be called by the ConnectionManager upon clean up of
+     * connections that ended or died.
+     */
+    public void closeOutput() {
+
+        try {
+            //sends telnetprotocol logout acknowledgement
+            write(IAC);
+            write(DO);
+            write(LOGOUT);
+            //and now close underlying outputstream
+
+            out.close();
+        } catch (IOException ex) {
+            LOG.log(Level.SEVERE, "closeOutput()", ex);
+            //handle?
+        }
+    }//close
+
+    private void rawWrite(int i) throws IOException {
+        out.write(i);
+    }//rawWrite
+
+    /**
+     * Method to read a byte from the InputStream.
+     * Invokes the IACHandler upon IAC (Byte=255).
+     *
+     * @return int read from stream.
+     */
+    public int read() throws IOException {
+        int c = rawread();
+        //if (c == 255) {
+        noIac = false;
+        while ((c == 255) && (!noIac)) {
+            /**
+             * Read next, and invoke
+             * the IACHandler he is taking care of the rest. Or at least he should :)
+             */
+            c = rawread();
+            if (c != 255) {
+                iacHandler.handleC(c);
+                c = rawread();
+            } else {
+                noIac = true;
+            }
+        }
+        return stripCRSeq(c);
+    }//read
+
+    /**
+     * Method to close the underlying inputstream to free system resources.<br>
+     * Most likely only to be called by the ConnectionManager upon clean up of
+     * connections that ended or died.
+     */
+    public void closeInput() {
+        try {
+            in.close();
+        } catch (IOException e) {
+            //handle?
+        }
+    }//closeInput
+
+    /**
+     * This method reads an unsigned 16bit Integer from the stream,
+     * its here for getting the NAWS Data Values for height and width.
+     */
+    private int read16int() throws IOException {
+        int c = in.readUnsignedShort();
+        return c;
+    }//read16int
+
+    /**
+     * The following options are options which might be of interest, but are not
+     * yet implemented or in use.
+     */
+
+    /**
+     * Method to read a raw byte from the InputStream.<br>
+     * Telnet protocol layer communication is filtered and processed here.
+     *
+     * @return int read from stream.
+     */
+    private int rawread() throws IOException {
+        int b = 0;
+
+        //try {
+        b = in.readUnsignedByte();
+        connectionData.activity();
+        return b;
+    }//rawread
+
+    /**
+     * Checks for the telnet protocol specified  CR followed by NULL or LF<BR>
+     * Subsequently reads for the next byte and forwards
+     * only a ENTER represented by LF internally.
+     */
+    private int stripCRSeq(int input) throws IOException {
+        if (input == 13) {
+            rawread();
+            return 10;
+        }
+        return input;
+    }//stripCRSeq
+
+    /**
+     * Method that initializes the telnet communication layer.
+     */
+    private void initTelnetCommunication() {
+
+        initializing = true;
+        try {
+            //start out, some clients just wait
+            if (connectionData.isLineMode()) {
+                iacHandler.doLineModeInit();
+                LOG.log(Level.FINE, "Line mode initialized.");
+            } else {
+                iacHandler.doCharacterModeInit();
+                LOG.log(Level.FINE, "Character mode initialized.");
+            }
+            //open for a defined timeout so we read incoming negotiation
+            connectionData.getSocket().setSoTimeout(1000);
+            read();
+
+        } catch (Exception e) {
+            //handle properly
+            //log.error("initTelnetCommunication()",e);
+        } finally {
+            //this is important, dont ask me why :)
+            try {
+                connectionData.getSocket().setSoTimeout(0);
+            } catch (Exception ex) {
+                LOG.log(Level.SEVERE, "initTelnetCommunication()", ex);
+            }
+        }
+        initializing = false;
+    }//initTelnetCommunication
+
+    /**
+     * Method that represents the answer to the
+     * AreYouThere question of the telnet protocol specification
+     * <p/>
+     * Output of the String [HostAdress:Yes]
+     */
+    private void IamHere() {
+        try {
+            write("[" + localAddress.toString() + ":Yes]");
+            flush();
+        } catch (Exception ex) {
+            LOG.log(Level.SEVERE, "IamHere()", ex);
+        }
+    }//IamHere
+
+    /**
+     * Network virtual terminal break.
+     */
+    private void nvtBreak() {
+        connection.processConnectionEvent(new ConnectionEvent(connection, ConnectionEvent.Type.CONNECTION_BREAK));
+    }//nvtBreak
+
+    /**
+     * Method that checks reported terminal sizes and sets the
+     * asserted values in the ConnectionData instance associated with
+     * the connection.
+     *
+     * @param width  Integer that represents the Window width in chars
+     * @param height Integer that represents the Window height in chars
+     */
+    private void setTerminalGeometry(int width, int height) {
+        if (width < SMALLEST_BELIEVABLE_WIDTH) {
+            width = DEFAULT_WIDTH;
+        }
+        if (height < SMALLEST_BELIEVABLE_HEIGHT) {
+            height = DEFAULT_HEIGHT;
+        }
+        //DEBUG: write("[New Window Size " + window_width + "x" + window_height + "]");
+        connectionData.setTerminalGeometry(width, height);
+        connection.processConnectionEvent(new ConnectionEvent(connection,
+                Type.CONNECTION_TERMINAL_GEOMETRY_CHANGED));
+    }//setTerminalGeometry
+
+    public void setEcho(boolean b) {
+    }//setEcho
+
+    /**
+     * An inner class for handling incoming option negotiations implementing the <B>telnet protocol</B>
+     * specification based upon following Standards and RFCs:
+     * <OL>
+     * <LI><A HREF="ftp://ds.internic.net/rfc/rfc854.txt">854 Telnet Protocol Specification</A>
+     * <LI><A HREF="ftp://ds.internic.net/rfc/rfc855.txt">855 Telnet Option Specifications</A>
+     * <LI><A HREF="ftp://ds.internic.net/rfc/rfc857.txt">857 Telnet Echo Option</A>
+     * <LI><A HREF="ftp://ds.internic.net/rfc/rfc858.txt">858 Telnet Supress Go Ahead Option</A>
+     * <LI><A HREF="ftp://ds.internic.net/rfc/rfc727.txt">727 Telnet Logout Option</A>
+     * <LI><A HREF="ftp://ds.internic.net/rfc/rfc1073.txt">1073 Telnet Window Size Option</A>
+     * <LI><A HREF="ftp://ds.internic.net/rfc/rfc1091.txt">1091 Telnet Terminal-Type Option</A>
+     * </OL>
+     * <p/>
+     * Furthermore there are some more, which helped to solve problems, or might be important
+     * for future enhancements:<BR>
+     * <A HREF="ftp://ds.internic.net/rfc/rfc1143.txt">1143 The Q Method of Implementing Option Negotiation</A><BR>
+     * <A HREF="ftp://ds.internic.net/rfc/rfc1416.txt">1416 Telnet Authentication Option</A><BR>
+     * <p/>
+     * After an intense study of the available material (mainly cryptical written RFCs,
+     * a telnet client implementation for the macintosh based upon NCSA telnet, and a server side
+     * implementation called key, a mud-like system completely written in Java) I realized
+     * the problems we are facing regarding to the telnet protocol:
+     * <OL>
+     * <LI> a minimal spread of invented options, which means there are a lot of invented options,
+     * but rarely they made it through to become a standard.
+     * <LI> Dependency on a special type of implementation is dangerous in our case.
+     * We are no kind of host that offers the user to run several processes at once,
+     * a BBS is intended to be a single process the user is interacting with.
+     * <LI> The <B>LAMER</B> has to be expected to log in with the standard Microsoft telnet
+     * implementation. This means forget every nice feature and most of the almost-standards.
+     * <p/>
+     * </OL>
+     * <BR>
+     *
+     * @author Dieter Wimberger
+     * @version 1.1 16/06/1998
+     * <p/>
+     * <p/>
+     * <B>To-Do</B>:<UL>
+     * <LI>UNIX conform new style TTYPE negotiation. Setting a list and selecting from it...
+     * </UL>
+     */
+    class IACHandler {
+
+        /**
+         * Telnet readin buffer
+         * Here its implemented guys. Open your eyes upon this solution.
+         * The others take a one byte solution :)
+         */
+        private int[] buffer = new int[2];
+
+        /**
+         * DO_ECHO or not
+         */
+        private boolean DO_ECHO = false;
+
+        /**
+         * DO_SUPGA or not
+         */
+        private boolean DO_SUPGA = false;
+
+        /**
+         * DO_NAWS or not
+         */
+        private boolean DO_NAWS = false;
+
+        /**
+         * DO_TTYPE or not
+         */
+        private boolean DO_TTYPE = false;
+
+        /**
+         * DO_LINEMODE or not
+         */
+        private boolean DO_LINEMODE = false;
+
+        /**
+         * DO_NEWENV or not
+         */
+        private boolean DO_NEWENV = false;
+
+        /**
+         * Are we waiting for a DO reply?
+         */
+        private boolean WAIT_DO_REPLY_SUPGA = false;
+        private boolean WAIT_DO_REPLY_ECHO = false;
+        private boolean WAIT_DO_REPLY_NAWS = false;
+        private boolean WAIT_DO_REPLY_TTYPE = false;
+        private boolean WAIT_DO_REPLY_LINEMODE = false;
+        private boolean WAIT_LM_MODE_ACK = false;
+        private boolean WAIT_LM_DO_REPLY_FORWARDMASK = false;
+        private boolean WAIT_DO_REPLY_NEWENV = false;
+        @SuppressWarnings("unused")
+        private boolean WAIT_NE_SEND_REPLY = false;
+
+        /**
+         * Are we waiting for a WILL reply?
+         */
+        private boolean WAIT_WILL_REPLY_SUPGA = false;
+        private boolean WAIT_WILL_REPLY_ECHO = false;
+        private boolean WAIT_WILL_REPLY_NAWS = false;
+        private boolean WAIT_WILL_REPLY_TTYPE = false;
+
+
+        public void doCharacterModeInit() throws IOException {
+            sendCommand(WILL, ECHO, true);
+            sendCommand(DONT, ECHO, true); //necessary for some clients
+            sendCommand(DO, NAWS, true);
+            sendCommand(WILL, SUPGA, true);
+            sendCommand(DO, SUPGA, true);
+            sendCommand(DO, TTYPE, true);
+            sendCommand(DO, NEWENV, true); //environment variables
+        }//doCharacterModeInit
+
+        public void doLineModeInit() throws IOException {
+            sendCommand(DO, NAWS, true);
+            sendCommand(WILL, SUPGA, true);
+            sendCommand(DO, SUPGA, true);
+            sendCommand(DO, TTYPE, true);
+            sendCommand(DO, LINEMODE, true);
+            sendCommand(DO, NEWENV, true);
+        }//doLineModeInit
+
+
+        /**
+         * Method to handle a IAC that came in over the line.
+         *
+         * @param i (int)ed byte that followed the IAC
+         */
+        public void handleC(int i) throws IOException {
+            buffer[0] = i;
+            if (!parseTWO(buffer)) {
+                buffer[1] = rawread();
+                parse(buffer);
+            }
+            buffer[0] = 0;
+            buffer[1] = 0;
+        }//handleC
+
+        /**
+         * Method that parses for options with two characters.
+         *
+         * @param buf int [] that represents the first byte that followed the IAC first.
+         * @return true when it was a two byte command (IAC OPTIONBYTE)
+         */
+        private boolean parseTWO(int[] buf) {
+            switch (buf[0]) {
+                case IAC:
+                    //doubled IAC to escape 255 is handled within the
+                    //read method.
+                    break;
+                case AYT:
+                    IamHere();
+                    break;
+                case AO:
+                case IP:
+                case EL:
+                case EC:
+                case NOP:
+                    break;
+                case BRK:
+                    nvtBreak();
+                    break;
+                default:
+                    return false;
+            }
+            return true;
+        }//parseTWO
+
+        /**
+         * Method that parses further on for options.
+         *
+         * @param buf that represents the first two bytes that followed the IAC.
+         */
+        private void parse(int[] buf) throws IOException {
+            switch (buf[0]) {
+        /* First switch on the Negotiation Option */
+                case WILL:
+                    if (supported(buf[1]) && isEnabled(buf[1])) {
+                        // do nothing
+                    } else {
+                        if (waitDOreply(buf[1]) && supported(buf[1])) {
+                            enable(buf[1]);
+                            setWait(DO, buf[1], false);
+                        } else {
+                            if (supported(buf[1])) {
+                                sendCommand(DO, buf[1], false);
+                                enable(buf[1]);
+                            } else {
+                                sendCommand(DONT, buf[1], false);
+                            }
+                        }
+                    }
+                    break;
+                case WONT:
+                    if (waitDOreply(buf[1]) && supported(buf[1])) {
+                        setWait(DO, buf[1], false);
+                    } else {
+                        if (supported(buf[1]) && isEnabled(buf[1])) {
+                            // eanable() Method disables an Option that is already enabled
+                            enable(buf[1]);
+                        }
+                    }
+                    break;
+                case DO:
+                    if (supported(buf[1]) && isEnabled(buf[1])) {
+                        // do nothing
+                    } else {
+                        if (waitWILLreply(buf[1]) && supported(buf[1])) {
+                            enable(buf[1]);
+                            setWait(WILL, buf[1], false);
+                        } else {
+                            if (supported(buf[1])) {
+                                sendCommand(WILL, buf[1], false);
+                                enable(buf[1]);
+                            } else {
+                                sendCommand(WONT, buf[1], false);
+                            }
+                        }
+                    }
+                    break;
+                case DONT:
+                    if (waitWILLreply(buf[1]) && supported(buf[1])) {
+                        setWait(WILL, buf[1], false);
+                    } else {
+                        if (supported(buf[1]) && isEnabled(buf[1])) {
+                            // enable() Method disables an Option that is already enabled
+                            enable(buf[1]);
+                        }
+                    }
+                    break;
+
+          /* Now about other two byte IACs */
+                case DM:    //How do I implement a SYNCH signal?
+                    break;
+                case SB: //handle subnegotiations
+                    if ((supported(buf[1])) && (isEnabled(buf[1]))) {
+                        switch (buf[1]) {
+                            case NAWS:
+                                handleNAWS();
+                                break;
+                            case TTYPE:
+                                handleTTYPE();
+                                break;
+                            case LINEMODE:
+                                handleLINEMODE();
+                                break;
+                            case NEWENV:
+                                handleNEWENV();
+                                break;
+                            default:
+                        }
+                    } else {
+                        //do nothing
+                    }
+                    break;
+                default:
+            }//switch
+        }//parse
+
+        /**
+         * Method that reads a NawsSubnegotiation that ends up with a IAC SE
+         * If the measurements are unbelieveable it switches to the defaults.
+         */
+        private void handleNAWS() throws IOException {
+            int width = read16int();
+            if (width == 255) {
+                width = read16int(); //handle doubled 255 value;
+            }
+            int height = read16int();
+            if (height == 255) {
+                height = read16int(); //handle doubled 255 value;
+            }
+            skipToSE();
+            setTerminalGeometry(width, height);
+        }//handleNAWS
+
+        /**
+         * Method that reads a TTYPE Subnegotiation String that ends up with a IAC SE
+         * If no Terminal is valid, we switch to the dumb "none" terminal.
+         */
+        private void handleTTYPE() throws IOException {
+            String tmpstr = "";
+            // The next read should be 0 which is IS by the protocol
+            // specs. hmmm?
+            rawread(); //that should be the is :)
+            tmpstr = readIACSETerminatedString(40);
+            LOG.log(Level.FINE, "Reported terminal name " + tmpstr);
+            connectionData.setNegotiatedTerminalType(tmpstr);
+        }//handleTTYPE
+
+        /**
+         * Method that handles LINEMODE subnegotiation.
+         */
+        public void handleLINEMODE() throws IOException {
+            int c = rawread();
+            switch (c) {
+                case LM_MODE:
+                    handleLMMode();
+                    break;
+                case LM_SLC:
+                    handleLMSLC();
+                    break;
+                case WONT:
+                case WILL:
+                    handleLMForwardMask(c);
+                    break;
+                default:
+                    //skip to (including) SE
+                    skipToSE();
+            }
+        }//handleLINEMODE
+
+        public void handleLMMode() throws IOException {
+            //we sent the default which no client might deny
+            //so we only wait the ACK
+            if (WAIT_LM_MODE_ACK) {
+                int mask = rawread();
+                if (mask != (LM_EDIT | LM_TRAPSIG | LM_MODEACK)) {
+                    LOG.log(Level.FINE, "Client violates linemodeack sent: " + mask);
+                }
+                WAIT_LM_MODE_ACK = false;
+            }
+            skipToSE();
+        }//handleLMMode
+
+        public void handleLMSLC() throws IOException {
+            int[] triple = new int[3];
+            if (!readTriple(triple)) return;
+
+            //SLC will be initiated by the client
+            //case 1. client requests set
+            //LINEMODE SLC 0 SLC_DEFAULT 0
+            if ((triple[0] == 0) && (triple[1] == LM_SLC_DEFAULT) && (triple[2] == 0)) {
+                skipToSE();
+                //reply with SLC xxx SLC_DEFAULT 0
+                rawWrite(IAC);
+                rawWrite(SB);
+                rawWrite(LINEMODE);
+                rawWrite(LM_SLC);
+                //triples defaults for all
+                for (int i = 1; i < 12; i++) {
+                    rawWrite(i);
+                    rawWrite(LM_SLC_DEFAULT);
+                    rawWrite(0);
+                }
+                rawWrite(IAC);
+                rawWrite(SE);
+                flush();
+            } else {
+
+                //case 2: just acknowledge anything we get from the client
+                rawWrite(IAC);
+                rawWrite(SB);
+                rawWrite(LINEMODE);
+                rawWrite(LM_SLC);
+                rawWrite(triple[0]);
+                rawWrite(triple[1] | LM_SLC_ACK);
+                rawWrite(triple[2]);
+                while (readTriple(triple)) {
+                    rawWrite(triple[0]);
+                    rawWrite(triple[1] | LM_SLC_ACK);
+                    rawWrite(triple[2]);
+                }
+                rawWrite(IAC);
+                rawWrite(SE);
+                flush();
+            }
+        }//handleLMSLC
+
+        public void handleLMForwardMask(int WHAT) throws IOException {
+            switch (WHAT) {
+                case WONT:
+                    if (WAIT_LM_DO_REPLY_FORWARDMASK) {
+                        WAIT_LM_DO_REPLY_FORWARDMASK = false;
+                    }
+                    break;
+            }
+            skipToSE();
+        }//handleLMForward
+
+        public void handleNEWENV() throws IOException {
+            LOG.log(Level.FINE, "handleNEWENV()");
+            int c = rawread();
+            switch (c) {
+                case IS:
+                    handleNEIs();
+                    break;
+                case NE_INFO:
+                    handleNEInfo();
+                    break;
+                default:
+                    //skip to (including) SE
+                    skipToSE();
+            }
+        }//handleNEWENV
+
+        /*
+          The characters following a "type" up to the next "type" or VALUE specify the
+          variable name.
+
+          If a "type" is not followed by a VALUE
+          (e.g., by another VAR, USERVAR, or IAC SE) then that variable is
+          undefined.
+         */
+        private int readNEVariableName(StringBuffer sbuf) throws IOException {
+            LOG.log(Level.FINE, "readNEVariableName()");
+            int i = -1;
+            do {
+                i = rawread();
+                if (i == -1) {
+                    return NE_IN_ERROR;
+                } else if (i == IAC) {
+                    i = rawread();
+                    if (i == IAC) {
+                        //duplicated IAC
+                        sbuf.append((char) i);
+                    } else if (i == SE) {
+                        return NE_IN_END;
+                    } else {
+                        //Error should have been duplicated
+                        return NE_IN_ERROR;
+                    }
+                } else if (i == NE_ESC) {
+                    i = rawread();
+                    if (i == NE_ESC || i == NE_VAR || i == NE_USERVAR || i == NE_VALUE) {
+                        sbuf.append((char) i);
+                    } else {
+                        return NE_IN_ERROR;
+                    }
+                } else if (i == NE_VAR || i == NE_USERVAR) {
+                    return NE_VAR_UNDEFINED;
+                } else if (i == NE_VALUE) {
+                    return NE_VAR_DEFINED;
+                } else {
+                    //check maximum length to prevent overflow
+                    if (sbuf.length() >= NE_VAR_NAME_MAXLENGTH) {
+                        //TODO: Log Overflow
+                        return NE_IN_ERROR;
+                    } else {
+                        sbuf.append((char) i);
+                    }
+                }
+            } while (true);
+        }//readNEVariableName
+
+
+        /*
+          The characters following a VALUE up to the next
+          "type" specify the value of the variable.
+          If a VALUE is immediately
+          followed by a "type" or IAC, then the variable is defined, but has
+          no value.
+          If an IAC is contained between the IS and the IAC SE,
+          it must be sent as IAC IAC.
+        */
+        private int readNEVariableValue(StringBuffer sbuf) throws IOException {
+            LOG.log(Level.FINE, "readNEVariableValue()");
+            //check conditions for first character after VALUE
+            int i = rawread();
+            if (i == -1) {
+                return NE_IN_ERROR;
+            } else if (i == IAC) {
+                i = rawread();
+                if (i == IAC) {
+                    //Double IAC
+                    return NE_VAR_DEFINED_EMPTY;
+                } else if (i == SE) {
+                    return NE_IN_END;
+                } else {
+                    //according to rule IAC has to be duplicated
+                    return NE_IN_ERROR;
+                }
+            } else if (i == NE_VAR || i == NE_USERVAR) {
+                return NE_VAR_DEFINED_EMPTY;
+            } else if (i == NE_ESC) {
+                //escaped value
+                i = rawread();
+                if (i == NE_ESC || i == NE_VAR || i == NE_USERVAR || i == NE_VALUE) {
+                    sbuf.append((char) i);
+                } else {
+                    return NE_IN_ERROR;
+                }
+            } else {
+                //character
+                sbuf.append((char) i);
+            }
+            //loop until end of value (IAC SE or TYPE)
+            do {
+                i = rawread();
+                if (i == -1) {
+                    return NE_IN_ERROR;
+                } else if (i == IAC) {
+                    i = rawread();
+                    if (i == IAC) {
+                        //duplicated IAC
+                        sbuf.append((char) i);
+                    } else if (i == SE) {
+                        return NE_IN_END;
+                    } else {
+                        //Error should have been duplicated
+                        return NE_IN_ERROR;
+                    }
+                } else if (i == NE_ESC) {
+                    i = rawread();
+                    if (i == NE_ESC || i == NE_VAR || i == NE_USERVAR || i == NE_VALUE) {
+                        sbuf.append((char) i);
+                    } else {
+                        return NE_IN_ERROR;
+                    }
+                } else if (i == NE_VAR || i == NE_USERVAR) {
+                    return NE_VAR_OK;
+                } else {
+                    //check maximum length to prevent overflow
+                    if (sbuf.length() > NE_VAR_VALUE_MAXLENGTH) {
+                        //TODO: LOG Overflow
+                        return NE_IN_ERROR;
+                    } else {
+                        sbuf.append((char) i);
+                    }
+                }
+            } while (true);
+        }//readNEVariableValue
+
+
+        public void readNEVariables() throws IOException {
+            LOG.log(Level.FINE, "readNEVariables()");
+            StringBuffer sbuf = new StringBuffer(50);
+            int i = rawread();
+            if (i == IAC) {
+                //invalid or empty response
+                skipToSE();
+                LOG.log(Level.FINE, "readNEVariables()::INVALID VARIABLE");
+                return;
+            }
+            boolean cont = true;
+            if (i == NE_VAR || i == NE_USERVAR) {
+                do {
+                    switch (readNEVariableName(sbuf)) {
+                        case NE_IN_ERROR:
+                            LOG.log(Level.FINE, "readNEVariables()::NE_IN_ERROR");
+                            return;
+                        case NE_IN_END:
+                            LOG.log(Level.FINE, "readNEVariables()::NE_IN_END");
+                            return;
+                        case NE_VAR_DEFINED:
+                            LOG.log(Level.FINE, "readNEVariables()::NE_VAR_DEFINED");
+                            String str = sbuf.toString();
+                            sbuf.delete(0, sbuf.length());
+                            switch (readNEVariableValue(sbuf)) {
+                                case NE_IN_ERROR:
+                                    LOG.log(Level.FINE, "readNEVariables()::NE_IN_ERROR");
+                                    return;
+                                case NE_IN_END:
+                                    LOG.log(Level.FINE, "readNEVariables()::NE_IN_END");
+                                    return;
+                                case NE_VAR_DEFINED_EMPTY:
+                                    LOG.log(Level.FINE, "readNEVariables()::NE_VAR_DEFINED_EMPTY");
+                                    break;
+                                case NE_VAR_OK:
+                                    //add variable
+                                    LOG.log(Level.FINE, "readNEVariables()::NE_VAR_OK:VAR=" + str + " VAL=" + sbuf.toString());
+                                    TelnetIO.this.connectionData.getEnvironment().put(str, sbuf.toString());
+                                    sbuf.delete(0, sbuf.length());
+                                    break;
+                            }
+                            break;
+                        case NE_VAR_UNDEFINED:
+                            LOG.log(Level.FINE, "readNEVariables()::NE_VAR_UNDEFINED");
+                            break;
+                    }
+                } while (cont);
+            }
+        }//readVariables
+
+        public void handleNEIs() throws IOException {
+            LOG.log(Level.FINE, "handleNEIs()");
+            if (isEnabled(NEWENV)) {
+                readNEVariables();
+            }
+        }//handleNEIs
+
+        public void handleNEInfo() throws IOException {
+            LOG.log(Level.FINE, "handleNEInfo()");
+            if (isEnabled(NEWENV)) {
+                readNEVariables();
+            }
+        }//handleNEInfo
+
+        /**
+         * Method that sends a TTYPE Subnegotiation Request.
+         * IAC SB TERMINAL-TYPE SEND
+         */
+        public void getTTYPE() throws IOException {
+            if (isEnabled(TTYPE)) {
+                rawWrite(IAC);
+                rawWrite(SB);
+                rawWrite(TTYPE);
+                rawWrite(SEND);
+                rawWrite(IAC);
+                rawWrite(SE);
+                flush();
+            }
+        }//getTTYPE
+
+        /**
+         * Method that sends a LINEMODE MODE Subnegotiation request.
+         * IAC LINEMODE MODE MASK SE
+         */
+        public void negotiateLineMode() throws IOException {
+            if (isEnabled(LINEMODE)) {
+                rawWrite(IAC);
+                rawWrite(SB);
+                rawWrite(LINEMODE);
+                rawWrite(LM_MODE);
+                rawWrite(LM_EDIT | LM_TRAPSIG);
+                rawWrite(IAC);
+                rawWrite(SE);
+                WAIT_LM_MODE_ACK = true;
+
+                //dont forwardmask
+                rawWrite(IAC);
+                rawWrite(SB);
+                rawWrite(LINEMODE);
+                rawWrite(DONT);
+                rawWrite(LM_FORWARDMASK);
+                rawWrite(IAC);
+                rawWrite(SE);
+                WAIT_LM_DO_REPLY_FORWARDMASK = true;
+                flush();
+            }
+        }//negotiateLineMode
+
+        /**
+         * Method that sends a NEW-ENVIRON SEND subnegotiation request
+         * for default variables and user variables.
+         * IAC SB NEW-ENVIRON SEND VAR USERVAR IAC SE
+         */
+        private void negotiateEnvironment() throws IOException {
+            //log.debug("negotiateEnvironment()");
+            if (isEnabled(NEWENV)) {
+                rawWrite(IAC);
+                rawWrite(SB);
+                rawWrite(NEWENV);
+                rawWrite(SEND);
+                rawWrite(NE_VAR);
+                rawWrite(NE_USERVAR);
+                rawWrite(IAC);
+                rawWrite(SE);
+                WAIT_NE_SEND_REPLY = true;
+                flush();
+            }
+        }//negotiateEnvironment
+
+        /**
+         * Method that skips a subnegotiation response.
+         */
+        private void skipToSE() throws IOException {
+            while (rawread() != SE) ;
+        }//skipSubnegotiation
+
+        private boolean readTriple(int[] triple) throws IOException {
+            triple[0] = rawread();
+            triple[1] = rawread();
+            if ((triple[0] == IAC) && (triple[1] == SE)) {
+                return false;
+            } else {
+                triple[2] = rawread();
+                return true;
+            }
+        }//readTriple
+
+        /**
+         * Method that reads a subnegotiation String,
+         * one of those that end with a IAC SE combination.
+         * A maximum length is observed to prevent overflow.
+         *
+         * @return IAC SE terminated String
+         */
+        private String readIACSETerminatedString(int maxlength) throws IOException {
+            int where = 0;
+            char[] cbuf = new char[maxlength];
+            char b = ' ';
+            boolean cont = true;
+
+            do {
+                int i;
+                i = rawread();
+                switch (i) {
+                    case IAC:
+                        i = rawread();
+                        if (i == SE) {
+                            cont = false;
+                        }
+                        break;
+                    case -1:
+                        return "default";
+                    default:
+                }
+                if (cont) {
+                    b = (char) i;
+                    //Fix for overflow wimpi (10/06/2004)
+                    if (b == '\n' || b == '\r' || where == maxlength) {
+                        cont = false;
+                    } else {
+                        cbuf[where++] = b;
+                    }
+                }
+            } while (cont);
+
+            return (new String(cbuf, 0, where));
+        }//readIACSETerminatedString
+
+        /**
+         * Method that informs internally about the supported Negotiation Options
+         *
+         * @param i int that represents requested the Option
+         * @return Boolean that represents support status
+         */
+        private boolean supported(int i) {
+            switch (i) {
+                case SUPGA:
+                case ECHO:
+                case NAWS:
+                case TTYPE:
+                case NEWENV:
+                    return true;
+                case LINEMODE:
+                    return connectionData.isLineMode();
+                default:
+                    return false;
+            }
+        }//supported
+
+        /**
+         * Method that sends a Telnet IAC String with TelnetIO.write(byte b) method.
+         *
+         * @param i int that represents requested Command Type (DO,DONT,WILL,WONT)
+         * @param j int that represents the Option itself (e.g. ECHO, NAWS)
+         */
+        private void sendCommand(int i, int j, boolean westarted) throws IOException {
+            rawWrite(IAC);
+            rawWrite(i);
+            rawWrite(j);
+            // we started with DO OPTION and now wait for reply
+            if ((i == DO) && westarted) setWait(DO, j, true);
+            // we started with WILL OPTION and now wait for reply
+            if ((i == WILL) && westarted) setWait(WILL, j, true);
+            flush();
+        }//sendCommand
+
+        /**
+         * Method enables or disables a supported Option
+         *
+         * @param i int that represents the Option
+         */
+        private void enable(int i) throws IOException {
+            switch (i) {
+                case SUPGA:
+                    DO_SUPGA = !DO_SUPGA;
+                    break;
+                case ECHO:
+                    DO_ECHO = !DO_ECHO;
+                    break;
+                case NAWS:
+                    DO_NAWS = !DO_NAWS;
+                    break;
+                case TTYPE:
+                    if (DO_TTYPE) {
+                        DO_TTYPE = false;
+                    } else {
+                        DO_TTYPE = true;
+                        getTTYPE();
+                    }
+                    break;
+                case LINEMODE:
+                    if (DO_LINEMODE) {
+                        DO_LINEMODE = false;
+                        //set false in connection data, so the application knows.
+                        connectionData.setLineMode(false);
+                    } else {
+                        DO_LINEMODE = true;
+                        negotiateLineMode();
+                    }
+                    break;
+                case NEWENV:
+                    if (DO_NEWENV) {
+                        DO_NEWENV = false;
+                    } else {
+                        DO_NEWENV = true;
+                        negotiateEnvironment();
+                    }
+                    break;
+            }
+        }//enable
+
+        /**
+         * Method that informs internally about the status of the supported
+         * Negotiation Options.
+         *
+         * @param i int that represents requested the Option
+         * @return Boolean that represents the enabled status
+         */
+        private boolean isEnabled(int i) {
+            switch (i) {
+                case SUPGA:
+                    return DO_SUPGA;
+                case ECHO:
+                    return DO_ECHO;
+                case NAWS:
+                    return DO_NAWS;
+                case TTYPE:
+                    return DO_TTYPE;
+                case LINEMODE:
+                    return DO_LINEMODE;
+                case NEWENV:
+                    return DO_NEWENV;
+                default:
+                    return false;
+            }
+        }//isEnabled
+
+        /**
+         * Method that informs internally about the WILL wait status
+         * of an option.
+         *
+         * @param i that represents requested the Option
+         * @return Boolean that represents WILL wait status of the Option
+         */
+        private boolean waitWILLreply(int i) {
+            switch (i) {
+                case SUPGA:
+                    return WAIT_WILL_REPLY_SUPGA;
+                case ECHO:
+                    return WAIT_WILL_REPLY_ECHO;
+                case NAWS:
+                    return WAIT_WILL_REPLY_NAWS;
+                case TTYPE:
+                    return WAIT_WILL_REPLY_TTYPE;
+                default:
+                    return false;
+            }
+        }//waitWILLreply
+
+        /**
+         * Method that informs internally about the DO wait status
+         * of an option.
+         *
+         * @param i Integer that represents requested the Option
+         * @return Boolean that represents DO wait status of the Option
+         */
+        private boolean waitDOreply(int i) {
+            switch (i) {
+                case SUPGA:
+                    return WAIT_DO_REPLY_SUPGA;
+                case ECHO:
+                    return WAIT_DO_REPLY_ECHO;
+                case NAWS:
+                    return WAIT_DO_REPLY_NAWS;
+                case TTYPE:
+                    return WAIT_DO_REPLY_TTYPE;
+                case LINEMODE:
+                    return WAIT_DO_REPLY_LINEMODE;
+                case NEWENV:
+                    return WAIT_DO_REPLY_NEWENV;
+                default:
+                    return false;
+            }
+        }//waitDOreply
+
+        /**
+         * Method that mutates the wait status of an option in
+         * negotiation. We need the wait status to keep track of
+         * negotiation in process. So we cant miss if we started out
+         * or the other and so on.
+         *
+         * @param WHAT   Integer values of  DO or WILL
+         * @param OPTION Integer that represents the Option
+         * @param WAIT   Boolean that represents the status of wait that should be set
+         */
+        private void setWait(int WHAT, int OPTION, boolean WAIT) {
+            switch (WHAT) {
+                case DO:
+                    switch (OPTION) {
+                        case SUPGA:
+                            WAIT_DO_REPLY_SUPGA = WAIT;
+                            break;
+                        case ECHO:
+                            WAIT_DO_REPLY_ECHO = WAIT;
+                            break;
+                        case NAWS:
+                            WAIT_DO_REPLY_NAWS = WAIT;
+                            break;
+                        case TTYPE:
+                            WAIT_DO_REPLY_TTYPE = WAIT;
+                            break;
+                        case LINEMODE:
+                            WAIT_DO_REPLY_LINEMODE = WAIT;
+                            break;
+                        case NEWENV:
+                            WAIT_DO_REPLY_NEWENV = WAIT;
+                            break;
+                    }
+                    break;
+                case WILL:
+                    switch (OPTION) {
+                        case SUPGA:
+                            WAIT_WILL_REPLY_SUPGA = WAIT;
+                            break;
+                        case ECHO:
+                            WAIT_WILL_REPLY_ECHO = WAIT;
+                            break;
+                        case NAWS:
+                            WAIT_WILL_REPLY_NAWS = WAIT;
+                            break;
+                        case TTYPE:
+                            WAIT_WILL_REPLY_TTYPE = WAIT;
+                            break;
+                    }
+                    break;
+            }
+        }//setWait
+
+    }//inner class IACHandler
+
+    /** end Constants declaration **************************************************/
+
+}//class TelnetIO