You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@karaf.apache.org by gn...@apache.org on 2012/04/10 17:26:20 UTC

svn commit: r1311786 - in /karaf/branches/karaf-2.3.x: assemblies/apache-karaf/src/main/distribution/text/etc/ shell/console/ shell/console/src/main/java/org/apache/felix/gogo/commands/ shell/console/src/main/java/org/apache/felix/gogo/commands/basic/ ...

Author: gnodet
Date: Tue Apr 10 15:26:18 2012
New Revision: 1311786

URL: http://svn.apache.org/viewvc?rev=1311786&view=rev
Log:
[KARAF-1045] Improve help system for subshells or other pages

Conflicts:

	assemblies/apache-karaf/src/main/distribution/text/etc/shell.init.script
	util/pom.xml

Added:
    karaf/branches/karaf-2.3.x/shell/console/src/main/java/org/apache/felix/gogo/commands/SubShell.java
    karaf/branches/karaf-2.3.x/shell/console/src/main/java/org/apache/karaf/shell/console/HelpProvider.java
    karaf/branches/karaf-2.3.x/shell/console/src/main/java/org/apache/karaf/shell/console/SubShell.java
    karaf/branches/karaf-2.3.x/shell/console/src/main/java/org/apache/karaf/shell/console/commands/AnnotatedSubShell.java
    karaf/branches/karaf-2.3.x/shell/console/src/main/java/org/apache/karaf/shell/console/commands/BasicSubShell.java
    karaf/branches/karaf-2.3.x/shell/console/src/main/java/org/apache/karaf/shell/console/completer/CommandNamesCompleter.java
    karaf/branches/karaf-2.3.x/shell/console/src/main/java/org/apache/karaf/shell/console/help/
    karaf/branches/karaf-2.3.x/shell/console/src/main/java/org/apache/karaf/shell/console/help/CommandListHelpProvider.java
    karaf/branches/karaf-2.3.x/shell/console/src/main/java/org/apache/karaf/shell/console/help/HelpAction.java
    karaf/branches/karaf-2.3.x/shell/console/src/main/java/org/apache/karaf/shell/console/help/HelpSystem.java
    karaf/branches/karaf-2.3.x/shell/console/src/main/java/org/apache/karaf/shell/console/help/SimpleHelpProvider.java
    karaf/branches/karaf-2.3.x/shell/console/src/main/java/org/apache/karaf/shell/console/help/SingleCommandHelpProvider.java
    karaf/branches/karaf-2.3.x/shell/console/src/main/java/org/apache/karaf/shell/console/help/SubShellHelpProvider.java
    karaf/branches/karaf-2.3.x/shell/console/src/main/java/org/apache/karaf/shell/console/util/
    karaf/branches/karaf-2.3.x/shell/console/src/main/java/org/apache/karaf/shell/console/util/Branding.java
    karaf/branches/karaf-2.3.x/shell/console/src/test/java/org/apache/karaf/shell/console/help/
    karaf/branches/karaf-2.3.x/shell/console/src/test/java/org/apache/karaf/shell/console/help/TestFormatting.java
    karaf/branches/karaf-2.3.x/shell/osgi/src/main/resources/org/
    karaf/branches/karaf-2.3.x/shell/osgi/src/main/resources/org/apache/
    karaf/branches/karaf-2.3.x/shell/osgi/src/main/resources/org/apache/karaf/
    karaf/branches/karaf-2.3.x/shell/osgi/src/main/resources/org/apache/karaf/shell/
    karaf/branches/karaf-2.3.x/shell/osgi/src/main/resources/org/apache/karaf/shell/osgi/
    karaf/branches/karaf-2.3.x/shell/osgi/src/main/resources/org/apache/karaf/shell/osgi/osgi.txt
    karaf/branches/karaf-2.3.x/util/src/main/java/org/apache/karaf/util/InterpolationHelper.java
Removed:
    karaf/branches/karaf-2.3.x/shell/console/src/main/java/org/apache/karaf/shell/console/HelpAction.java
Modified:
    karaf/branches/karaf-2.3.x/assemblies/apache-karaf/src/main/distribution/text/etc/shell.init.script
    karaf/branches/karaf-2.3.x/shell/console/pom.xml
    karaf/branches/karaf-2.3.x/shell/console/src/main/java/org/apache/felix/gogo/commands/basic/DefaultActionPreparator.java
    karaf/branches/karaf-2.3.x/shell/console/src/main/java/org/apache/karaf/shell/console/jline/Console.java
    karaf/branches/karaf-2.3.x/shell/console/src/main/resources/META-INF/services/org/apache/karaf/shell/commands
    karaf/branches/karaf-2.3.x/shell/console/src/main/resources/OSGI-INF/blueprint/karaf-console.xml
    karaf/branches/karaf-2.3.x/shell/osgi/src/main/resources/OSGI-INF/blueprint/shell-osgi.xml
    karaf/branches/karaf-2.3.x/util/pom.xml

Modified: karaf/branches/karaf-2.3.x/assemblies/apache-karaf/src/main/distribution/text/etc/shell.init.script
URL: http://svn.apache.org/viewvc/karaf/branches/karaf-2.3.x/assemblies/apache-karaf/src/main/distribution/text/etc/shell.init.script?rev=1311786&r1=1311785&r2=1311786&view=diff
==============================================================================
--- karaf/branches/karaf-2.3.x/assemblies/apache-karaf/src/main/distribution/text/etc/shell.init.script (original)
+++ karaf/branches/karaf-2.3.x/assemblies/apache-karaf/src/main/distribution/text/etc/shell.init.script Tue Apr 10 15:26:18 2012
@@ -24,6 +24,7 @@ ld = { log:display $args } ;
 lde = { log:display-exception $args } ;
 la = { osgi:list -t 0 $args } ;
 cl = { config:list "(service.pid=$args)" } ;
+help = { *:help $args | more } ;
 man = { help $args } ;
 
 // system:* aliases
@@ -62,4 +63,4 @@ feature:list = { features:list $args } ;
 feature:uninstall = { features:uninstall $args } ;
 feature:list-repository = { features:listrepositories $args } ;
 feature:list-url = { features:listurl $args } ;
-feature:list-version = { features:listversions $args } ;
\ No newline at end of file
+feature:list-version = { features:listversions $args } ;

Modified: karaf/branches/karaf-2.3.x/shell/console/pom.xml
URL: http://svn.apache.org/viewvc/karaf/branches/karaf-2.3.x/shell/console/pom.xml?rev=1311786&r1=1311785&r2=1311786&view=diff
==============================================================================
--- karaf/branches/karaf-2.3.x/shell/console/pom.xml (original)
+++ karaf/branches/karaf-2.3.x/shell/console/pom.xml Tue Apr 10 15:26:18 2012
@@ -64,6 +64,10 @@
             <artifactId>org.apache.karaf.jaas.modules</artifactId>
         </dependency>
         <dependency>
+            <groupId>org.apache.karaf</groupId>
+            <artifactId>org.apache.karaf.util</artifactId>
+        </dependency>
+        <dependency>
             <groupId>org.apache.aries.blueprint</groupId>
             <artifactId>org.apache.aries.blueprint</artifactId>
         </dependency>
@@ -111,6 +115,7 @@
                     <instructions>
                         <Import-Package>
                             !org.apache.karaf.shell.console*,
+                            !org.apache.karaf.util*,
                             !org.apache.felix.gogo.commands*,
                             !org.fusesource.jansi*,
                             !javax.swing,
@@ -132,6 +137,7 @@
                         <Private-Package>
                             org.fusesource.jansi.internal,
                             org.apache.felix.gogo.runtime*,
+                            org.apache.karaf.util*,
                             META-INF.native.*
                         </Private-Package>
                         <Bundle-NativeCode>

Added: karaf/branches/karaf-2.3.x/shell/console/src/main/java/org/apache/felix/gogo/commands/SubShell.java
URL: http://svn.apache.org/viewvc/karaf/branches/karaf-2.3.x/shell/console/src/main/java/org/apache/felix/gogo/commands/SubShell.java?rev=1311786&view=auto
==============================================================================
--- karaf/branches/karaf-2.3.x/shell/console/src/main/java/org/apache/felix/gogo/commands/SubShell.java (added)
+++ karaf/branches/karaf-2.3.x/shell/console/src/main/java/org/apache/felix/gogo/commands/SubShell.java Tue Apr 10 15:26:18 2012
@@ -0,0 +1,45 @@
+/*
+ * 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.commands;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ElementType.TYPE})
+public @interface SubShell {
+
+    /**
+     * Returns the name of the command if used inside a shell
+     */
+    String name();
+
+    /**
+     * Returns the description of the command which is used to generate command line help
+     */
+    String description() default "";
+
+    /**
+     * Returns a detailed description of the command
+     */
+    String detailedDescription() default "";
+
+}

Modified: karaf/branches/karaf-2.3.x/shell/console/src/main/java/org/apache/felix/gogo/commands/basic/DefaultActionPreparator.java
URL: http://svn.apache.org/viewvc/karaf/branches/karaf-2.3.x/shell/console/src/main/java/org/apache/felix/gogo/commands/basic/DefaultActionPreparator.java?rev=1311786&r1=1311785&r2=1311786&view=diff
==============================================================================
--- karaf/branches/karaf-2.3.x/shell/console/src/main/java/org/apache/felix/gogo/commands/basic/DefaultActionPreparator.java (original)
+++ karaf/branches/karaf-2.3.x/shell/console/src/main/java/org/apache/felix/gogo/commands/basic/DefaultActionPreparator.java Tue Apr 10 15:26:18 2012
@@ -570,13 +570,33 @@ public class DefaultActionPreparator imp
 
     // TODO move this to a helper class?
     public static void printFormatted(String prefix, String str, int termWidth, PrintStream out) {
+        printFormatted(prefix, str, termWidth, out, true);
+    }
+
+    public static void printFormatted(String prefix, String str, int termWidth, PrintStream out, boolean prefixFirstLine) {
         int pfxLen = length(prefix);
         int maxwidth = termWidth - pfxLen;
         Pattern wrap = Pattern.compile("(\\S\\S{" + maxwidth + ",}|.{1," + maxwidth + "})(\\s+|$)");
-        Matcher m = wrap.matcher(str);
-        while (m.find()) {
-            out.print(prefix);
-            out.println(m.group());
+        int cur = 0;
+        while (cur >= 0) {
+            int lst = str.indexOf('\n', cur);
+            String s = (lst >= 0) ? str.substring(cur, lst) : str.substring(cur);
+            if (s.length() == 0) {
+                out.println();
+            } else {
+                Matcher m = wrap.matcher(s);
+                while (m.find()) {
+                    if (cur > 0 || prefixFirstLine) {
+                        out.print(prefix);
+                    }
+                    out.println(m.group());
+                }
+            }
+            if (lst >= 0) {
+                cur = lst + 1;
+            } else {
+                break;
+            }
         }
     }
 

Added: karaf/branches/karaf-2.3.x/shell/console/src/main/java/org/apache/karaf/shell/console/HelpProvider.java
URL: http://svn.apache.org/viewvc/karaf/branches/karaf-2.3.x/shell/console/src/main/java/org/apache/karaf/shell/console/HelpProvider.java?rev=1311786&view=auto
==============================================================================
--- karaf/branches/karaf-2.3.x/shell/console/src/main/java/org/apache/karaf/shell/console/HelpProvider.java (added)
+++ karaf/branches/karaf-2.3.x/shell/console/src/main/java/org/apache/karaf/shell/console/HelpProvider.java Tue Apr 10 15:26:18 2012
@@ -0,0 +1,26 @@
+/**
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.karaf.shell.console;
+
+import org.apache.felix.service.command.CommandSession;
+
+public interface HelpProvider {
+    
+    String getHelp(CommandSession session, String path);
+    
+}

Added: karaf/branches/karaf-2.3.x/shell/console/src/main/java/org/apache/karaf/shell/console/SubShell.java
URL: http://svn.apache.org/viewvc/karaf/branches/karaf-2.3.x/shell/console/src/main/java/org/apache/karaf/shell/console/SubShell.java?rev=1311786&view=auto
==============================================================================
--- karaf/branches/karaf-2.3.x/shell/console/src/main/java/org/apache/karaf/shell/console/SubShell.java (added)
+++ karaf/branches/karaf-2.3.x/shell/console/src/main/java/org/apache/karaf/shell/console/SubShell.java Tue Apr 10 15:26:18 2012
@@ -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.karaf.shell.console;
+
+public interface SubShell {
+
+    /**
+     * Returns the name of the command if used inside a shell
+     */
+    String getName();
+
+    /**
+     * Returns the description of the command which is used to generate command line help
+     */
+    String getDescription();
+
+    /**
+     * Returns a detailed description of the command
+     */
+    String getDetailedDescription();
+
+}

Added: karaf/branches/karaf-2.3.x/shell/console/src/main/java/org/apache/karaf/shell/console/commands/AnnotatedSubShell.java
URL: http://svn.apache.org/viewvc/karaf/branches/karaf-2.3.x/shell/console/src/main/java/org/apache/karaf/shell/console/commands/AnnotatedSubShell.java?rev=1311786&view=auto
==============================================================================
--- karaf/branches/karaf-2.3.x/shell/console/src/main/java/org/apache/karaf/shell/console/commands/AnnotatedSubShell.java (added)
+++ karaf/branches/karaf-2.3.x/shell/console/src/main/java/org/apache/karaf/shell/console/commands/AnnotatedSubShell.java Tue Apr 10 15:26:18 2012
@@ -0,0 +1,43 @@
+/**
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+package org.apache.karaf.shell.console.commands;
+
+import org.apache.karaf.shell.console.SubShell;
+
+public class AnnotatedSubShell implements SubShell {
+
+    public String getName() {
+        return getAnnotation().name();
+    }
+
+    public String getDescription() {
+        return getAnnotation().description();
+    }
+
+    public String getDetailedDescription() {
+        return getAnnotation().detailedDescription();
+    }
+    
+    org.apache.felix.gogo.commands.SubShell getAnnotation() {
+        org.apache.felix.gogo.commands.SubShell ann = getClass().getAnnotation(org.apache.felix.gogo.commands.SubShell.class);
+        if (ann == null) {
+            throw new IllegalStateException("The class should be annotated with the org.apache.felix.gogo.commands.SubShell annotation");
+        }
+        return ann;
+    }
+}

Added: karaf/branches/karaf-2.3.x/shell/console/src/main/java/org/apache/karaf/shell/console/commands/BasicSubShell.java
URL: http://svn.apache.org/viewvc/karaf/branches/karaf-2.3.x/shell/console/src/main/java/org/apache/karaf/shell/console/commands/BasicSubShell.java?rev=1311786&view=auto
==============================================================================
--- karaf/branches/karaf-2.3.x/shell/console/src/main/java/org/apache/karaf/shell/console/commands/BasicSubShell.java (added)
+++ karaf/branches/karaf-2.3.x/shell/console/src/main/java/org/apache/karaf/shell/console/commands/BasicSubShell.java Tue Apr 10 15:26:18 2012
@@ -0,0 +1,51 @@
+/**
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+package org.apache.karaf.shell.console.commands;
+
+import org.apache.karaf.shell.console.SubShell;
+
+public class BasicSubShell implements SubShell {
+    
+    private String name;
+    private String description;
+    private String detailedDescription;
+
+    public String getName() {
+        return name;
+    }
+
+    public void setName(String name) {
+        this.name = name;
+    }
+
+    public String getDescription() {
+        return description;
+    }
+
+    public void setDescription(String description) {
+        this.description = description;
+    }
+
+    public String getDetailedDescription() {
+        return detailedDescription;
+    }
+
+    public void setDetailedDescription(String detailedDescription) {
+        this.detailedDescription = detailedDescription;
+    }
+}

Added: karaf/branches/karaf-2.3.x/shell/console/src/main/java/org/apache/karaf/shell/console/completer/CommandNamesCompleter.java
URL: http://svn.apache.org/viewvc/karaf/branches/karaf-2.3.x/shell/console/src/main/java/org/apache/karaf/shell/console/completer/CommandNamesCompleter.java?rev=1311786&view=auto
==============================================================================
--- karaf/branches/karaf-2.3.x/shell/console/src/main/java/org/apache/karaf/shell/console/completer/CommandNamesCompleter.java (added)
+++ karaf/branches/karaf-2.3.x/shell/console/src/main/java/org/apache/karaf/shell/console/completer/CommandNamesCompleter.java Tue Apr 10 15:26:18 2012
@@ -0,0 +1,98 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.karaf.shell.console.completer;
+
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.CopyOnWriteArraySet;
+
+import org.apache.felix.gogo.runtime.CommandSessionImpl;
+import org.apache.felix.service.command.CommandProcessor;
+import org.apache.felix.service.command.CommandSession;
+import org.apache.karaf.shell.console.Completer;
+import org.apache.karaf.shell.console.jline.CommandSessionHolder;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.FrameworkUtil;
+import org.osgi.framework.ServiceEvent;
+import org.osgi.framework.ServiceListener;
+
+/**
+ * Completes command names
+ */
+public class CommandNamesCompleter implements Completer {
+
+    private CommandSession session;
+    private final Set<String> commands = new CopyOnWriteArraySet<String>();
+
+    public CommandNamesCompleter() {
+        this(CommandSessionHolder.getSession());
+    }
+
+    public CommandNamesCompleter(CommandSession session) {
+        this.session = session;
+
+        try {
+            new CommandTracker();
+        } catch (Throwable t) {
+            // Ignore in case we're not in OSGi
+        }
+    }
+
+
+    public int complete(String buffer, int cursor, List<String> candidates) {
+        if (session == null) {
+            session = CommandSessionHolder.getSession();
+        }
+        checkData();
+        int res = new StringsCompleter(commands).complete(buffer, cursor, candidates);
+        Collections.sort(candidates);
+        return res;
+    }
+
+    protected void checkData() {
+        if (commands.isEmpty()) {
+            Set<String> names = new HashSet<String>((Set<String>) session.get(CommandSessionImpl.COMMANDS));
+            for (String name : names) {
+                commands.add(name);
+                if (name.indexOf(':') > 0) {
+                    commands.add(name.substring(0, name.indexOf(':')));
+                }
+            }
+        }
+    }
+
+    private class CommandTracker {
+        public CommandTracker() throws Exception {
+            BundleContext context = FrameworkUtil.getBundle(getClass()).getBundleContext();
+            ServiceListener listener = new ServiceListener() {
+                public void serviceChanged(ServiceEvent event) {
+                    commands.clear();
+                }
+            };
+            context.addServiceListener(listener,
+                    String.format("(&(%s=*)(%s=*))",
+                            CommandProcessor.COMMAND_SCOPE,
+                            CommandProcessor.COMMAND_FUNCTION));
+        }
+    }
+
+}
+

Added: karaf/branches/karaf-2.3.x/shell/console/src/main/java/org/apache/karaf/shell/console/help/CommandListHelpProvider.java
URL: http://svn.apache.org/viewvc/karaf/branches/karaf-2.3.x/shell/console/src/main/java/org/apache/karaf/shell/console/help/CommandListHelpProvider.java?rev=1311786&view=auto
==============================================================================
--- karaf/branches/karaf-2.3.x/shell/console/src/main/java/org/apache/karaf/shell/console/help/CommandListHelpProvider.java (added)
+++ karaf/branches/karaf-2.3.x/shell/console/src/main/java/org/apache/karaf/shell/console/help/CommandListHelpProvider.java Tue Apr 10 15:26:18 2012
@@ -0,0 +1,152 @@
+/**
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.karaf.shell.console.help;
+
+import java.io.ByteArrayOutputStream;
+import java.io.PrintStream;
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+import java.util.Map;
+import java.util.Set;
+import java.util.SortedMap;
+import java.util.TreeMap;
+
+import jline.Terminal;
+import org.apache.felix.gogo.commands.Action;
+import org.apache.felix.gogo.commands.Command;
+import org.apache.felix.gogo.commands.basic.AbstractCommand;
+import org.apache.felix.gogo.commands.basic.DefaultActionPreparator;
+import org.apache.felix.gogo.runtime.CommandSessionImpl;
+import org.apache.felix.service.command.CommandSession;
+import org.apache.felix.service.command.Function;
+import org.apache.karaf.shell.console.HelpProvider;
+import org.apache.karaf.shell.console.NameScoping;
+import org.fusesource.jansi.Ansi;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.ServiceReference;
+
+public class CommandListHelpProvider implements HelpProvider {
+
+    public String getHelp(CommandSession session, String path) {
+        if (path.indexOf('|') > 0) {
+            if (path.startsWith("command-list|")) {
+                path = path.substring("command-list|".length());
+            } else {
+                return null;
+            }
+        }
+        SortedMap<String, String> commands = getCommandDescriptions(session, path);
+        ByteArrayOutputStream baos = new ByteArrayOutputStream();
+        printMethodList(session, new PrintStream(baos), commands);
+        return baos.toString();
+    }
+
+    private SortedMap<String, String> getCommandDescriptions(CommandSession session, String command) {
+        Set<String> names = (Set<String>) session.get(CommandSessionImpl.COMMANDS);
+        SortedMap<String,String> commands = new TreeMap<String,String>();
+        for (String name : names) {
+            if (command != null && !name.startsWith(command)) {
+                continue;
+            }
+            String description = null;
+            Function function = (Function) session.get(name);
+            function = unProxy(function);
+            if (function instanceof AbstractCommand) {
+                try {
+                    Method mth = AbstractCommand.class.getDeclaredMethod("createNewAction");
+                    mth.setAccessible(true);
+                    Action action = (Action) mth.invoke(function);
+                    Class<? extends Action> clazz = action.getClass();
+                    Command ann = clazz.getAnnotation(Command.class);
+                    description = ann.description();
+                } catch (Throwable e) {
+                }
+                if (name.startsWith("*:")) {
+                    name = name.substring(2);
+                }
+                commands.put(name, description);
+            }
+        }
+        return commands;
+    }
+
+    protected void printMethodList(CommandSession session, PrintStream out, SortedMap<String, String> commands) {
+        Terminal term = (Terminal) session.get(".jline.terminal");
+        out.println(Ansi.ansi().a(Ansi.Attribute.INTENSITY_BOLD).a("COMMANDS").a(Ansi.Attribute.RESET));
+        int max = 0;
+        for (Map.Entry<String,String> entry : commands.entrySet()) {
+            String key = NameScoping.getCommandNameWithoutGlobalPrefix(session, entry.getKey());
+            max = Math.max(max,key.length());
+        }
+        int margin = 4;
+        String prefix1 = "        ";
+        if (term != null && term.getWidth() - max - prefix1.length() - margin > 50) {
+            String prefix2 = prefix1;
+            for (int i = 0; i < max + margin; i++) {
+                prefix2 += " ";
+            }
+            for (Map.Entry<String,String> entry : commands.entrySet()) {
+                out.print(prefix1);
+                String key = NameScoping.getCommandNameWithoutGlobalPrefix(session, entry.getKey());
+                out.print(Ansi.ansi().a(Ansi.Attribute.INTENSITY_BOLD).a(key).a(Ansi.Attribute.RESET));
+                for (int i = 0; i < max - key.length() + margin; i++) {
+                    out.print(' ');
+                }
+                if (entry.getValue() != null) {
+                    DefaultActionPreparator.printFormatted(prefix2, entry.getValue(), term.getWidth(), out, false);
+                }
+            }
+        } else {
+            String prefix2 = prefix1 + prefix1;
+            for (Map.Entry<String,String> entry : commands.entrySet()) {
+                out.print(prefix1);
+                String key = NameScoping.getCommandNameWithoutGlobalPrefix(session, entry.getKey());
+                out.println(Ansi.ansi().a(Ansi.Attribute.INTENSITY_BOLD).a(key).a(Ansi.Attribute.RESET));
+                if (entry.getValue() != null) {
+                    DefaultActionPreparator.printFormatted(prefix2, entry.getValue(),
+                            term != null ? term.getWidth() : 80, out);
+                }
+            }
+        }
+        out.println();
+    }
+    
+    protected Function unProxy(Function function) {
+        try {
+            if (function.getClass().getName().contains("CommandProxy")) {
+                Field contextField = function.getClass().getDeclaredField("context");
+                Field referenceField = function.getClass().getDeclaredField("reference");
+                contextField.setAccessible(true);
+                referenceField.setAccessible(true);
+                BundleContext context = (BundleContext) contextField.get(function);
+                ServiceReference reference = (ServiceReference) referenceField.get(function);
+                Object target = context.getService(reference);
+                try {
+                    if (target instanceof Function) {
+                        function = (Function) target;
+                    }
+                } finally {
+                    context.ungetService(reference);
+                }
+            }
+        } catch (Throwable t) {
+        }
+        return function;
+    }
+
+}

Added: karaf/branches/karaf-2.3.x/shell/console/src/main/java/org/apache/karaf/shell/console/help/HelpAction.java
URL: http://svn.apache.org/viewvc/karaf/branches/karaf-2.3.x/shell/console/src/main/java/org/apache/karaf/shell/console/help/HelpAction.java?rev=1311786&view=auto
==============================================================================
--- karaf/branches/karaf-2.3.x/shell/console/src/main/java/org/apache/karaf/shell/console/help/HelpAction.java (added)
+++ karaf/branches/karaf-2.3.x/shell/console/src/main/java/org/apache/karaf/shell/console/help/HelpAction.java Tue Apr 10 15:26:18 2012
@@ -0,0 +1,192 @@
+/**
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.karaf.shell.console.help;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.PrintStream;
+import java.io.Reader;
+import java.io.StringWriter;
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+import java.net.URL;
+import java.util.Map;
+import java.util.Set;
+import java.util.SortedMap;
+import java.util.TreeMap;
+
+import jline.Terminal;
+import org.apache.felix.gogo.commands.Action;
+import org.apache.felix.gogo.commands.Argument;
+import org.apache.felix.gogo.commands.Command;
+import org.apache.felix.gogo.commands.basic.AbstractCommand;
+import org.apache.felix.gogo.commands.basic.DefaultActionPreparator;
+import org.apache.felix.service.command.Function;
+import org.apache.karaf.shell.console.AbstractAction;
+import org.apache.karaf.shell.console.NameScoping;
+import org.apache.karaf.shell.console.SubShell;
+import org.fusesource.jansi.Ansi;
+import org.osgi.framework.Bundle;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.ServiceReference;
+
+import static org.apache.felix.gogo.commands.basic.DefaultActionPreparator.printFormatted;
+
+/**
+ * Displays help on the available commands
+ */
+@Command(scope = "*", name = "help", description = "Displays this help or help about a command")
+public class HelpAction extends AbstractAction {
+
+    @Argument(name = "command", required = false, description = "The command to get help for")
+    private String command;
+    
+    private HelpSystem provider;
+
+    public void setProvider(HelpSystem provider) {
+        this.provider = provider;
+    }
+
+    public Object doExecute() throws Exception {
+        String help = provider.getHelp(session, command);
+        if (help != null) {
+            System.out.println(help);
+        }
+        return null;
+    }
+
+    private SortedMap<String, String> getCommandDescriptions(Set<String> names) {
+        SortedMap<String,String> commands = new TreeMap<String,String>();
+        for (String name : names) {
+            if (command != null && !name.startsWith(command)) {
+                continue;
+            }
+            String description = null;
+            Function function = (Function) session.get(name);
+            function = unProxy(function);
+            if (function instanceof AbstractCommand) {
+                try {
+                    Method mth = AbstractCommand.class.getDeclaredMethod("createNewAction");
+                    mth.setAccessible(true);
+                    Action action = (Action) mth.invoke(function);
+                    Class<? extends Action> clazz = action.getClass();
+                    Command ann = clazz.getAnnotation(Command.class);
+                    description = ann.description();
+                } catch (Throwable e) {
+                }
+                if (name.startsWith("*:")) {
+                    name = name.substring(2);
+                }
+                commands.put(name, description);
+            }
+        }
+        return commands;
+    }
+
+    private void printMethodList(Terminal term, PrintStream out, SortedMap<String, String> commands) {
+        out.println(Ansi.ansi().a(Ansi.Attribute.INTENSITY_BOLD).a("COMMANDS").a(Ansi.Attribute.RESET));
+//        int max = 0;
+//        for (Map.Entry<String,String> entry : commands.entrySet()) {
+//            String key = NameScoping.getCommandNameWithoutGlobalPrefix(session, entry.getKey());
+//            max = Math.max(max,key.length());
+//        }
+        for (Map.Entry<String,String> entry : commands.entrySet()) {
+            out.print("        ");
+            String key = NameScoping.getCommandNameWithoutGlobalPrefix(session, entry.getKey());
+            out.println(Ansi.ansi().a(Ansi.Attribute.INTENSITY_BOLD).a(key).a(Ansi.Attribute.RESET));
+            if (entry.getValue() != null) {
+                DefaultActionPreparator.printFormatted("                ", entry.getValue(),
+                        term != null ? term.getWidth() : 80, out);
+            }
+        }
+        out.println();
+    }
+
+    private void printSubShellHelp(Bundle bundle, SubShell subShell, PrintStream out) {
+        Terminal term = session != null ? (Terminal) session.get(".jline.terminal") : null;
+        out.println(Ansi.ansi().a(Ansi.Attribute.INTENSITY_BOLD).a("SUBSHELL").a(Ansi.Attribute.RESET));
+        out.print("        ");
+        if (subShell.getName() != null) {
+            out.println(Ansi.ansi().a(Ansi.Attribute.INTENSITY_BOLD).a(subShell.getName()).a(Ansi.Attribute.RESET));
+            out.println();
+        }
+        out.print("\t");
+        out.println(subShell.getDescription());
+        out.println();
+        if (subShell.getDetailedDescription() != null) {
+            out.println(Ansi.ansi().a(Ansi.Attribute.INTENSITY_BOLD).a("DETAILS").a(Ansi.Attribute.RESET));
+            String desc = loadDescription(bundle, subShell.getDetailedDescription());
+            printFormatted("        ", desc, term != null ? term.getWidth() : 80, out);
+        }
+    }
+
+    protected String loadDescription(Bundle bundle, String desc) {
+        if (desc.startsWith("classpath:")) {
+            URL url = bundle.getResource(desc.substring("classpath:".length()));
+            if (url == null) {
+                desc = "Unable to load description from " + desc;
+            } else {
+                InputStream is = null;
+                try {
+                    is = url.openStream();
+                    Reader r = new InputStreamReader(is);
+                    StringWriter sw = new StringWriter();
+                    int c;
+                    while ((c = r.read()) != -1) {
+                        sw.append((char) c);
+                    }
+                    desc = sw.toString();
+                } catch (IOException e) {
+                    desc = "Unable to load description from " + desc;
+                } finally {
+                    try {
+                        is.close();
+                    } catch (IOException e) {
+                        // Ignore
+                    }
+                }
+            }
+        }
+        return desc;
+    }
+
+    protected Function unProxy(Function function) {
+        try {
+            if (function.getClass().getName().contains("CommandProxy")) {
+                Field contextField = function.getClass().getDeclaredField("context");
+                Field referenceField = function.getClass().getDeclaredField("reference");
+                contextField.setAccessible(true);
+                referenceField.setAccessible(true);
+                BundleContext context = (BundleContext) contextField.get(function);
+                ServiceReference reference = (ServiceReference) referenceField.get(function);
+                Object target = context.getService(reference);
+                try {
+                    if (target instanceof Function) {
+                        function = (Function) target;
+                    }
+                } finally {
+                    context.ungetService(reference);
+                }
+            }
+        } catch (Throwable t) {
+        }
+        return function;
+    }
+
+}

Added: karaf/branches/karaf-2.3.x/shell/console/src/main/java/org/apache/karaf/shell/console/help/HelpSystem.java
URL: http://svn.apache.org/viewvc/karaf/branches/karaf-2.3.x/shell/console/src/main/java/org/apache/karaf/shell/console/help/HelpSystem.java?rev=1311786&view=auto
==============================================================================
--- karaf/branches/karaf-2.3.x/shell/console/src/main/java/org/apache/karaf/shell/console/help/HelpSystem.java (added)
+++ karaf/branches/karaf-2.3.x/shell/console/src/main/java/org/apache/karaf/shell/console/help/HelpSystem.java Tue Apr 10 15:26:18 2012
@@ -0,0 +1,85 @@
+/**
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.karaf.shell.console.help;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.felix.service.command.CommandSession;
+import org.apache.karaf.shell.console.HelpProvider;
+import org.apache.karaf.util.InterpolationHelper;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.ServiceReference;
+import org.osgi.util.tracker.ServiceTracker;
+
+public class HelpSystem implements HelpProvider {
+
+    private BundleContext context;
+    private ServiceTracker tracker;
+
+    public void setContext(BundleContext context) {
+        this.context = context;
+    }
+
+    public void start() {
+        tracker = new ServiceTracker(context, HelpProvider.class.getName(), null);
+        tracker.open();
+    }
+    
+    public void stop() {
+        tracker.close();
+    }
+    
+    public synchronized List<HelpProvider> getProviders() {
+        ServiceReference[] refs = tracker.getServiceReferences();
+        Arrays.sort(refs);
+        List<HelpProvider> providers = new ArrayList<HelpProvider>();
+        for (int i = refs.length - 1; i >= 0; i--) {
+            providers.add((HelpProvider) tracker.getService(refs[i]));
+        }
+        return providers;
+    }
+    
+    public String getHelp(final CommandSession session, String path) {
+        if (path == null) {
+            path = "%root%";
+        }
+        Map<String,String> props = new HashMap<String,String>();
+        props.put("data", "${" + path + "}");
+        final List<HelpProvider> providers = getProviders();
+        InterpolationHelper.performSubstitution(props, new InterpolationHelper.SubstitutionCallback() {
+            public String getValue(final String key) {
+                for (HelpProvider hp : providers) {
+                    String help = hp.getHelp(session, key);
+                    if (help != null) {
+                        if (help.endsWith("\n")) {
+                            help = help.substring(0, help.length()  -1);
+                        }
+                        return help;
+                    }
+                }
+                return null;
+            }
+        });
+        return props.get("data");
+    }
+    
+}

Added: karaf/branches/karaf-2.3.x/shell/console/src/main/java/org/apache/karaf/shell/console/help/SimpleHelpProvider.java
URL: http://svn.apache.org/viewvc/karaf/branches/karaf-2.3.x/shell/console/src/main/java/org/apache/karaf/shell/console/help/SimpleHelpProvider.java?rev=1311786&view=auto
==============================================================================
--- karaf/branches/karaf-2.3.x/shell/console/src/main/java/org/apache/karaf/shell/console/help/SimpleHelpProvider.java (added)
+++ karaf/branches/karaf-2.3.x/shell/console/src/main/java/org/apache/karaf/shell/console/help/SimpleHelpProvider.java Tue Apr 10 15:26:18 2012
@@ -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.karaf.shell.console.help;
+
+import java.io.ByteArrayOutputStream;
+import java.io.PrintStream;
+import java.util.Map;
+
+import jline.Terminal;
+import org.apache.felix.service.command.CommandSession;
+import org.apache.karaf.shell.console.HelpProvider;
+
+import static org.apache.felix.gogo.commands.basic.DefaultActionPreparator.printFormatted;
+
+public class SimpleHelpProvider implements HelpProvider {
+    
+    private Map<String, String> help;
+
+    public Map<String, String> getHelp() {
+        return help;
+    }
+
+    public void setHelp(Map<String, String> help) {
+        this.help = help;
+    }
+
+    public String getHelp(CommandSession session, String path) {
+        if (path.indexOf('|') > 0) {
+            if (path.startsWith("simple|")) {
+                path = path.substring("simple|".length());
+            } else {
+                return null;
+            }
+        }
+        String str = help.get(path);
+        if (str != null) {
+            Terminal term = (Terminal) session.get(".jline.terminal");
+            ByteArrayOutputStream baos = new ByteArrayOutputStream();
+            printFormatted("", str, term != null ? term.getWidth() : 80, new PrintStream(baos, true));
+            str = baos.toString();
+        }
+        return str;
+    }
+}

Added: karaf/branches/karaf-2.3.x/shell/console/src/main/java/org/apache/karaf/shell/console/help/SingleCommandHelpProvider.java
URL: http://svn.apache.org/viewvc/karaf/branches/karaf-2.3.x/shell/console/src/main/java/org/apache/karaf/shell/console/help/SingleCommandHelpProvider.java?rev=1311786&view=auto
==============================================================================
--- karaf/branches/karaf-2.3.x/shell/console/src/main/java/org/apache/karaf/shell/console/help/SingleCommandHelpProvider.java (added)
+++ karaf/branches/karaf-2.3.x/shell/console/src/main/java/org/apache/karaf/shell/console/help/SingleCommandHelpProvider.java Tue Apr 10 15:26:18 2012
@@ -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.karaf.shell.console.help;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.PrintStream;
+import java.util.Set;
+
+import org.apache.felix.gogo.runtime.CommandSessionImpl;
+import org.apache.felix.service.command.CommandSession;
+import org.apache.felix.service.threadio.ThreadIO;
+import org.apache.karaf.shell.console.HelpProvider;
+
+public class SingleCommandHelpProvider implements HelpProvider {
+
+    private ThreadIO io;
+
+    public void setIo(ThreadIO io) {
+        this.io = io;
+    }
+
+    public String getHelp(CommandSession session, String path) {
+        if (path.indexOf('|') > 0) {
+            if (path.startsWith("command|")) {
+                path = path.substring("command|".length());
+            } else {
+                return null;
+            }
+        }
+        Set<String> names = (Set<String>) session.get(CommandSessionImpl.COMMANDS);
+        if (path != null && names.contains(path)) {
+            ByteArrayOutputStream baos = new ByteArrayOutputStream();
+            io.setStreams(new ByteArrayInputStream(new byte[0]), new PrintStream(baos, true), new PrintStream(baos, true));
+            try {
+                session.execute(path + " --help");
+            } catch (Throwable t) {
+                t.printStackTrace();
+            } finally {
+                io.close();
+            }
+            return baos.toString();
+        }
+        return null;
+    }
+}

Added: karaf/branches/karaf-2.3.x/shell/console/src/main/java/org/apache/karaf/shell/console/help/SubShellHelpProvider.java
URL: http://svn.apache.org/viewvc/karaf/branches/karaf-2.3.x/shell/console/src/main/java/org/apache/karaf/shell/console/help/SubShellHelpProvider.java?rev=1311786&view=auto
==============================================================================
--- karaf/branches/karaf-2.3.x/shell/console/src/main/java/org/apache/karaf/shell/console/help/SubShellHelpProvider.java (added)
+++ karaf/branches/karaf-2.3.x/shell/console/src/main/java/org/apache/karaf/shell/console/help/SubShellHelpProvider.java Tue Apr 10 15:26:18 2012
@@ -0,0 +1,130 @@
+/**
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.karaf.shell.console.help;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.PrintStream;
+import java.io.Reader;
+import java.io.StringWriter;
+import java.net.URL;
+
+import jline.Terminal;
+import org.apache.felix.service.command.CommandSession;
+import org.apache.karaf.shell.console.HelpProvider;
+import org.apache.karaf.shell.console.SubShell;
+import org.fusesource.jansi.Ansi;
+import org.osgi.framework.Bundle;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.ServiceReference;
+import org.osgi.util.tracker.ServiceTracker;
+
+import static org.apache.felix.gogo.commands.basic.DefaultActionPreparator.printFormatted;
+
+public class SubShellHelpProvider implements HelpProvider {
+
+    private BundleContext context;
+    private ServiceTracker tracker;
+
+    public void setContext(BundleContext context) {
+        this.context = context;
+    }
+
+    public void start() {
+        tracker = new ServiceTracker(context, SubShell.class.getName(), null);
+        tracker.open();
+    }
+    
+    public void stop() {
+        tracker.close();
+    }
+
+    public String getHelp(CommandSession session, String path) {
+        if (path.indexOf('|') > 0) {
+            if (path.startsWith("subshell|")) {
+                path = path.substring("subshell|".length());
+            } else {
+                return null;
+            }
+        }
+        for (ServiceReference ref : tracker.getServiceReferences()) {
+            if (path.equals(ref.getProperty("name"))) {
+                ByteArrayOutputStream baos = new ByteArrayOutputStream();
+                printSubShellHelp(session, ref.getBundle(), (SubShell) tracker.getService(ref), new PrintStream(baos, true));
+                return baos.toString();
+            }
+        }
+        return null;
+    }
+
+    private void printSubShellHelp(CommandSession session, Bundle bundle, SubShell subShell, PrintStream out) {
+        Terminal term = session != null ? (Terminal) session.get(".jline.terminal") : null;
+        out.println(Ansi.ansi().a(Ansi.Attribute.INTENSITY_BOLD).a("SUBSHELL").a(Ansi.Attribute.RESET));
+        out.print("        ");
+        if (subShell.getName() != null) {
+            out.println(Ansi.ansi().a(Ansi.Attribute.INTENSITY_BOLD).a(subShell.getName()).a(Ansi.Attribute.RESET));
+            out.println();
+        }
+        out.print("\t");
+        out.println(subShell.getDescription());
+        out.println();
+        if (subShell.getDetailedDescription() != null) {
+            out.println(Ansi.ansi().a(Ansi.Attribute.INTENSITY_BOLD).a("DETAILS").a(Ansi.Attribute.RESET));
+            String desc = loadDescription(bundle, subShell.getDetailedDescription());
+            while (desc.endsWith("\n")) {
+                desc = desc.substring(0, desc.length()  -1);
+            }
+            printFormatted("        ", desc, term != null ? term.getWidth() : 80, out);
+        }
+        out.println();
+        out.println("${command-list|" + subShell.getName() + ":}");
+    }
+
+    protected String loadDescription(Bundle bundle, String desc) {
+        if (desc.startsWith("classpath:")) {
+            URL url = bundle.getResource(desc.substring("classpath:".length()));
+            if (url == null) {
+                desc = "Unable to load description from " + desc;
+            } else {
+                InputStream is = null;
+                try {
+                    is = url.openStream();
+                    Reader r = new InputStreamReader(is);
+                    StringWriter sw = new StringWriter();
+                    int c;
+                    while ((c = r.read()) != -1) {
+                        sw.append((char) c);
+                    }
+                    desc = sw.toString();
+                } catch (IOException e) {
+                    desc = "Unable to load description from " + desc;
+                } finally {
+                    try {
+                        is.close();
+                    } catch (IOException e) {
+                        // Ignore
+                    }
+                }
+            }
+        }
+        return desc;
+    }
+
+}

Modified: karaf/branches/karaf-2.3.x/shell/console/src/main/java/org/apache/karaf/shell/console/jline/Console.java
URL: http://svn.apache.org/viewvc/karaf/branches/karaf-2.3.x/shell/console/src/main/java/org/apache/karaf/shell/console/jline/Console.java?rev=1311786&r1=1311785&r2=1311786&view=diff
==============================================================================
--- karaf/branches/karaf-2.3.x/shell/console/src/main/java/org/apache/karaf/shell/console/jline/Console.java (original)
+++ karaf/branches/karaf-2.3.x/shell/console/src/main/java/org/apache/karaf/shell/console/jline/Console.java Tue Apr 10 15:26:18 2012
@@ -49,6 +49,7 @@ import org.apache.felix.service.command.
 import org.apache.karaf.shell.console.CloseShellException;
 import org.apache.karaf.shell.console.Completer;
 import org.apache.karaf.shell.console.completer.CommandsCompleter;
+import org.apache.karaf.shell.console.util.Branding;
 import org.fusesource.jansi.Ansi;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -297,7 +298,7 @@ public class Console implements Runnable
     }
 
     protected void welcome() {
-        Properties props = loadBrandingProperties();
+        Properties props = Branding.loadBrandingProperties();
         String welcome = props.getProperty("welcome");
         if (welcome != null && welcome.length() > 0) {
             session.getConsole().println(welcome);
@@ -305,7 +306,7 @@ public class Console implements Runnable
     }
 
     protected void setSessionProperties() {
-        Properties props = loadBrandingProperties();
+        Properties props = Branding.loadBrandingProperties();
         for (Map.Entry<Object, Object> entry : props.entrySet()) {
             String key = (String) entry.getKey();
             if (key.startsWith("session.")) {
@@ -318,33 +319,6 @@ public class Console implements Runnable
         return new CommandsCompleter(session);
     }
 
-    protected Properties loadBrandingProperties() {
-        Properties props = new Properties();
-        loadProps(props, "org/apache/karaf/shell/console/branding.properties");
-        loadProps(props, "org/apache/karaf/branding/branding.properties");
-        return props;
-    }
-
-    protected void loadProps(Properties props, String resource) {
-        InputStream is = null;
-        try {
-            is = getClass().getClassLoader().getResourceAsStream(resource);
-            if (is != null) {
-                props.load(is);
-            }
-        } catch (IOException e) {
-            // ignore
-        } finally {
-            if (is != null) {
-                try {
-                    is.close();
-                } catch (IOException e) {
-                    // Ignore
-                }
-            }
-        }
-    }
-
     protected String getPrompt() {
         try {
             String prompt;
@@ -353,7 +327,7 @@ public class Console implements Runnable
                 if (p != null) {
                     prompt = p.toString();
                 } else {
-                    Properties properties = loadBrandingProperties();
+                    Properties properties = Branding.loadBrandingProperties();
                     if (properties.getProperty("prompt") != null) {
                         prompt = properties.getProperty("prompt");
                         // we put the PROMPT in ConsoleSession to avoid to read

Added: karaf/branches/karaf-2.3.x/shell/console/src/main/java/org/apache/karaf/shell/console/util/Branding.java
URL: http://svn.apache.org/viewvc/karaf/branches/karaf-2.3.x/shell/console/src/main/java/org/apache/karaf/shell/console/util/Branding.java?rev=1311786&view=auto
==============================================================================
--- karaf/branches/karaf-2.3.x/shell/console/src/main/java/org/apache/karaf/shell/console/util/Branding.java (added)
+++ karaf/branches/karaf-2.3.x/shell/console/src/main/java/org/apache/karaf/shell/console/util/Branding.java Tue Apr 10 15:26:18 2012
@@ -0,0 +1,56 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.karaf.shell.console.util;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Properties;
+
+public final class Branding {
+    
+    private Branding() { }
+
+    public static Properties loadBrandingProperties() {
+        Properties props = new Properties();
+        loadProps(props, "org/apache/karaf/shell/console/branding.properties");
+        loadProps(props, "org/apache/karaf/branding/branding.properties");
+        return props;
+    }
+
+    protected static void loadProps(Properties props, String resource) {
+        InputStream is = null;
+        try {
+            is = Branding.class.getClassLoader().getResourceAsStream(resource);
+            if (is != null) {
+                props.load(is);
+            }
+        } catch (IOException e) {
+            // ignore
+        } finally {
+            if (is != null) {
+                try {
+                    is.close();
+                } catch (IOException e) {
+                    // Ignore
+                }
+            }
+        }
+    }
+
+}

Modified: karaf/branches/karaf-2.3.x/shell/console/src/main/resources/META-INF/services/org/apache/karaf/shell/commands
URL: http://svn.apache.org/viewvc/karaf/branches/karaf-2.3.x/shell/console/src/main/resources/META-INF/services/org/apache/karaf/shell/commands?rev=1311786&r1=1311785&r2=1311786&view=diff
==============================================================================
--- karaf/branches/karaf-2.3.x/shell/console/src/main/resources/META-INF/services/org/apache/karaf/shell/commands (original)
+++ karaf/branches/karaf-2.3.x/shell/console/src/main/resources/META-INF/services/org/apache/karaf/shell/commands Tue Apr 10 15:26:18 2012
@@ -14,4 +14,4 @@
 ##  See the License for the specific language governing permissions and
 ##  limitations under the License.
 ##---------------------------------------------------------------------------
-org.apache.karaf.shell.console.HelpAction
+org.apache.karaf.shell.console.help.HelpAction

Modified: karaf/branches/karaf-2.3.x/shell/console/src/main/resources/OSGI-INF/blueprint/karaf-console.xml
URL: http://svn.apache.org/viewvc/karaf/branches/karaf-2.3.x/shell/console/src/main/resources/OSGI-INF/blueprint/karaf-console.xml?rev=1311786&r1=1311785&r2=1311786&view=diff
==============================================================================
--- karaf/branches/karaf-2.3.x/shell/console/src/main/resources/OSGI-INF/blueprint/karaf-console.xml (original)
+++ karaf/branches/karaf-2.3.x/shell/console/src/main/resources/OSGI-INF/blueprint/karaf-console.xml Tue Apr 10 15:26:18 2012
@@ -59,9 +59,45 @@
             <property name="blueprintContainer" ref="blueprintContainer"/>
             <property name="blueprintConverter" ref="blueprintConverter"/>
             <property name="actionId" value="help"/>
+            <property name="completers">
+                <list>
+                    <bean class="org.apache.karaf.shell.console.completer.CommandNamesCompleter"/>
+                </list>
+            </property>
+        </bean>
+    </service>
+    <bean id="help" class="org.apache.karaf.shell.console.help.HelpAction" activation="lazy" scope="prototype">
+        <property name="provider" ref="helpSystem"/>
+    </bean>
+    
+    <bean id="helpSystem" class="org.apache.karaf.shell.console.help.HelpSystem" init-method="start" destroy-method="stop">
+        <property name="context" ref="blueprintBundleContext"/>
+    </bean>
+    
+    <service auto-export="interfaces" ranking="-20">
+        <bean class="org.apache.karaf.shell.console.help.CommandListHelpProvider" />
+    </service>
+    <service auto-export="interfaces" ranking="-10">
+        <bean class="org.apache.karaf.shell.console.help.SingleCommandHelpProvider">
+            <property name="io">
+                <reference interface="org.apache.felix.service.threadio.ThreadIO"/>
+            </property>
+        </bean>
+    </service>
+    <service auto-export="interfaces" ref="subShellHelpProvider" ranking="-10"/>
+    <bean id="subShellHelpProvider" class="org.apache.karaf.shell.console.help.SubShellHelpProvider" init-method="start" destroy-method="stop">
+        <property name="context" ref="blueprintBundleContext"/>
+    </bean>
+    <service auto-export="interfaces" ranking="-5">
+        <bean class="org.apache.karaf.shell.console.help.SimpleHelpProvider">
+            <property name="help">
+                <map>
+                    <entry key="%root%"><value><![CDATA[${command-list|}]]></value></entry>
+                    <entry key="all"><value><![CDATA[${command-list|}]]></value></entry>
+                </map>
+            </property>
         </bean>
     </service>
-    <bean id="help" class="org.apache.karaf.shell.console.HelpAction" activation="lazy" scope="prototype" />
 
 
 </blueprint>

Added: karaf/branches/karaf-2.3.x/shell/console/src/test/java/org/apache/karaf/shell/console/help/TestFormatting.java
URL: http://svn.apache.org/viewvc/karaf/branches/karaf-2.3.x/shell/console/src/test/java/org/apache/karaf/shell/console/help/TestFormatting.java?rev=1311786&view=auto
==============================================================================
--- karaf/branches/karaf-2.3.x/shell/console/src/test/java/org/apache/karaf/shell/console/help/TestFormatting.java (added)
+++ karaf/branches/karaf-2.3.x/shell/console/src/test/java/org/apache/karaf/shell/console/help/TestFormatting.java Tue Apr 10 15:26:18 2012
@@ -0,0 +1,35 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.karaf.shell.console.help;
+
+import java.io.ByteArrayOutputStream;
+import java.io.PrintStream;
+
+import junit.framework.TestCase;
+import org.apache.felix.gogo.commands.basic.DefaultActionPreparator;
+
+public class TestFormatting extends TestCase {
+    
+    public void testFormat() throws Exception {
+        ByteArrayOutputStream baos = new ByteArrayOutputStream();
+        DefaultActionPreparator.printFormatted("  ",
+                "  This is a test with a long paragraph\n\n  with an indented paragraph\nAnd another one\n", 20, new PrintStream(baos, true));
+        System.err.println(baos.toString());
+    }
+}

Modified: karaf/branches/karaf-2.3.x/shell/osgi/src/main/resources/OSGI-INF/blueprint/shell-osgi.xml
URL: http://svn.apache.org/viewvc/karaf/branches/karaf-2.3.x/shell/osgi/src/main/resources/OSGI-INF/blueprint/shell-osgi.xml?rev=1311786&r1=1311785&r2=1311786&view=diff
==============================================================================
--- karaf/branches/karaf-2.3.x/shell/osgi/src/main/resources/OSGI-INF/blueprint/shell-osgi.xml (original)
+++ karaf/branches/karaf-2.3.x/shell/osgi/src/main/resources/OSGI-INF/blueprint/shell-osgi.xml Tue Apr 10 15:26:18 2012
@@ -74,6 +74,18 @@
         </command>
     </command-bundle>
 
+    <service auto-export="interfaces">
+        <service-properties>
+            <entry key="name" value="osgi"/>
+            <entry key="description" value="Commands to manage the OSGi framework"/>
+        </service-properties>
+        <bean class="org.apache.karaf.shell.console.commands.BasicSubShell">
+            <property name="name" value="osgi"/>
+            <property name="description" value="Commands to manage the OSGi framework"/>
+            <property name="detailedDescription" value="classpath:/org/apache/karaf/shell/osgi/osgi.txt"/>
+        </bean>
+    </service>
+
     <bean id="blueprintListener" class="org.apache.karaf.shell.osgi.BlueprintListener" />
     <service ref="blueprintListener" interface="org.osgi.service.blueprint.container.BlueprintListener" />
 

Added: karaf/branches/karaf-2.3.x/shell/osgi/src/main/resources/org/apache/karaf/shell/osgi/osgi.txt
URL: http://svn.apache.org/viewvc/karaf/branches/karaf-2.3.x/shell/osgi/src/main/resources/org/apache/karaf/shell/osgi/osgi.txt?rev=1311786&view=auto
==============================================================================
--- karaf/branches/karaf-2.3.x/shell/osgi/src/main/resources/org/apache/karaf/shell/osgi/osgi.txt (added)
+++ karaf/branches/karaf-2.3.x/shell/osgi/src/main/resources/org/apache/karaf/shell/osgi/osgi.txt Tue Apr 10 15:26:18 2012
@@ -0,0 +1,5 @@
+The commands in this subshell can be used to inspect or modify the OSGi framework.
+
+The full list of commands is available below and you can obtain a more detailed help for a given command by using the help command followed by the name of the command:
+  > help osgi:list
+

Modified: karaf/branches/karaf-2.3.x/util/pom.xml
URL: http://svn.apache.org/viewvc/karaf/branches/karaf-2.3.x/util/pom.xml?rev=1311786&r1=1311785&r2=1311786&view=diff
==============================================================================
--- karaf/branches/karaf-2.3.x/util/pom.xml (original)
+++ karaf/branches/karaf-2.3.x/util/pom.xml Tue Apr 10 15:26:18 2012
@@ -37,4 +37,11 @@
         <appendedResourcesDirectory>${basedir}/../etc/appended-resources</appendedResourcesDirectory>
     </properties>
 
-</project>
\ No newline at end of file
+    <dependencies>
+        <dependency>
+            <groupId>org.osgi</groupId>
+            <artifactId>org.osgi.core</artifactId>
+        </dependency>
+    </dependencies>
+
+</project>

Added: karaf/branches/karaf-2.3.x/util/src/main/java/org/apache/karaf/util/InterpolationHelper.java
URL: http://svn.apache.org/viewvc/karaf/branches/karaf-2.3.x/util/src/main/java/org/apache/karaf/util/InterpolationHelper.java?rev=1311786&view=auto
==============================================================================
--- karaf/branches/karaf-2.3.x/util/src/main/java/org/apache/karaf/util/InterpolationHelper.java (added)
+++ karaf/branches/karaf-2.3.x/util/src/main/java/org/apache/karaf/util/InterpolationHelper.java Tue Apr 10 15:26:18 2012
@@ -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.karaf.util;
+
+
+import java.util.HashMap;
+import java.util.Map;
+
+import org.osgi.framework.BundleContext;
+
+/**
+ * <p>
+ * Enhancement of the standard <code>Properties</code>
+ * managing the maintain of comments, etc.
+ * </p>
+ *
+ * @author gnodet, jbonofre
+ */
+public class InterpolationHelper {
+
+    private InterpolationHelper() {
+    }
+
+    private static final char ESCAPE_CHAR = '\\';
+    private static final String DELIM_START = "${";
+    private static final String DELIM_STOP = "}";
+
+
+    /**
+     * Callback for substitution
+     */
+    public interface SubstitutionCallback {
+
+        public String getValue(String key);
+
+    }
+
+    /**
+     * Perform substitution on a property set
+     *
+     * @param properties the property set to perform substitution on
+     */
+    public static void performSubstitution(Map<String, String> properties) {
+        performSubstitution(properties, (BundleContext) null);
+    }
+
+    /**
+     * Perform substitution on a property set
+     *
+     * @param properties the property set to perform substitution on
+     */
+    public static void performSubstitution(Map<String, String> properties, final BundleContext context) {
+        performSubstitution(properties, new SubstitutionCallback() {
+            public String getValue(String key) {
+                String value = null;
+                if (context != null) {
+                    value = context.getProperty(key);
+                }
+                if (value == null) {
+                    value = System.getProperty(value, "");
+                }
+                return value;
+            }
+        });
+    }
+
+    /**
+     * Perform substitution on a property set
+     *
+     * @param properties the property set to perform substitution on
+     */
+    public static void performSubstitution(Map<String, String> properties, SubstitutionCallback callback) {
+        for (String name : properties.keySet()) {
+            String value = properties.get(name);
+            properties.put(name, substVars(value, name, null, properties, callback));
+        }
+    }
+
+
+    /**
+     * <p>
+     * This method performs property variable substitution on the
+     * specified value. If the specified value contains the syntax
+     * <tt>${&lt;prop-name&gt;}</tt>, where <tt>&lt;prop-name&gt;</tt>
+     * refers to either a configuration property or a system property,
+     * then the corresponding property value is substituted for the variable
+     * placeholder. Multiple variable placeholders may exist in the
+     * specified value as well as nested variable placeholders, which
+     * are substituted from inner most to outer most. Configuration
+     * properties override system properties.
+     * </p>
+     *
+     * @param val         The string on which to perform property substitution.
+     * @param currentKey  The key of the property being evaluated used to
+     *                    detect cycles.
+     * @param cycleMap    Map of variable references used to detect nested cycles.
+     * @param configProps Set of configuration properties.
+     * @param callback    the callback to obtain substitution values
+     * @return The value of the specified string after system property substitution.
+     * @throws IllegalArgumentException If there was a syntax error in the
+     *                                  property placeholder syntax or a recursive variable reference.
+     */
+    public static String substVars(String val,
+                                   String currentKey,
+                                   Map<String, String> cycleMap,
+                                   Map<String, String> configProps,
+                                   SubstitutionCallback callback)
+            throws IllegalArgumentException {
+        if (cycleMap == null) {
+            cycleMap = new HashMap<String, String>();
+        }
+
+        // Put the current key in the cycle map.
+        cycleMap.put(currentKey, currentKey);
+
+        // Assume we have a value that is something like:
+        // "leading ${foo.${bar}} middle ${baz} trailing"
+
+        // Find the first ending '}' variable delimiter, which
+        // will correspond to the first deepest nested variable
+        // placeholder.
+        int stopDelim = val.indexOf(DELIM_STOP);
+        while (stopDelim > 0 && val.charAt(stopDelim - 1) == ESCAPE_CHAR) {
+            stopDelim = val.indexOf(DELIM_STOP, stopDelim + 1);
+        }
+
+        // Find the matching starting "${" variable delimiter
+        // by looping until we find a start delimiter that is
+        // greater than the stop delimiter we have found.
+        int startDelim = val.indexOf(DELIM_START);
+        while (stopDelim >= 0) {
+            int idx = val.indexOf(DELIM_START, startDelim + DELIM_START.length());
+            if ((idx < 0) || (idx > stopDelim)) {
+                break;
+            } else if (idx < stopDelim) {
+                startDelim = idx;
+            }
+        }
+
+        // If we do not have a start or stop delimiter, then just
+        // return the existing value.
+        if ((startDelim < 0) || (stopDelim < 0)) {
+            return unescape(val);
+        }
+
+        // At this point, we have found a variable placeholder so
+        // we must perform a variable substitution on it.
+        // Using the start and stop delimiter indices, extract
+        // the first, deepest nested variable placeholder.
+        String variable = val.substring(startDelim + DELIM_START.length(), stopDelim);
+
+        // Verify that this is not a recursive variable reference.
+        if (cycleMap.get(variable) != null) {
+            throw new IllegalArgumentException("recursive variable reference: " + variable);
+        }
+
+        // Get the value of the deepest nested variable placeholder.
+        // Try to configuration properties first.
+        String substValue = (String) ((configProps != null) ? configProps.get(variable) : null);
+        if (substValue == null) {
+            if (variable.length() <= 0) {
+                substValue = "";
+            } else {
+                if (callback != null) {
+                    substValue = callback.getValue(variable);
+                }
+                if (substValue == null) {
+                    substValue = System.getProperty(variable, "");
+                }
+            }
+        }
+
+        // Remove the found variable from the cycle map, since
+        // it may appear more than once in the value and we don't
+        // want such situations to appear as a recursive reference.
+        cycleMap.remove(variable);
+
+        // Append the leading characters, the substituted value of
+        // the variable, and the trailing characters to get the new
+        // value.
+        val = val.substring(0, startDelim) + substValue + val.substring(stopDelim + DELIM_STOP.length(), val.length());
+
+        // Now perform substitution again, since there could still
+        // be substitutions to make.
+        val = substVars(val, currentKey, cycleMap, configProps, callback);
+
+        // Remove escape characters preceding {, } and \
+        val = unescape(val);
+
+        // Return the value.
+        return val;
+    }
+
+    private static String unescape(String val) {
+        int escape = val.indexOf(ESCAPE_CHAR);
+        while (escape >= 0 && escape < val.length() - 1) {
+            char c = val.charAt(escape + 1);
+            if (c == '{' || c == '}' || c == ESCAPE_CHAR) {
+                val = val.substring(0, escape) + val.substring(escape + 1);
+            }
+            escape = val.indexOf(ESCAPE_CHAR, escape + 1);
+        }
+        return val;
+    }
+
+}