[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 <-> Telnet Client. <BR>
+ * It should work like this we send a TCP urgent package and <IAC> <DM> the receiver
+ * should get the urgent package (SYNCH) and just discard everything until he receives
+ * our <IAC> <DM>.<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