You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@mina.apache.org by lg...@apache.org on 2018/04/17 12:33:10 UTC

[6/6] mina-sshd git commit: [SSHD-816] Moved all 'main' code for client commands to sshd-cli module

[SSHD-816] Moved all 'main' code for client commands to sshd-cli module


Project: http://git-wip-us.apache.org/repos/asf/mina-sshd/repo
Commit: http://git-wip-us.apache.org/repos/asf/mina-sshd/commit/536effdc
Tree: http://git-wip-us.apache.org/repos/asf/mina-sshd/tree/536effdc
Diff: http://git-wip-us.apache.org/repos/asf/mina-sshd/diff/536effdc

Branch: refs/heads/master
Commit: 536effdc5c1a9a0b8dcc0fc290b59fddd7efeb51
Parents: 76fe821
Author: Goldstein Lyor <ly...@c-b4.com>
Authored: Tue Apr 17 10:13:01 2018 +0300
Committer: Goldstein Lyor <ly...@c-b4.com>
Committed: Tue Apr 17 15:32:31 2018 +0300

----------------------------------------------------------------------
 README.md                                       |  14 +-
 assembly/pom.xml                                |   5 +
 assembly/src/main/distribution/bin/scp.bat      |   2 +-
 assembly/src/main/distribution/bin/scp.sh       |   2 +-
 assembly/src/main/distribution/bin/sftp.bat     |   2 +-
 assembly/src/main/distribution/bin/sftp.sh      |   2 +-
 .../src/main/distribution/bin/ssh-keyscan.bat   |   2 +-
 .../src/main/distribution/bin/ssh-keyscan.sh    |   2 +-
 assembly/src/main/distribution/bin/ssh.bat      |   2 +-
 assembly/src/main/distribution/bin/ssh.sh       |   2 +-
 pom.xml                                         |   1 +
 sshd-cli/pom.xml                                | 115 +++
 .../java/org/apache/sshd/cli/CliSupport.java    |  38 +
 .../apache/sshd/cli/client/ScpCommandMain.java  | 220 +++++
 .../sshd/cli/client/SftpCommandExecutor.java    |  34 +
 .../apache/sshd/cli/client/SftpCommandMain.java | 919 ++++++++++++++++++
 .../sshd/cli/client/SshClientCliSupport.java    | 632 +++++++++++++
 .../apache/sshd/cli/client/SshClientMain.java   | 193 ++++
 .../apache/sshd/cli/client/SshKeyScanMain.java  | 741 +++++++++++++++
 .../apache/sshd/cli/client/ChannelExecMain.java |  93 ++
 .../cli/client/ScpCommandMainDevelopment.java   |  36 +
 .../cli/client/SftpCommandMainDevelopment.java  |  36 +
 .../cli/client/SshClientMainDevelopment.java    |  36 +
 .../cli/client/SshKeyScanMainDevelopment.java   |  36 +
 .../java/org/apache/sshd/client/SshClient.java  | 740 ---------------
 .../java/org/apache/sshd/client/SshKeyScan.java | 740 ---------------
 .../sshd/client/scp/DefaultScpClient.java       | 184 ----
 .../org/apache/sshd/client/SshClientMain.java   |  36 -
 .../org/apache/sshd/client/SshKeyScanMain.java  |  36 -
 .../sshd/client/channel/ChannelExecMain.java    |  93 --
 .../apache/sshd/client/scp/ScpCommandMain.java  |  36 -
 .../sshd/client/subsystem/sftp/SftpCommand.java | 920 -------------------
 .../client/subsystem/sftp/SftpCommandMain.java  |  36 -
 33 files changed, 3155 insertions(+), 2831 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/536effdc/README.md
----------------------------------------------------------------------
diff --git a/README.md b/README.md
index ecf2317..bcef46f 100644
--- a/README.md
+++ b/README.md
@@ -1279,9 +1279,19 @@ in case the classes it used it are modified or deleted.
 
 ## Command line clients
 
-The _apache-sshd.zip_ distribution provides `Windows/Linux` scripts that use the MINA SSHD code base to implement the common _ssh, scp, sftp_ commands. The clients accept most useful switches from the original commands they mimic, where the `-o Option=Value` arguments can be used to configure the client/server in addition to the system properties mechanism. For more details, consult the _main_ methods code in the respective `SshClient`, `SftpCommand` and `DefaultScpClient` classes. The code also includes `SshKeyScan#main` that is a simple implementation for [ssh-keyscan(1)](https://www.freebsd.org/cgi/man.cgi?query=ssh-keyscan&sektion=1).
+The _apache-sshd.zip_ distribution provides `Windows/Linux` scripts that use the MINA SSHD code base to implement the common _ssh, scp, sftp_ commands. The clients accept most useful switches from the original commands they mimic, where the `-o Option=Value` arguments can be used to configure the client/server in addition to the system properties mechanism. For more details, consult the _main_ methods code in the respective `SshClientMain`, `SftpCommandMain` and `ScpClientMain` classes. The code also includes `SshKeyScanMain` that is a simple implementation for [ssh-keyscan(1)](https://www.freebsd.org/cgi/man.cgi?query=ssh-keyscan&sektion=1).
 
-The distribution also includes also an _sshd_ script that can be used to launch a server instance - see `SshServer#main` for activation command line arguments and options.
+The distribution also includes also an _sshd_ script that can be used to launch a server instance - see `SshServerMain#main` for activation command line arguments and options.
+
+In order to use this CLI code as part of another project, one needs to include the _sshd-cli_ module:
+
+```xml
+    <dependency>
+        <groupId>org.apache.sshd</groupId>
+        <artifactId>sshd-cli</artifactId>
+        <version>...same version as the core...</version>
+    </dependency>
+```
 
 ## GIT support
 

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/536effdc/assembly/pom.xml
----------------------------------------------------------------------
diff --git a/assembly/pom.xml b/assembly/pom.xml
index 85eab19..f3c4716 100644
--- a/assembly/pom.xml
+++ b/assembly/pom.xml
@@ -47,6 +47,11 @@
             <version>${project.version}</version>
         </dependency>
         <dependency>
+            <groupId>org.apache.sshd</groupId>
+            <artifactId>sshd-cli</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+        <dependency>
             <groupId>org.slf4j</groupId>
             <artifactId>slf4j-jdk14</artifactId>
         </dependency>

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/536effdc/assembly/src/main/distribution/bin/scp.bat
----------------------------------------------------------------------
diff --git a/assembly/src/main/distribution/bin/scp.bat b/assembly/src/main/distribution/bin/scp.bat
index ce86067..68eea30 100644
--- a/assembly/src/main/distribution/bin/scp.bat
+++ b/assembly/src/main/distribution/bin/scp.bat
@@ -91,7 +91,7 @@ goto :EOF
 SET ARGS=%1 %2 %3 %4 %5 %6 %7 %8
 rem Execute the Java Virtual Machine
 cd %SSHD_HOME%
-"%JAVA%" %JAVA_OPTS% %OPTS% -classpath "%CLASSPATH%" -Dsshd.home="%SSHD_HOME%" org.apache.sshd.client.scp.DefaultScpClient %ARGS%
+"%JAVA%" %JAVA_OPTS% %OPTS% -classpath "%CLASSPATH%" -Dsshd.home="%SSHD_HOME%" org.apache.sshd.cli.client.ScpCommandMain %ARGS%
 
 rem # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
 

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/536effdc/assembly/src/main/distribution/bin/scp.sh
----------------------------------------------------------------------
diff --git a/assembly/src/main/distribution/bin/scp.sh b/assembly/src/main/distribution/bin/scp.sh
index 55d26b8..0d28c89 100644
--- a/assembly/src/main/distribution/bin/scp.sh
+++ b/assembly/src/main/distribution/bin/scp.sh
@@ -255,7 +255,7 @@ run() {
         CLASSPATH=`cygpath --path --windows "$CLASSPATH"`
     fi
     cd $SSHD_BASE
-    exec $JAVA $JAVA_OPTS -Dsshd.home="$SSHD_HOME" $OPTS -classpath "$CLASSPATH" org.apache.sshd.client.scp.DefaultScpClient "$@"
+    exec $JAVA $JAVA_OPTS -Dsshd.home="$SSHD_HOME" $OPTS -classpath "$CLASSPATH" org.apache.sshd.cli.client.ScpCommandMain "$@"
 }
 
 main() {

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/536effdc/assembly/src/main/distribution/bin/sftp.bat
----------------------------------------------------------------------
diff --git a/assembly/src/main/distribution/bin/sftp.bat b/assembly/src/main/distribution/bin/sftp.bat
index f4c5aee..3a1b908 100644
--- a/assembly/src/main/distribution/bin/sftp.bat
+++ b/assembly/src/main/distribution/bin/sftp.bat
@@ -91,7 +91,7 @@ goto :EOF
 SET ARGS=%1 %2 %3 %4 %5 %6 %7 %8
 rem Execute the Java Virtual Machine
 cd %SSHD_HOME%
-"%JAVA%" %JAVA_OPTS% %OPTS% -classpath "%CLASSPATH%" -Dsshd.home="%SSHD_HOME%" org.apache.sshd.client.subsystem.sftp.SftpCommand %ARGS%
+"%JAVA%" %JAVA_OPTS% %OPTS% -classpath "%CLASSPATH%" -Dsshd.home="%SSHD_HOME%" org.apache.sshd.cli.client.SftpCommandMain %ARGS%
 
 rem # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
 

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/536effdc/assembly/src/main/distribution/bin/sftp.sh
----------------------------------------------------------------------
diff --git a/assembly/src/main/distribution/bin/sftp.sh b/assembly/src/main/distribution/bin/sftp.sh
index 8884ce8..477e7fe 100644
--- a/assembly/src/main/distribution/bin/sftp.sh
+++ b/assembly/src/main/distribution/bin/sftp.sh
@@ -255,7 +255,7 @@ run() {
         CLASSPATH=`cygpath --path --windows "$CLASSPATH"`
     fi
     cd $SSHD_BASE
-    exec $JAVA $JAVA_OPTS -Dsshd.home="$SSHD_HOME" $OPTS -classpath "$CLASSPATH" org.apache.sshd.client.subsystem.sftp.SftpCommand "$@"
+    exec $JAVA $JAVA_OPTS -Dsshd.home="$SSHD_HOME" $OPTS -classpath "$CLASSPATH" org.apache.sshd.cli.client.SftpCommandMain "$@"
 }
 
 main() {

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/536effdc/assembly/src/main/distribution/bin/ssh-keyscan.bat
----------------------------------------------------------------------
diff --git a/assembly/src/main/distribution/bin/ssh-keyscan.bat b/assembly/src/main/distribution/bin/ssh-keyscan.bat
index bbbd1e0..1c2b78e 100644
--- a/assembly/src/main/distribution/bin/ssh-keyscan.bat
+++ b/assembly/src/main/distribution/bin/ssh-keyscan.bat
@@ -91,7 +91,7 @@ goto :EOF
 SET ARGS=%1 %2 %3 %4 %5 %6 %7 %8
 rem Execute the Java Virtual Machine
 cd %SSHD_HOME%
-"%JAVA%" %JAVA_OPTS% %OPTS% -classpath "%CLASSPATH%" -Dsshd.home="%SSHD_HOME%" org.apache.sshd.client.SshKeyScan %ARGS%
+"%JAVA%" %JAVA_OPTS% %OPTS% -classpath "%CLASSPATH%" -Dsshd.home="%SSHD_HOME%" org.apache.sshd.cli.client.SshKeyScanMain %ARGS%
 
 rem # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
 

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/536effdc/assembly/src/main/distribution/bin/ssh-keyscan.sh
----------------------------------------------------------------------
diff --git a/assembly/src/main/distribution/bin/ssh-keyscan.sh b/assembly/src/main/distribution/bin/ssh-keyscan.sh
index e6245e0..083f7b8 100644
--- a/assembly/src/main/distribution/bin/ssh-keyscan.sh
+++ b/assembly/src/main/distribution/bin/ssh-keyscan.sh
@@ -255,7 +255,7 @@ run() {
         CLASSPATH=`cygpath --path --windows "$CLASSPATH"`
     fi
     cd $SSHD_BASE
-    exec $JAVA $JAVA_OPTS -Dsshd.home="$SSHD_HOME" $OPTS -classpath "$CLASSPATH" org.apache.sshd.client.SshKeyScan "$@"
+    exec $JAVA $JAVA_OPTS -Dsshd.home="$SSHD_HOME" $OPTS -classpath "$CLASSPATH" org.apache.sshd.cli.client.SshKeyScanMain "$@"
 }
 
 main() {

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/536effdc/assembly/src/main/distribution/bin/ssh.bat
----------------------------------------------------------------------
diff --git a/assembly/src/main/distribution/bin/ssh.bat b/assembly/src/main/distribution/bin/ssh.bat
index 14b5b41..85e93c2 100644
--- a/assembly/src/main/distribution/bin/ssh.bat
+++ b/assembly/src/main/distribution/bin/ssh.bat
@@ -91,7 +91,7 @@ goto :EOF
 SET ARGS=%1 %2 %3 %4 %5 %6 %7 %8
 rem Execute the Java Virtual Machine
 cd %SSHD_HOME%
-"%JAVA%" %JAVA_OPTS% %OPTS% -classpath "%CLASSPATH%" -Dsshd.home="%SSHD_HOME%" org.apache.sshd.client.SshClient %ARGS%
+"%JAVA%" %JAVA_OPTS% %OPTS% -classpath "%CLASSPATH%" -Dsshd.home="%SSHD_HOME%" org.apache.sshd.cli.client.SshClientMain %ARGS%
 
 rem # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
 

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/536effdc/assembly/src/main/distribution/bin/ssh.sh
----------------------------------------------------------------------
diff --git a/assembly/src/main/distribution/bin/ssh.sh b/assembly/src/main/distribution/bin/ssh.sh
index 3e29813..fe07901 100644
--- a/assembly/src/main/distribution/bin/ssh.sh
+++ b/assembly/src/main/distribution/bin/ssh.sh
@@ -255,7 +255,7 @@ run() {
         CLASSPATH=`cygpath --path --windows "$CLASSPATH"`
     fi
     cd $SSHD_BASE
-    exec $JAVA $JAVA_OPTS -Dsshd.home="$SSHD_HOME" $OPTS -classpath "$CLASSPATH" org.apache.sshd.client.SshClient "$@"
+    exec $JAVA $JAVA_OPTS -Dsshd.home="$SSHD_HOME" $OPTS -classpath "$CLASSPATH" org.apache.sshd.cli.client.SshClientMain "$@"
 }
 
 main() {

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/536effdc/pom.xml
----------------------------------------------------------------------
diff --git a/pom.xml b/pom.xml
index ea73bb4..9cbf957 100644
--- a/pom.xml
+++ b/pom.xml
@@ -1010,6 +1010,7 @@
         <module>sshd-git</module>
         <module>sshd-contrib</module>
         <module>sshd-spring-sftp</module>
+        <module>sshd-cli</module>
         <module>assembly</module>
     </modules>
 </project>

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/536effdc/sshd-cli/pom.xml
----------------------------------------------------------------------
diff --git a/sshd-cli/pom.xml b/sshd-cli/pom.xml
new file mode 100644
index 0000000..bd19a7e
--- /dev/null
+++ b/sshd-cli/pom.xml
@@ -0,0 +1,115 @@
+<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">
+
+
+    <!--
+
+        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.
+    -->
+
+    <modelVersion>4.0.0</modelVersion>
+
+    <parent>
+        <groupId>org.apache.sshd</groupId>
+        <artifactId>sshd</artifactId>
+        <version>1.7.1-SNAPSHOT</version>
+    </parent>
+
+    <artifactId>sshd-cli</artifactId>
+    <name>Apache Mina SSHD :: CLI</name>
+    <packaging>jar</packaging>
+    <inceptionYear>2018</inceptionYear>
+
+    <properties>
+        <projectRoot>${project.basedir}/..</projectRoot>
+    </properties>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.apache.sshd</groupId>
+            <artifactId>sshd-core</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.sshd</groupId>
+            <artifactId>sshd-sftp</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+
+        <dependency>
+            <groupId>org.apache.sshd</groupId>
+            <artifactId>sshd-core</artifactId>
+            <version>${project.version}</version>
+            <type>test-jar</type>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>junit</groupId>
+            <artifactId>junit</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.mockito</groupId>
+            <artifactId>mockito-core</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.slf4j</groupId>
+            <artifactId>jcl-over-slf4j</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.slf4j</groupId>
+            <artifactId>slf4j-log4j12</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>com.jcraft</groupId>
+            <artifactId>jsch</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>com.jcraft</groupId>
+            <artifactId>jzlib</artifactId>
+            <scope>test</scope>
+        </dependency>
+    </dependencies>
+
+    <build>
+        <resources>
+            <resource>
+                <directory>src/main/filtered-resources</directory>
+                <filtering>true</filtering>
+            </resource>
+        </resources>
+
+        <plugins>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-surefire-plugin</artifactId>
+                <configuration>
+                    <redirectTestOutputToFile>true</redirectTestOutputToFile>
+                </configuration>
+            </plugin>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-javadoc-plugin</artifactId>
+                <configuration>
+                    <additionalparam>-Xdoclint:none</additionalparam>
+                </configuration>
+            </plugin>
+        </plugins>
+    </build>
+</project>

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/536effdc/sshd-cli/src/main/java/org/apache/sshd/cli/CliSupport.java
----------------------------------------------------------------------
diff --git a/sshd-cli/src/main/java/org/apache/sshd/cli/CliSupport.java b/sshd-cli/src/main/java/org/apache/sshd/cli/CliSupport.java
new file mode 100644
index 0000000..14737bb
--- /dev/null
+++ b/sshd-cli/src/main/java/org/apache/sshd/cli/CliSupport.java
@@ -0,0 +1,38 @@
+/*
+ * 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.sshd.cli;
+
+import java.io.PrintStream;
+
+/**
+ * TODO Add javadoc
+ *
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+public abstract class CliSupport {
+    protected CliSupport() {
+        super();
+    }
+
+    public static boolean showError(PrintStream stderr, String message) {
+        stderr.println(message);
+        return true;
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/536effdc/sshd-cli/src/main/java/org/apache/sshd/cli/client/ScpCommandMain.java
----------------------------------------------------------------------
diff --git a/sshd-cli/src/main/java/org/apache/sshd/cli/client/ScpCommandMain.java b/sshd-cli/src/main/java/org/apache/sshd/cli/client/ScpCommandMain.java
new file mode 100644
index 0000000..770ab32
--- /dev/null
+++ b/sshd-cli/src/main/java/org/apache/sshd/cli/client/ScpCommandMain.java
@@ -0,0 +1,220 @@
+/*
+ * 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.sshd.cli.client;
+
+import java.io.BufferedReader;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.io.PrintStream;
+import java.nio.charset.Charset;
+import java.nio.file.Path;
+import java.nio.file.attribute.PosixFilePermission;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.EnumSet;
+import java.util.List;
+import java.util.Set;
+import java.util.logging.Level;
+
+import org.apache.sshd.client.scp.ScpClient;
+import org.apache.sshd.client.scp.ScpClient.Option;
+import org.apache.sshd.client.session.ClientSession;
+import org.apache.sshd.common.scp.ScpLocation;
+import org.apache.sshd.common.scp.ScpTransferEventListener;
+import org.apache.sshd.common.util.GenericUtils;
+import org.apache.sshd.common.util.io.NoCloseInputStream;
+
+/**
+ * TODO Add javadoc
+ *
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+public class ScpCommandMain extends SshClientCliSupport {
+    /**
+     * Command line option used to indicate a non-default port
+     */
+    public static final String SCP_PORT_OPTION = "-P";
+
+    public ScpCommandMain() {
+        super();    // in case someone wants to extend it
+    }
+
+    //////////////////////////////////////////////////////////////////////////
+
+    public static String[] normalizeCommandArguments(PrintStream stdout, PrintStream stderr, String... args) {
+        int numArgs = GenericUtils.length(args);
+        if (numArgs <= 0) {
+            return args;
+        }
+
+        List<String> effective = new ArrayList<>(numArgs);
+        boolean error = false;
+        for (int index = 0; (index < numArgs) && (!error); index++) {
+            String argName = args[index];
+            // handled by 'setupClientSession'
+            if (isArgumentedOption(SCP_PORT_OPTION, argName)) {
+                if ((index + 1) >= numArgs) {
+                    error = showError(stderr, "option requires an argument: " + argName);
+                    break;
+                }
+
+                effective.add(argName);
+                effective.add(args[++index]);
+            } else if ("-r".equals(argName) || "-p".equals(argName)
+                    || "-q".equals(argName) || "-C".equals(argName)
+                    || "-v".equals(argName) || "-vv".equals(argName) || "-vvv".equals(argName)) {
+                effective.add(argName);
+            } else if (argName.charAt(0) == '-') {
+                error = showError(stderr, "Unknown option: " + argName);
+                break;
+            } else {
+                if ((index + 1) >= numArgs) {
+                    error = showError(stderr, "Not enough arguments");
+                    break;
+                }
+
+                ScpLocation source = new ScpLocation(argName);
+                ScpLocation target = new ScpLocation(args[++index]);
+                if (index < (numArgs - 1)) {
+                    error = showError(stderr, "Unexpected extra arguments");
+                    break;
+                }
+
+                if (source.isLocal() == target.isLocal()) {
+                    error = showError(stderr, "Both targets are either remote or local");
+                    break;
+                }
+
+                ScpLocation remote = source.isLocal() ? target : source;
+                effective.add(remote.resolveUsername() + "@" + remote.getHost());
+                effective.add(source.toString());
+                effective.add(target.toString());
+                break;
+            }
+        }
+
+        if (error) {
+            return null;
+        }
+
+        return effective.toArray(new String[effective.size()]);
+    }
+
+    public static void main(String[] args) throws Exception {
+        final PrintStream stdout = System.out;
+        final PrintStream stderr = System.err;
+        OutputStream logStream = stdout;
+        try (BufferedReader stdin = new BufferedReader(
+                new InputStreamReader(new NoCloseInputStream(System.in), Charset.defaultCharset()))) {
+            args = normalizeCommandArguments(stdout, stderr, args);
+            int numArgs = GenericUtils.length(args);
+            // see the way normalizeCommandArguments works...
+            if (numArgs >= 2) {
+                Level level = resolveLoggingVerbosity(args, numArgs - 2);
+                logStream = resolveLoggingTargetStream(stdout, stderr, args, numArgs - 2);
+                if (logStream != null) {
+                    setupLogging(level, stdout, stderr, logStream);
+                }
+            }
+
+            ClientSession session = (logStream == null) || GenericUtils.isEmpty(args)
+                ? null : setupClientSession(SCP_PORT_OPTION, stdin, stdout, stderr, args);
+            if (session == null) {
+                stderr.println("usage: scp [" + SCP_PORT_OPTION + " port] [-i identity]"
+                         + " [-v[v][v]] [-E logoutput] [-r] [-p] [-q] [-o option=value]"
+                         + " [-c cipherlist] [-m maclist] [-w password] [-C] <source> <target>");
+                stderr.println();
+                stderr.println("Where <source> or <target> are either 'user@host:file' or a local file path");
+                stderr.println("NOTE: exactly ONE of the source or target must be remote and the other one local");
+                System.exit(-1);
+                return; // not that we really need it...
+            }
+
+            try {
+                // see the way normalizeCommandArguments works...
+                Collection<Option> options = EnumSet.noneOf(Option.class);
+                boolean quiet = false;
+                for (int index = 0; index < numArgs; index++) {
+                    String argName = args[index];
+                    if ("-r".equals(argName)) {
+                        options.add(Option.Recursive);
+                    } else if ("-p".equals(argName)) {
+                        options.add(Option.PreserveAttributes);
+                    } else if ("-q".equals(argName)) {
+                        quiet = true;
+                    }
+                }
+
+                if (!quiet) {
+                    session.setScpTransferEventListener(new ScpTransferEventListener() {
+                        @Override
+                        public void startFolderEvent(FileOperation op, Path file, Set<PosixFilePermission> perms) {
+                            logEvent("startFolderEvent", op, file, -1L, perms, null);
+                        }
+
+                        @Override
+                        public void endFolderEvent(FileOperation op, Path file, Set<PosixFilePermission> perms, Throwable thrown) {
+                            logEvent("endFolderEvent", op, file, -1L, perms, thrown);
+                        }
+
+                        @Override
+                        public void startFileEvent(FileOperation op, Path file, long length, Set<PosixFilePermission> perms) {
+                            logEvent("startFileEvent", op, file, length, perms, null);
+                        }
+
+                        @Override
+                        public void endFileEvent(FileOperation op, Path file, long length, Set<PosixFilePermission> perms, Throwable thrown) {
+                            logEvent("endFileEvent", op, file, length, perms, thrown);
+                        }
+
+                        private void logEvent(String name, FileOperation op, Path file, long length, Collection<PosixFilePermission> perms, Throwable thrown) {
+                            PrintStream ps = (thrown == null) ? stdout : stderr;
+                            ps.append('\t').append(name).append('[').append(op.name()).append(']').append(' ').append(file.toString());
+                            if (length > 0L) {
+                                ps.append(' ').append("length=").append(Long.toString(length));
+                            }
+                            ps.append(' ').append(String.valueOf(perms));
+
+                            if (thrown != null) {
+                                ps.append(" - ").append(thrown.getClass().getSimpleName()).append(": ").append(thrown.getMessage());
+                            }
+                            ps.println();
+                        }
+                    });
+                }
+
+                ScpClient client = session.createScpClient();
+                ScpLocation source = new ScpLocation(args[numArgs - 2]);
+                ScpLocation target = new ScpLocation(args[numArgs - 1]);
+                if (source.isLocal()) {
+                    client.upload(source.getPath(), target.getPath(), options);
+                } else {
+                    client.download(source.getPath(), target.getPath(), options);
+                }
+            } finally {
+                session.close();
+            }
+        } finally {
+            if ((logStream != stdout) && (logStream != stderr)) {
+                logStream.close();
+            }
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/536effdc/sshd-cli/src/main/java/org/apache/sshd/cli/client/SftpCommandExecutor.java
----------------------------------------------------------------------
diff --git a/sshd-cli/src/main/java/org/apache/sshd/cli/client/SftpCommandExecutor.java b/sshd-cli/src/main/java/org/apache/sshd/cli/client/SftpCommandExecutor.java
new file mode 100644
index 0000000..fb1a762
--- /dev/null
+++ b/sshd-cli/src/main/java/org/apache/sshd/cli/client/SftpCommandExecutor.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.
+ */
+package org.apache.sshd.cli.client;
+
+import java.io.BufferedReader;
+import java.io.PrintStream;
+
+import org.apache.sshd.common.NamedResource;
+
+/**
+ * TODO Add javadoc
+ *
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+public interface SftpCommandExecutor extends NamedResource {
+    // return value is whether to stop running
+    boolean executeCommand(String args, BufferedReader stdin, PrintStream stdout, PrintStream stderr) throws Exception;
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/536effdc/sshd-cli/src/main/java/org/apache/sshd/cli/client/SftpCommandMain.java
----------------------------------------------------------------------
diff --git a/sshd-cli/src/main/java/org/apache/sshd/cli/client/SftpCommandMain.java b/sshd-cli/src/main/java/org/apache/sshd/cli/client/SftpCommandMain.java
new file mode 100644
index 0000000..1d982f2
--- /dev/null
+++ b/sshd-cli/src/main/java/org/apache/sshd/cli/client/SftpCommandMain.java
@@ -0,0 +1,919 @@
+/*
+ * 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.sshd.cli.client;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.io.PrintStream;
+import java.lang.reflect.Field;
+import java.lang.reflect.Modifier;
+import java.nio.channels.Channel;
+import java.nio.file.DirectoryStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Map;
+import java.util.Objects;
+import java.util.TreeMap;
+import java.util.logging.Level;
+
+import org.apache.sshd.client.session.ClientSession;
+import org.apache.sshd.client.subsystem.sftp.SftpClient;
+import org.apache.sshd.client.subsystem.sftp.SftpClient.Attributes;
+import org.apache.sshd.client.subsystem.sftp.SftpClient.DirEntry;
+import org.apache.sshd.client.subsystem.sftp.SftpClientFactory;
+import org.apache.sshd.client.subsystem.sftp.SftpFileSystemProvider;
+import org.apache.sshd.client.subsystem.sftp.extensions.openssh.OpenSSHStatExtensionInfo;
+import org.apache.sshd.client.subsystem.sftp.extensions.openssh.OpenSSHStatPathExtension;
+import org.apache.sshd.common.io.IoSession;
+import org.apache.sshd.common.kex.KexProposalOption;
+import org.apache.sshd.common.session.Session;
+import org.apache.sshd.common.subsystem.sftp.SftpConstants;
+import org.apache.sshd.common.subsystem.sftp.SftpException;
+import org.apache.sshd.common.subsystem.sftp.extensions.ParserUtils;
+import org.apache.sshd.common.subsystem.sftp.extensions.openssh.StatVfsExtensionParser;
+import org.apache.sshd.common.util.GenericUtils;
+import org.apache.sshd.common.util.OsUtils;
+import org.apache.sshd.common.util.ValidateUtils;
+import org.apache.sshd.common.util.buffer.BufferUtils;
+import org.apache.sshd.common.util.io.IoUtils;
+import org.apache.sshd.common.util.io.NoCloseInputStream;
+
+/**
+ * TODO Add javadoc
+ *
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+public class SftpCommandMain extends SshClientCliSupport implements Channel {
+    /**
+     * Command line option used to indicate a non-default port number
+     */
+    public static final String SFTP_PORT_OPTION = "-P";
+
+    private final SftpClient client;
+    private final Map<String, SftpCommandExecutor> commandsMap;
+    private String cwdRemote;
+    private String cwdLocal;
+
+    public SftpCommandMain(SftpClient client) {
+        this.client = Objects.requireNonNull(client, "No client");
+
+        Map<String, SftpCommandExecutor> map = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
+        for (SftpCommandExecutor e : Arrays.asList(
+                new ExitCommandExecutor(),
+                new PwdCommandExecutor(),
+                new InfoCommandExecutor(),
+                new SessionCommandExecutor(),
+                new VersionCommandExecutor(),
+                new CdCommandExecutor(),
+                new LcdCommandExecutor(),
+                new MkdirCommandExecutor(),
+                new LsCommandExecutor(),
+                new LStatCommandExecutor(),
+                new ReadLinkCommandExecutor(),
+                new RmCommandExecutor(),
+                new RmdirCommandExecutor(),
+                new RenameCommandExecutor(),
+                new StatVfsCommandExecutor(),
+                new GetCommandExecutor(),
+                new PutCommandExecutor(),
+                new HelpCommandExecutor()
+        )) {
+            String name = e.getName();
+            ValidateUtils.checkTrue(map.put(name, e) == null, "Multiple commands named '%s'", name);
+        }
+        commandsMap = Collections.unmodifiableMap(map);
+        cwdLocal = System.getProperty("user.dir");
+    }
+
+    public final SftpClient getClient() {
+        return client;
+    }
+
+    public void doInteractive(BufferedReader stdin, PrintStream stdout, PrintStream stderr) throws Exception {
+        SftpClient sftp = getClient();
+        setCurrentRemoteDirectory(sftp.canonicalPath("."));
+        while (true) {
+            stdout.append(getCurrentRemoteDirectory()).append(" > ").flush();
+            String line = stdin.readLine();
+            if (line == null) { // EOF
+                break;
+            }
+
+            line = GenericUtils.replaceWhitespaceAndTrim(line);
+            if (GenericUtils.isEmpty(line)) {
+                continue;
+            }
+
+            String cmd;
+            String args;
+            int pos = line.indexOf(' ');
+            if (pos > 0) {
+                cmd = line.substring(0, pos);
+                args = line.substring(pos + 1).trim();
+            } else {
+                cmd = line;
+                args = "";
+            }
+
+            SftpCommandExecutor exec = commandsMap.get(cmd);
+            try {
+                if (exec == null) {
+                    stderr.append("Unknown command: ").println(line);
+                } else {
+                    try {
+                        if (exec.executeCommand(args, stdin, stdout, stderr)) {
+                            break;
+                        }
+                    } catch (Exception e) {
+                        stderr.append(e.getClass().getSimpleName()).append(": ").println(e.getMessage());
+                    } finally {
+                        stdout.flush();
+                    }
+                }
+            } finally {
+                stderr.flush(); // just makings sure
+            }
+        }
+    }
+
+    protected String resolveLocalPath(String pathArg) {
+        String cwd = getCurrentLocalDirectory();
+        if (GenericUtils.isEmpty(pathArg)) {
+            return cwd;
+        }
+
+        if (OsUtils.isWin32()) {
+            if ((pathArg.length() >= 2) && (pathArg.charAt(1) == ':')) {
+                return pathArg;
+            }
+        } else {
+            if (pathArg.charAt(0) == '/') {
+                return pathArg;
+            }
+        }
+
+        return cwd + File.separator + pathArg.replace('/', File.separatorChar);
+    }
+
+    protected String resolveRemotePath(String pathArg) {
+        String cwd = getCurrentRemoteDirectory();
+        if (GenericUtils.isEmpty(pathArg)) {
+            return cwd;
+        }
+
+        if (pathArg.charAt(0) == '/') {
+            return pathArg;
+        } else {
+            return cwd + "/" + pathArg;
+        }
+    }
+
+    protected <A extends Appendable> A appendFileAttributes(A stdout, SftpClient sftp, String path, Attributes attrs) throws IOException {
+        stdout.append('\t').append(Long.toString(attrs.getSize()))
+              .append('\t').append(SftpFileSystemProvider.getRWXPermissions(attrs.getPermissions()));
+        if (attrs.isSymbolicLink()) {
+            String linkValue = sftp.readLink(path);
+            stdout.append(" => ")
+                  .append('(').append(attrs.isDirectory() ? "dir" : "file").append(')')
+                  .append(' ').append(linkValue);
+        }
+
+        return stdout;
+    }
+
+    public String getCurrentRemoteDirectory() {
+        return cwdRemote;
+    }
+
+    public void setCurrentRemoteDirectory(String path) {
+        cwdRemote = path;
+    }
+
+    public String getCurrentLocalDirectory() {
+        return cwdLocal;
+    }
+
+    public void setCurrentLocalDirectory(String path) {
+        cwdLocal = path;
+    }
+
+    @Override
+    public boolean isOpen() {
+        return client.isOpen();
+    }
+
+    @Override
+    public void close() throws IOException {
+        if (isOpen()) {
+            client.close();
+        }
+    }
+
+    //////////////////////////////////////////////////////////////////////////
+
+    public static <A extends Appendable> A appendInfoValue(A sb, CharSequence name, Object value) throws IOException {
+        sb.append('\t').append(name).append(": ").append(Objects.toString(value));
+        return sb;
+    }
+
+    public static void main(String[] args) throws Exception {
+        PrintStream stdout = System.out;
+        PrintStream stderr = System.err;
+        OutputStream logStream = stderr;
+        try (BufferedReader stdin = new BufferedReader(new InputStreamReader(new NoCloseInputStream(System.in)))) {
+            Level level = resolveLoggingVerbosity(args);
+            logStream = resolveLoggingTargetStream(stdout, stderr, args);
+            if (logStream != null) {
+                setupLogging(level, stdout, stderr, logStream);
+            }
+
+            ClientSession session = (logStream == null) ? null : setupClientSession(SFTP_PORT_OPTION, stdin, stdout, stderr, args);
+            if (session == null) {
+                System.err.println("usage: sftp [-v[v][v]] [-E logoutput] [-i identity]"
+                        + " [-l login] [" + SFTP_PORT_OPTION + " port] [-o option=value]"
+                        + " [-w password] [-c cipherlist]  [-m maclist] [-C] hostname/user@host");
+                System.exit(-1);
+                return;
+            }
+
+            try {
+                // TODO allow command-line specification of SftpClientFactory
+                SftpClientFactory clientFactory = SftpClientFactory.instance();
+                try (SftpClient sftpClient = clientFactory.createSftpClient(session);
+                     SftpCommandMain sftp = new SftpCommandMain(sftpClient)) {
+                    // TODO allow injection of extra CommandExecutor(s) via command line and/or service loading
+                    sftp.doInteractive(stdin, stdout, stderr);
+                }
+            } finally {
+                session.close();
+            }
+        } finally {
+            if ((logStream != stdout) && (logStream != stderr)) {
+                logStream.close();
+            }
+        }
+    }
+
+    private static class ExitCommandExecutor implements SftpCommandExecutor {
+        ExitCommandExecutor() {
+            super();
+        }
+
+        @Override
+        public String getName() {
+            return "exit";
+        }
+
+        @Override
+        public boolean executeCommand(String args, BufferedReader stdin, PrintStream stdout, PrintStream stderr) throws Exception {
+            ValidateUtils.checkTrue(GenericUtils.isEmpty(args), "Unexpected arguments: %s", args);
+            stdout.println("Exiting");
+            return true;
+        }
+    }
+
+    private class PwdCommandExecutor implements SftpCommandExecutor {
+        protected PwdCommandExecutor() {
+            super();
+        }
+
+        @Override
+        public String getName() {
+            return "pwd";
+        }
+
+        @Override
+        public boolean executeCommand(String args, BufferedReader stdin, PrintStream stdout, PrintStream stderr) throws Exception {
+            ValidateUtils.checkTrue(GenericUtils.isEmpty(args), "Unexpected arguments: %s", args);
+            stdout.append('\t').append("Remote: ").println(getCurrentRemoteDirectory());
+            stdout.append('\t').append("Local: ").println(getCurrentLocalDirectory());
+            return false;
+        }
+    }
+
+    private class SessionCommandExecutor implements SftpCommandExecutor {
+        SessionCommandExecutor() {
+            super();
+        }
+
+        @Override
+        public String getName() {
+            return "session";
+        }
+
+        @Override
+        public boolean executeCommand(String args, BufferedReader stdin, PrintStream stdout, PrintStream stderr) throws Exception {
+            ValidateUtils.checkTrue(GenericUtils.isEmpty(args), "Unexpected arguments: %s", args);
+            SftpClient sftp = getClient();
+            ClientSession session = sftp.getSession();
+            appendInfoValue(stdout, "Session ID", BufferUtils.toHex(session.getSessionId())).println();
+            appendInfoValue(stdout, "Connect address", session.getConnectAddress()).println();
+
+            IoSession ioSession = session.getIoSession();
+            appendInfoValue(stdout, "Local address", ioSession.getLocalAddress()).println();
+            appendInfoValue(stdout, "Remote address", ioSession.getRemoteAddress()).println();
+
+            for (KexProposalOption option : KexProposalOption.VALUES) {
+                appendInfoValue(stdout, option.getDescription(), session.getNegotiatedKexParameter(option)).println();
+            }
+
+            return false;
+        }
+    }
+
+    private class InfoCommandExecutor implements SftpCommandExecutor {
+        InfoCommandExecutor() {
+            super();
+        }
+
+        @Override
+        public String getName() {
+            return "info";
+        }
+
+        @Override
+        public boolean executeCommand(String args, BufferedReader stdin, PrintStream stdout, PrintStream stderr) throws Exception {
+            ValidateUtils.checkTrue(GenericUtils.isEmpty(args), "Unexpected arguments: %s", args);
+            SftpClient sftp = getClient();
+            Session session = sftp.getSession();
+            stdout.append('\t').println(session.getServerVersion());
+
+            Map<String, byte[]> extensions = sftp.getServerExtensions();
+            Map<String, ?> parsed = ParserUtils.parse(extensions);
+            if (GenericUtils.size(extensions) > 0) {
+                stdout.println();
+            }
+
+            extensions.forEach((name, value) -> {
+                Object info = parsed.get(name);
+
+                stdout.append('\t').append(name).append(": ");
+                if (info == null) {
+                    stdout.println(BufferUtils.toHex(value));
+                } else {
+                    stdout.println(info);
+                }
+            });
+
+            return false;
+        }
+    }
+
+    private class VersionCommandExecutor implements SftpCommandExecutor {
+        VersionCommandExecutor() {
+            super();
+        }
+
+        @Override
+        public String getName() {
+            return "version";
+        }
+
+        @Override
+        public boolean executeCommand(String args, BufferedReader stdin, PrintStream stdout, PrintStream stderr) throws Exception {
+            ValidateUtils.checkTrue(GenericUtils.isEmpty(args), "Unexpected arguments: %s", args);
+            SftpClient sftp = getClient();
+            stdout.append('\t').println(sftp.getVersion());
+            return false;
+        }
+    }
+
+    private class CdCommandExecutor extends PwdCommandExecutor {
+        CdCommandExecutor() {
+            super();
+        }
+
+        @Override
+        public String getName() {
+            return "cd";
+        }
+
+        @Override
+        public boolean executeCommand(String args, BufferedReader stdin, PrintStream stdout, PrintStream stderr) throws Exception {
+            ValidateUtils.checkNotNullAndNotEmpty(args, "No remote directory specified");
+
+            String newPath = resolveRemotePath(args);
+            SftpClient sftp = getClient();
+            setCurrentRemoteDirectory(sftp.canonicalPath(newPath));
+            return super.executeCommand("", stdin, stdout, stderr);
+        }
+    }
+
+    private class LcdCommandExecutor extends PwdCommandExecutor {
+        LcdCommandExecutor() {
+            super();
+        }
+
+        @Override
+        public String getName() {
+            return "lcd";
+        }
+
+        @Override
+        public boolean executeCommand(String args, BufferedReader stdin, PrintStream stdout, PrintStream stderr) throws Exception {
+            if (GenericUtils.isEmpty(args)) {
+                setCurrentLocalDirectory(System.getProperty("user.home"));
+            } else {
+                Path path = Paths.get(resolveLocalPath(args)).normalize().toAbsolutePath();
+                ValidateUtils.checkTrue(Files.exists(path), "No such local directory: %s", path);
+                ValidateUtils.checkTrue(Files.isDirectory(path), "Path is not a directory: %s", path);
+                setCurrentLocalDirectory(path.toString());
+            }
+
+            return super.executeCommand("", stdin, stdout, stderr);
+        }
+    }
+
+    private class MkdirCommandExecutor implements SftpCommandExecutor {
+        MkdirCommandExecutor() {
+            super();
+        }
+
+        @Override
+        public String getName() {
+            return "mkdir";
+        }
+
+        @Override
+        public boolean executeCommand(String args, BufferedReader stdin, PrintStream stdout, PrintStream stderr) throws Exception {
+            ValidateUtils.checkNotNullAndNotEmpty(args, "No remote directory specified");
+
+            String path = resolveRemotePath(args);
+            SftpClient sftp = getClient();
+            sftp.mkdir(path);
+            return false;
+        }
+    }
+
+    private class LsCommandExecutor implements SftpCommandExecutor {
+        LsCommandExecutor() {
+            super();
+        }
+
+        @Override
+        public String getName() {
+            return "ls";
+        }
+
+        @Override
+        public boolean executeCommand(String args, BufferedReader stdin, PrintStream stdout, PrintStream stderr) throws Exception {
+            String[] comps = GenericUtils.split(args, ' ');
+            int numComps = GenericUtils.length(comps);
+            String pathArg = (numComps <= 0) ? null : GenericUtils.trimToEmpty(comps[numComps - 1]);
+            String flags = (numComps >= 2) ? GenericUtils.trimToEmpty(comps[0]) : null;
+            // ignore all flags
+            if ((GenericUtils.length(pathArg) > 0) && (pathArg.charAt(0) == '-')) {
+                flags = pathArg;
+                pathArg = null;
+            }
+
+            String path = resolveRemotePath(pathArg);
+            SftpClient sftp = getClient();
+            int version = sftp.getVersion();
+            boolean showLongName = (version == SftpConstants.SFTP_V3) && (GenericUtils.length(flags) > 1) && (flags.indexOf('l') > 0);
+            for (SftpClient.DirEntry entry : sftp.readDir(path)) {
+                String fileName = entry.getFilename();
+                SftpClient.Attributes attrs = entry.getAttributes();
+                appendFileAttributes(stdout.append('\t').append(fileName), sftp, path + "/" + fileName, attrs).println();
+                if (showLongName) {
+                    stdout.append("\t\tlong-name: ").println(entry.getLongFilename());
+                }
+            }
+
+            return false;
+        }
+    }
+
+    private class RmCommandExecutor implements SftpCommandExecutor {
+        RmCommandExecutor() {
+            super();
+        }
+
+        @Override
+        public String getName() {
+            return "rm";
+        }
+
+        @Override
+        public boolean executeCommand(String args, BufferedReader stdin, PrintStream stdout, PrintStream stderr) throws Exception {
+            String[] comps = GenericUtils.split(args, ' ');
+            int numArgs = GenericUtils.length(comps);
+            ValidateUtils.checkTrue(numArgs >= 1, "No arguments");
+            ValidateUtils.checkTrue(numArgs <= 2, "Too many arguments: %s", args);
+
+            String remotePath = comps[0];
+            boolean recursive = false;
+            boolean verbose = false;
+            if (remotePath.charAt(0) == '-') {
+                ValidateUtils.checkTrue(remotePath.length() > 1, "Missing flags specification: %s", args);
+                ValidateUtils.checkTrue(numArgs == 2, "Missing remote directory: %s", args);
+
+                for (int index = 1; index < remotePath.length(); index++) {
+                    char ch = remotePath.charAt(index);
+                    switch(ch) {
+                        case 'r' :
+                            recursive = true;
+                            break;
+                        case 'v':
+                            verbose = true;
+                            break;
+                        default:
+                            throw new IllegalArgumentException("Unknown flag (" + String.valueOf(ch) + ")");
+                    }
+                }
+                remotePath = comps[1];
+            }
+
+            String path = resolveRemotePath(remotePath);
+            SftpClient sftp = getClient();
+            if (recursive) {
+                Attributes attrs = sftp.stat(path);
+                ValidateUtils.checkTrue(attrs.isDirectory(), "Remote path not a directory: %s", args);
+                removeRecursive(sftp, path, attrs, stdout, verbose);
+            } else {
+                sftp.remove(path);
+                if (verbose) {
+                    stdout.append('\t').append("Removed ").println(path);
+                }
+            }
+
+            return false;
+        }
+
+        private void removeRecursive(SftpClient sftp, String path, Attributes attrs, PrintStream stdout, boolean verbose) throws IOException {
+            if (attrs.isDirectory()) {
+                for (DirEntry entry : sftp.readDir(path)) {
+                    String name = entry.getFilename();
+                    if (".".equals(name) || "..".equals(name)) {
+                        continue;
+                    }
+
+                    removeRecursive(sftp, path + "/" + name, entry.getAttributes(), stdout, verbose);
+                }
+
+                sftp.rmdir(path);
+            } else if (attrs.isRegularFile()) {
+                sftp.remove(path);
+            } else {
+                if (verbose) {
+                    stdout.append('\t').append("Skip special file ").println(path);
+                    return;
+                }
+            }
+
+            if (verbose) {
+                stdout.append('\t').append("Removed ").println(path);
+            }
+        }
+    }
+
+    private class RmdirCommandExecutor implements SftpCommandExecutor {
+        RmdirCommandExecutor() {
+            super();
+        }
+
+        @Override
+        public String getName() {
+            return "rmdir";
+        }
+
+        @Override
+        public boolean executeCommand(String args, BufferedReader stdin, PrintStream stdout, PrintStream stderr) throws Exception {
+            ValidateUtils.checkNotNullAndNotEmpty(args, "No remote directory specified");
+
+            String path = resolveRemotePath(args);
+            SftpClient sftp = getClient();
+            sftp.rmdir(path);
+            return false;
+        }
+    }
+
+    private class RenameCommandExecutor implements SftpCommandExecutor {
+        RenameCommandExecutor() {
+            super();
+        }
+
+        @Override
+        public String getName() {
+            return "rename";
+        }
+
+        @Override
+        public boolean executeCommand(String args, BufferedReader stdin, PrintStream stdout, PrintStream stderr) throws Exception {
+            String[] comps = GenericUtils.split(args, ' ');
+            ValidateUtils.checkTrue(GenericUtils.length(comps) == 2, "Invalid number of arguments: %s", args);
+
+            String oldPath = resolveRemotePath(GenericUtils.trimToEmpty(comps[0]));
+            String newPath = resolveRemotePath(GenericUtils.trimToEmpty(comps[1]));
+            SftpClient sftp = getClient();
+            sftp.rename(oldPath, newPath);
+            return false;
+        }
+    }
+
+    private class StatVfsCommandExecutor implements SftpCommandExecutor {
+        StatVfsCommandExecutor() {
+            super();
+        }
+
+        @Override
+        public String getName() {
+            return StatVfsExtensionParser.NAME;
+        }
+
+        @Override
+        public boolean executeCommand(String args, BufferedReader stdin, PrintStream stdout, PrintStream stderr) throws Exception {
+            String[] comps = GenericUtils.split(args, ' ');
+            int numArgs = GenericUtils.length(comps);
+            ValidateUtils.checkTrue(numArgs <= 1, "Invalid number of arguments: %s", args);
+
+            SftpClient sftp = getClient();
+            OpenSSHStatPathExtension ext = sftp.getExtension(OpenSSHStatPathExtension.class);
+            ValidateUtils.checkTrue(ext.isSupported(), "Extension not supported by server: %s", ext.getName());
+
+            String remPath = resolveRemotePath((numArgs >= 1) ? GenericUtils.trimToEmpty(comps[0]) :  GenericUtils.trimToEmpty(args));
+            OpenSSHStatExtensionInfo info = ext.stat(remPath);
+            Field[] fields = info.getClass().getFields();
+            for (Field f : fields) {
+                String name = f.getName();
+                int mod = f.getModifiers();
+                if (Modifier.isStatic(mod)) {
+                    continue;
+                }
+
+                Object value = f.get(info);
+                stdout.append('\t').append(name).append(": ").println(value);
+            }
+
+            return false;
+        }
+    }
+
+    private class LStatCommandExecutor implements SftpCommandExecutor {
+        LStatCommandExecutor() {
+            super();
+        }
+
+        @Override
+        public String getName() {
+            return "lstat";
+        }
+
+        @Override
+        public boolean executeCommand(String args, BufferedReader stdin, PrintStream stdout, PrintStream stderr) throws Exception {
+            String[] comps = GenericUtils.split(args, ' ');
+            ValidateUtils.checkTrue(GenericUtils.length(comps) <= 1, "Invalid number of arguments: %s", args);
+
+            String path = GenericUtils.trimToEmpty(resolveRemotePath(args));
+            SftpClient client = getClient();
+            Attributes attrs = client.lstat(path);
+            appendFileAttributes(stdout, client, path, attrs).println();
+            return false;
+        }
+    }
+
+    private class ReadLinkCommandExecutor implements SftpCommandExecutor {
+        ReadLinkCommandExecutor() {
+            super();
+        }
+
+        @Override
+        public String getName() {
+            return "readlink";
+        }
+
+        @Override
+        public boolean executeCommand(String args, BufferedReader stdin, PrintStream stdout, PrintStream stderr) throws Exception {
+            String[] comps = GenericUtils.split(args, ' ');
+            ValidateUtils.checkTrue(GenericUtils.length(comps) <= 1, "Invalid number of arguments: %s", args);
+
+            String path = GenericUtils.trimToEmpty(resolveRemotePath(args));
+            SftpClient client = getClient();
+            String linkData = client.readLink(path);
+            stdout.append('\t').println(linkData);
+            return false;
+        }
+    }
+
+    private class HelpCommandExecutor implements SftpCommandExecutor {
+        HelpCommandExecutor() {
+            super();
+        }
+
+        @Override
+        public String getName() {
+            return "help";
+        }
+
+        @Override
+        @SuppressWarnings("synthetic-access")
+        public boolean executeCommand(String args, BufferedReader stdin, PrintStream stdout, PrintStream stderr) throws Exception {
+            ValidateUtils.checkTrue(GenericUtils.isEmpty(args), "Unexpected arguments: %s", args);
+            for (String cmd : commandsMap.keySet()) {
+                stdout.append('\t').println(cmd);
+            }
+            return false;
+        }
+    }
+
+    private abstract class TransferCommandExecutor implements SftpCommandExecutor {
+        protected TransferCommandExecutor() {
+            super();
+        }
+
+        protected void createDirectories(SftpClient sftp, String remotePath) throws IOException {
+            try {
+                Attributes attrs = sftp.stat(remotePath);
+                ValidateUtils.checkTrue(attrs.isDirectory(), "Remote path already exists but is not a directory: %s", remotePath);
+                return;
+            } catch (SftpException e) {
+                int status = e.getStatus();
+                ValidateUtils.checkTrue(status == SftpConstants.SSH_FX_NO_SUCH_FILE, "Failed to get status of %s: %s", remotePath, e.getMessage());
+            }
+
+            int pos = remotePath.lastIndexOf('/');
+            ValidateUtils.checkTrue(pos > 0, "No more parents for %s", remotePath);
+            createDirectories(sftp, remotePath.substring(0, pos));
+        }
+
+        protected void transferFile(SftpClient sftp, Path localPath, String remotePath, boolean upload, PrintStream stdout, boolean verbose) throws IOException {
+            // Create the file's hierarchy
+            if (upload) {
+                int pos = remotePath.lastIndexOf('/');
+                ValidateUtils.checkTrue(pos > 0, "Missing full remote file path: %s", remotePath);
+                createDirectories(sftp, remotePath.substring(0, pos));
+            } else {
+                Files.createDirectories(localPath.getParent());
+            }
+
+            try (InputStream input = upload ? Files.newInputStream(localPath) : sftp.read(remotePath);
+                 OutputStream output = upload ? sftp.write(remotePath) : Files.newOutputStream(localPath)) {
+                IoUtils.copy(input, output, SftpClient.IO_BUFFER_SIZE);
+            }
+
+            if (verbose) {
+                stdout.append('\t')
+                      .append("Copied ").append(upload ? localPath.toString() : remotePath)
+                      .append(" to ").println(upload ? remotePath : localPath.toString());
+            }
+        }
+
+        protected void transferRemoteDir(SftpClient sftp, Path localPath, String remotePath, Attributes attrs, PrintStream stdout, boolean verbose) throws IOException {
+            if (attrs.isDirectory()) {
+                for (DirEntry entry : sftp.readDir(remotePath)) {
+                    String name = entry.getFilename();
+                    if (".".equals(name) || "..".equals(name)) {
+                        continue;
+                    }
+
+                    transferRemoteDir(sftp, localPath.resolve(name), remotePath + "/" + name, entry.getAttributes(), stdout, verbose);
+                }
+            } else if (attrs.isRegularFile()) {
+                transferFile(sftp, localPath, remotePath, false, stdout, verbose);
+            } else {
+                if (verbose) {
+                    stdout.append('\t').append("Skip remote special file ").println(remotePath);
+                }
+            }
+        }
+
+        protected void transferLocalDir(SftpClient sftp, Path localPath, String remotePath, PrintStream stdout, boolean verbose) throws IOException {
+            if (Files.isDirectory(localPath)) {
+                try (DirectoryStream<Path> ds = Files.newDirectoryStream(localPath)) {
+                    for (Path entry : ds) {
+                        String name = entry.getFileName().toString();
+                        transferLocalDir(sftp, localPath.resolve(name), remotePath + "/" + name, stdout, verbose);
+                    }
+                }
+            } else if (Files.isRegularFile(localPath)) {
+                transferFile(sftp, localPath, remotePath, true, stdout, verbose);
+            } else {
+                if (verbose) {
+                    stdout.append('\t').append("Skip local special file ").println(localPath);
+                }
+            }
+        }
+
+        protected void executeCommand(String args, boolean upload, PrintStream stdout) throws IOException {
+            String[] comps = GenericUtils.split(args, ' ');
+            int numArgs = GenericUtils.length(comps);
+            ValidateUtils.checkTrue((numArgs >= 1) && (numArgs <= 3), "Invalid number of arguments: %s", args);
+
+            String src = comps[0];
+            boolean recursive = false;
+            boolean verbose = false;
+            int tgtIndex = 1;
+            if (src.charAt(0) == '-') {
+                ValidateUtils.checkTrue(src.length() > 1, "Missing flags specification: %s", args);
+                ValidateUtils.checkTrue(numArgs >= 2, "Missing source specification: %s", args);
+
+                for (int index = 1; index < src.length(); index++) {
+                    char ch = src.charAt(index);
+                    switch(ch) {
+                        case 'r' :
+                            recursive = true;
+                            break;
+                        case 'v':
+                            verbose = true;
+                            break;
+                        default:
+                            throw new IllegalArgumentException("Unknown flag (" + String.valueOf(ch) + ")");
+                    }
+                }
+                src = comps[1];
+                tgtIndex++;
+            }
+
+            String tgt = (tgtIndex < numArgs) ? comps[tgtIndex] : null;
+            String localPath;
+            String remotePath;
+            if (upload) {
+                localPath = src;
+                remotePath = ValidateUtils.checkNotNullAndNotEmpty(tgt, "No remote target specified: %s", args);
+            } else {
+                localPath = GenericUtils.isEmpty(tgt) ? getCurrentLocalDirectory() : tgt;
+                remotePath = src;
+            }
+
+            SftpClient sftp = getClient();
+            Path local = Paths.get(resolveLocalPath(localPath)).normalize().toAbsolutePath();
+            String remote = resolveRemotePath(remotePath);
+            if (recursive) {
+                if (upload) {
+                    ValidateUtils.checkTrue(Files.isDirectory(local), "Local path not a directory or does not exist: %s", local);
+                    transferLocalDir(sftp, local, remote, stdout, verbose);
+                } else {
+                    Attributes attrs = sftp.stat(remote);
+                    ValidateUtils.checkTrue(attrs.isDirectory(), "Remote path not a directory: %s", remote);
+                    transferRemoteDir(sftp, local, remote, attrs, stdout, verbose);
+                }
+            } else {
+                if (Files.exists(local) && Files.isDirectory(local)) {
+                    int pos = remote.lastIndexOf('/');
+                    String name = (pos >= 0) ? remote.substring(pos + 1) : remote;
+                    local = local.resolve(name);
+                }
+
+                transferFile(sftp, local, remote, upload, stdout, verbose);
+            }
+        }
+    }
+
+    private class GetCommandExecutor extends TransferCommandExecutor {
+        GetCommandExecutor() {
+            super();
+        }
+
+        @Override
+        public String getName() {
+            return "get";
+        }
+
+        @Override
+        public boolean executeCommand(String args, BufferedReader stdin, PrintStream stdout, PrintStream stderr) throws Exception {
+            executeCommand(args, false, stdout);
+            return false;
+        }
+    }
+
+    private class PutCommandExecutor extends TransferCommandExecutor {
+        PutCommandExecutor() {
+            super();
+        }
+
+        @Override
+        public String getName() {
+            return "put";
+        }
+
+        @Override
+        public boolean executeCommand(String args, BufferedReader stdin, PrintStream stdout, PrintStream stderr) throws Exception {
+            executeCommand(args, true, stdout);
+            return false;
+        }
+    }
+}