commandBeans = applicationContext.getBeansWithAnnotation(ShellComponent.class);
for (Object bean : commandBeans.values()) {
Class> clazz = bean.getClass();
ReflectionUtils.doWithMethods(clazz, method -> {
ShellMethod shellMapping = method.getAnnotation(ShellMethod.class);
String[] keys = shellMapping.value();
- if (keys.length == 1 && "".equals(keys[0])) {
- keys[0] = method.getName();
+ if (keys.length == 0) {
+ keys = new String[]{method.getName()};
}
for (String key : keys) {
methodTargets.put(key, new MethodTarget(method, bean, shellMapping.help()));
diff --git a/src/main/java/org/springframework/shell2/DefaultParameterResolver.java b/src/main/java/org/springframework/shell2/DefaultParameterResolver.java
index ce6d767f..4e776d0e 100644
--- a/src/main/java/org/springframework/shell2/DefaultParameterResolver.java
+++ b/src/main/java/org/springframework/shell2/DefaultParameterResolver.java
@@ -1,5 +1,23 @@
+/*
+ * Copyright 2015 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
package org.springframework.shell2;
+import static org.springframework.shell2.Utils.unCamelify;
+
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.util.ArrayList;
@@ -23,25 +41,27 @@ import org.springframework.util.ConcurrentReferenceHashMap;
/**
* Default ParameterResolver implementation that supports the following features:
- * - named parameters (recognized because they start with some {@link ShellMethod#prefix()}
- * - implicit named parameters (from the actual method parameter name)
- * - positional parameters (in order, for all parameter values that were not resolved via named parameters)
- * - default values (for all remaining parameters)
+ * - named parameters (recognized because they start with some {@link ShellMethod#prefix()}
+ * - implicit named parameters (from the actual method parameter name)
+ * - positional parameters (in order, for all parameter values that were not resolved via named parameters)
+ * - default values (for all remaining parameters)
*
*
* Method arguments can consume several words of input at once (driven by {@link ShellOption#arity()}, default 1).
- * If several words are consumed, they will be joined together as a comma separated value and passed to the {@link ConversionService}
+ * If several words are consumed, they will be joined together as a comma separated value and passed to the {@link
+ * ConversionService}
* (which will typically return a List or array).
*
- * Boolean parameters are by default expected to have an arity of 0, allowing invocations in the form {@code rm --force --dir /foo}:
- * the presence of {@code --force} passes {@code true} as a parameter value, while its absence passes {@code false}. Both
+ *
Boolean parameters are by default expected to have an arity of 0, allowing invocations in the form {@code rm
+ * --force --dir /foo}:
+ * the presence of {@code --force} passes {@code true} as a parameter value, while its absence passes {@code false}.
+ * Both
* the default arity of 0 and the default value of {@code false} can be overridden via {@link ShellOption}
* if needed.
- *
* @author Eric Bottard
* @author Florent Biville
*/
-class DefaultParameterResolver implements ParameterResolver {
+public class DefaultParameterResolver implements ParameterResolver {
private final ConversionService conversionService;
@@ -63,7 +83,7 @@ class DefaultParameterResolver implements ParameterResolver {
@Override
public Object resolve(MethodParameter methodParameter, List words) {
- String prefix = methodParameter.getMethod().getAnnotation(ShellMethod.class).prefix();
+ String prefix = prefixForMethod(methodParameter);
CacheKey cacheKey = new CacheKey(methodParameter.getMethod(), words);
Map resolved = parameterCache.computeIfAbsent(cacheKey, (k) -> {
@@ -113,11 +133,7 @@ class DefaultParameterResolver implements ParameterResolver {
offset += arity;
} // No more input. Try defaultValues
else {
- Optional defaultValue = Optional.empty();
- ShellOption option = parameter.getAnnotation(ShellOption.class);
- if (option != null && !ShellOption.NULL.equals(option.defaultValue())) {
- defaultValue = Optional.of(option.defaultValue());
- }
+ Optional defaultValue = defaultValueFor(parameter);
String value = defaultValue.orElseThrow(() -> new RuntimeException(String.format("Ran out of input for " + keys)));
result.put(parameter, value);
}
@@ -133,7 +149,72 @@ class DefaultParameterResolver implements ParameterResolver {
});
String s = resolved.get(methodParameter.getMethod().getParameters()[methodParameter.getParameterIndex()]);
- return conversionService.convert(s, TypeDescriptor.valueOf(String.class), new TypeDescriptor(methodParameter));
+ if (ShellOption.NULL.equals(s)) {
+ return null;
+ }
+ else {
+ return conversionService.convert(s, TypeDescriptor.valueOf(String.class), new TypeDescriptor(methodParameter));
+ }
+ }
+
+ private String prefixForMethod(MethodParameter methodParameter) {
+ return methodParameter.getMethod().getAnnotation(ShellMethod.class).prefix();
+ }
+
+ private Optional defaultValueFor(Parameter parameter) {
+ Optional defaultValue = Optional.empty();
+ ShellOption option = parameter.getAnnotation(ShellOption.class);
+ if (option != null && !ShellOption.NONE.equals(option.defaultValue())) {
+ defaultValue = Optional.of(option.defaultValue());
+ }
+ return defaultValue;
+ }
+
+ @Override
+ public ParameterDescription describe(MethodParameter parameter) {
+ Parameter jlrParameter = parameter.getMethod().getParameters()[parameter.getParameterIndex()];
+ int arity = getArity(jlrParameter);
+ Class> type = parameter.getParameterType();
+ ShellOption option = jlrParameter.getAnnotation(ShellOption.class);
+ StringBuilder sb = new StringBuilder();
+ for (int i = 0; i < arity; i++) {
+ if (i > 0) {
+ sb.append(" ");
+ }
+ sb.append(arity > 1 ? unCamelify(removeMultiplicityFromType(parameter).getSimpleName()) : unCamelify(type.getSimpleName()));
+ }
+ ParameterDescription result = ParameterDescription.ofType(type);
+ result.formal(sb.toString());
+ if (option != null) {
+ result.help(option.help());
+ Optional defaultValue = defaultValueFor(jlrParameter);
+ if (defaultValue.isPresent()) {
+ result.defaultValue(defaultValue.map(dv -> dv.equals(ShellOption.NULL) ? "" : dv).get());
+ }
+ }
+ List rawKeys = getKeysForParameter(parameter.getMethod(), parameter.getParameterIndex());
+ String prefix = prefixForMethod(parameter);
+ result
+ .keys(rawKeys.stream().map(k -> prefix + k).collect(Collectors.toList()))
+ .mandatoryKey(false);
+
+ return result;
+ }
+
+ /**
+ * In case of {@code foo[] or Collection} and arity > 1, return the element type.
+ */
+ private Class> removeMultiplicityFromType(MethodParameter parameter) {
+ Class> parameterType = parameter.getParameterType();
+ if (parameterType.isArray()) {
+ return parameterType.getComponentType();
+ }
+ else if (parameterType.isAssignableFrom(Collection.class)) {
+ return parameter.getNestedParameterType();
+ }
+ else {
+ throw new RuntimeException("For " + parameter + " (with arity > 1) expected an array/collection type");
+ }
}
/**
@@ -167,7 +248,7 @@ class DefaultParameterResolver implements ParameterResolver {
* or from the actual parameter name.
* @throws IllegalArgumentException if parameter names could not be extracted
*/
- private Collection getKeysForParameter(Method method, int index) {
+ private List getKeysForParameter(Method method, int index) {
Parameter parameter = method.getParameters()[index];
ShellOption option = parameter.getAnnotation(ShellOption.class);
if (option != null && option.value().length > 0) {
@@ -180,7 +261,7 @@ class DefaultParameterResolver implements ParameterResolver {
Assert.notNull(parameterName, String.format(
"Could not discover parameter name at index %d for %s, and option key(s) were not specified via %s annotation",
index, method, ShellOption.class.getSimpleName()));
- return Collections.singleton(parameterName);
+ return Collections.singletonList(parameterName);
}
}
diff --git a/src/main/java/org/springframework/shell2/JLineShell.java b/src/main/java/org/springframework/shell2/JLineShell.java
index 25e21b36..4432b288 100644
--- a/src/main/java/org/springframework/shell2/JLineShell.java
+++ b/src/main/java/org/springframework/shell2/JLineShell.java
@@ -1,3 +1,19 @@
+/*
+ * Copyright 2015 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
package org.springframework.shell2;
import java.lang.reflect.Parameter;
@@ -7,6 +23,7 @@ import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
+import java.util.stream.Collectors;
import javax.annotation.PostConstruct;
import javax.validation.ConstraintViolation;
@@ -22,7 +39,9 @@ import org.jline.reader.ParsedLine;
import org.jline.reader.impl.DefaultParser;
import org.jline.terminal.Terminal;
import org.jline.terminal.TerminalBuilder;
+import org.jline.utils.AttributedCharSequence;
import org.jline.utils.AttributedString;
+import org.jline.utils.AttributedStringBuilder;
import org.jline.utils.AttributedStyle;
import org.springframework.beans.factory.annotation.Autowired;
@@ -37,7 +56,7 @@ import org.springframework.util.ReflectionUtils;
* Created by ericbottard on 26/11/15.
*/
@Component
-public class JLineShell {
+public class JLineShell implements Shell {
@Autowired
private ApplicationContext applicationContext;
@@ -54,6 +73,11 @@ public class JLineShell {
@Autowired
private List parameterResolvers = new ArrayList<>();
+ @Override
+ public Map listCommands() {
+ return methodTargets;
+ }
+
@PostConstruct
public void init() throws Exception {
for (MethodTargetResolver resolver : applicationContext.getBeansOfType(MethodTargetResolver.class).values()) {
@@ -76,11 +100,19 @@ public class JLineShell {
@Override
public AttributedString highlight(LineReader reader, String buffer) {
- if (buffer.length() < 4) {
- return new AttributedString(buffer, AttributedStyle.DEFAULT.blink().foreground(AttributedStyle.RED));
+ int l = 0;
+ String best = null;
+ for (String command : methodTargets.keySet()) {
+ if (buffer.startsWith(command) && command.length() > l) {
+ l = command.length();
+ best = command;
+ }
+ }
+ if (best != null) {
+ return new AttributedStringBuilder(buffer.length()).append(best, AttributedStyle.BOLD).append(buffer.substring(l)).toAttributedString();
}
else {
- return new AttributedString(buffer);
+ return new AttributedString(buffer, AttributedStyle.DEFAULT.foreground(AttributedStyle.RED));
}
}
})
@@ -111,6 +143,8 @@ public class JLineShell {
}
List words = lineReader.getParsedLine().words();
+ // TODO investigate trailing empty string in e.g. "help 'WTF 2'"
+ words = words.stream().filter(w -> w.length() > 0).collect(Collectors.toList());
if (methodTarget != null) {
Parameter[] parameters = methodTarget.getMethod().getParameters();
Object[] rawArgs = new Object[parameters.length];
@@ -135,7 +169,9 @@ public class JLineShell {
Set> constraintViolations = executableValidator.validateParameters(methodTarget.getBean(),
methodTarget.getMethod(),
rawArgs);
- System.out.println(constraintViolations);
+ if (constraintViolations.size() > 0) {
+ System.out.println(constraintViolations);
+ }
Object result = null;
try {
@@ -145,7 +181,12 @@ public class JLineShell {
result = e;
}
- System.out.println(String.valueOf(result));
+ if (result instanceof AttributedCharSequence) {
+ System.out.println(((AttributedCharSequence) result).toAnsi(lineReader.getTerminal()));
+ }
+ else {
+ System.out.println(String.valueOf(result));
+ }
}
else {
diff --git a/src/main/java/org/springframework/shell2/MethodTarget.java b/src/main/java/org/springframework/shell2/MethodTarget.java
index 1a18271a..de124433 100644
--- a/src/main/java/org/springframework/shell2/MethodTarget.java
+++ b/src/main/java/org/springframework/shell2/MethodTarget.java
@@ -1,7 +1,25 @@
+/*
+ * Copyright 2015 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
package org.springframework.shell2;
import java.lang.reflect.Method;
+import org.springframework.util.Assert;
+
/**
* Created by ericbottard on 27/11/15.
*/
@@ -14,6 +32,9 @@ public class MethodTarget {
private final String help;
public MethodTarget(Method method, Object bean, String help) {
+ Assert.notNull(method, "Method cannot be null");
+ Assert.notNull(bean, "Bean cannot be null");
+ Assert.hasText(help, "Help cannot be blank");
this.method = method;
this.bean = bean;
this.help = help;
diff --git a/src/main/java/org/springframework/shell2/MethodTargetResolver.java b/src/main/java/org/springframework/shell2/MethodTargetResolver.java
index 9997ab3d..d2c12adb 100644
--- a/src/main/java/org/springframework/shell2/MethodTargetResolver.java
+++ b/src/main/java/org/springframework/shell2/MethodTargetResolver.java
@@ -1,3 +1,19 @@
+/*
+ * Copyright 2015 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
package org.springframework.shell2;
import java.util.Map;
diff --git a/src/main/java/org/springframework/shell2/ParameterDescription.java b/src/main/java/org/springframework/shell2/ParameterDescription.java
new file mode 100644
index 00000000..e4870b74
--- /dev/null
+++ b/src/main/java/org/springframework/shell2/ParameterDescription.java
@@ -0,0 +1,125 @@
+/*
+ * Copyright 2015 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.shell2;
+
+import java.util.List;
+import java.util.Optional;
+
+import org.springframework.core.MethodParameter;
+
+/**
+ * Encapsulates information about a shell invokable method parameter, so that it can be documented.
+ *
+ * Instances of this class are constructed by {@link ParameterResolver#describe(MethodParameter)}.
+ *
+ * @author Eric Bottard
+ */
+public class ParameterDescription {
+
+ /**
+ * A string representation of the type of the parameter.
+ */
+ private final String type;
+
+ /**
+ * A string representation of the parameter, as it should appear in a parameter list.
+ * If not provided, this is derived from the parameter type.
+ */
+ private String formal;
+
+ /**
+ * A string representation of the default value for the parameter, if any.
+ */
+ private Optional defaultValue = Optional.empty();
+
+ /**
+ * The list of 'keys' that can be used to specify this parameter, if any.
+ */
+ private List keys;
+
+ /**
+ * Depending on the {@link ParameterResolver}, whether keys are mandatory to identify this parameter.
+ */
+ private boolean mandatoryKey = true;
+
+ /**
+ * A short description of this parameter.
+ */
+ private String help = "";
+
+ public ParameterDescription(String type) {
+ this.type = type;
+ this.formal = type;
+ }
+
+ public static ParameterDescription ofType(String type) {
+ return new ParameterDescription(Utils.unCamelify(type));
+ }
+
+ public static ParameterDescription ofType(Class> type) {
+ return ofType(type.getSimpleName());
+ }
+
+ public ParameterDescription help(String help) {
+ this.help = help;
+ return this;
+ }
+
+ public boolean mandatoryKey() {
+ return mandatoryKey;
+ }
+
+ public List keys() {
+ return keys;
+ }
+
+ public Optional defaultValue() {
+ return defaultValue;
+ }
+
+ public ParameterDescription defaultValue(String defaultValue) {
+ this.defaultValue = Optional.of(defaultValue);
+ return this;
+ }
+
+ public ParameterDescription keys(List keys) {
+ this.keys = keys;
+ return this;
+ }
+
+ public ParameterDescription mandatoryKey(boolean mandatoryKey) {
+ this.mandatoryKey = mandatoryKey;
+ return this;
+ }
+
+ public String type() {
+ return type;
+ }
+
+ public String formal() {
+ return formal;
+ }
+
+ public String help() {
+ return help;
+ }
+
+ public ParameterDescription formal(String formal) {
+ this.formal = formal;
+ return this;
+ }
+}
diff --git a/src/main/java/org/springframework/shell2/ParameterResolver.java b/src/main/java/org/springframework/shell2/ParameterResolver.java
index 2f15e5dc..5697abb0 100644
--- a/src/main/java/org/springframework/shell2/ParameterResolver.java
+++ b/src/main/java/org/springframework/shell2/ParameterResolver.java
@@ -1,3 +1,19 @@
+/*
+ * Copyright 2015 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
package org.springframework.shell2;
import java.util.List;
@@ -11,6 +27,8 @@ public interface ParameterResolver {
boolean supports(MethodParameter parameter);
- public Object resolve(MethodParameter methodParameter, List words);
+ Object resolve(MethodParameter methodParameter, List words);
+
+ ParameterDescription describe(MethodParameter parameter);
}
diff --git a/src/main/java/org/springframework/shell2/Shell.java b/src/main/java/org/springframework/shell2/Shell.java
new file mode 100644
index 00000000..5a82dcd7
--- /dev/null
+++ b/src/main/java/org/springframework/shell2/Shell.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2015 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.shell2;
+
+import java.util.Map;
+
+/**
+ * Created by ericbottard on 17/12/15.
+ */
+public interface Shell {
+
+
+ public Map listCommands();
+
+}
diff --git a/src/main/java/org/springframework/shell2/ShellComponent.java b/src/main/java/org/springframework/shell2/ShellComponent.java
index 1dfce3d5..6184f773 100644
--- a/src/main/java/org/springframework/shell2/ShellComponent.java
+++ b/src/main/java/org/springframework/shell2/ShellComponent.java
@@ -1,3 +1,19 @@
+/*
+ * Copyright 2015 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
package org.springframework.shell2;
import java.lang.annotation.Documented;
@@ -9,7 +25,13 @@ import java.lang.annotation.Target;
import org.springframework.stereotype.Component;
/**
- * Created by ericbottard on 27/11/15.
+ * Indicates that an annotated class may contain shell methods (themselves annotated with {@link @ShellMethod}) that
+ * is,
+ * methods that may be invoked reflectively by the shell.
+ *
+ * This annotation is a specialization of {@link Component}.
+ * @author Eric Bottard
+ * @see Component
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@@ -17,5 +39,8 @@ import org.springframework.stereotype.Component;
@Component
public @interface ShellComponent {
- String value();
+ /**
+ * Used to indicate a suggestion for a logical name for the component.
+ */
+ String value() default "";
}
diff --git a/src/main/java/org/springframework/shell2/ShellMethod.java b/src/main/java/org/springframework/shell2/ShellMethod.java
index c7f2003f..1647ff04 100644
--- a/src/main/java/org/springframework/shell2/ShellMethod.java
+++ b/src/main/java/org/springframework/shell2/ShellMethod.java
@@ -1,3 +1,19 @@
+/*
+ * Copyright 2015 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
package org.springframework.shell2;
import java.lang.annotation.Documented;
@@ -8,7 +24,6 @@ import java.lang.annotation.Target;
/**
* Used to mark a method as invokable via Spring Shell.
- *
* @author Eric Bottard
* @author Florent Biville
*/
diff --git a/src/main/java/org/springframework/shell2/ShellOption.java b/src/main/java/org/springframework/shell2/ShellOption.java
index 598b0b17..a0bc14e7 100644
--- a/src/main/java/org/springframework/shell2/ShellOption.java
+++ b/src/main/java/org/springframework/shell2/ShellOption.java
@@ -1,3 +1,19 @@
+/*
+ * Copyright 2015 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
package org.springframework.shell2;
import java.lang.annotation.Documented;
@@ -16,6 +32,8 @@ public @interface ShellOption {
String NULL = "__NULL__";
+ String NONE = "__NONE__";
+
/**
* The key(s) (without the {@link ShellMethod#prefix()}) by which this parameter can be referenced
* when using named parameters. If none is specified, the actual method parameter name will be used.
@@ -30,5 +48,10 @@ public @interface ShellOption {
/**
* The textual (pre-conversion) value to assign to this parameter if no value is provided by the user.
*/
- String defaultValue() default NULL;
+ String defaultValue() default NONE;
+
+ /**
+ * Return a short description of the parameter.
+ */
+ String help() default "";
}
diff --git a/src/main/java/org/springframework/shell2/Utils.java b/src/main/java/org/springframework/shell2/Utils.java
new file mode 100644
index 00000000..9f05a7ed
--- /dev/null
+++ b/src/main/java/org/springframework/shell2/Utils.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2015 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.shell2;
+
+/**
+ * Some text utilities.
+ *
+ * @author Eric Bottard
+ */
+public class Utils {
+
+ /**
+ * Turn CamelCaseText into gnu-style-lowercase.
+ */
+ public static String unCamelify(CharSequence original) {
+ StringBuilder result = new StringBuilder(original.length());
+ boolean wasLowercase = false;
+ for (int i = 0; i < original.length(); i++) {
+ char ch = original.charAt(i);
+ if (Character.isUpperCase(ch) && wasLowercase) {
+ result.append('-');
+ }
+ wasLowercase = Character.isLowerCase(ch);
+ result.append(Character.toLowerCase(ch));
+ }
+ return result.toString();
+ }
+
+}
diff --git a/src/main/java/org/springframework/shell2/commands/Help.java b/src/main/java/org/springframework/shell2/commands/Help.java
new file mode 100644
index 00000000..ac7d48d1
--- /dev/null
+++ b/src/main/java/org/springframework/shell2/commands/Help.java
@@ -0,0 +1,218 @@
+/*
+ * Copyright 2015 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.shell2.commands;
+
+import static java.util.stream.Collectors.mapping;
+import static java.util.stream.Collectors.toCollection;
+
+import java.io.IOException;
+import java.lang.reflect.Parameter;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.TreeSet;
+import java.util.stream.Collectors;
+
+import org.jline.utils.AttributedStringBuilder;
+import org.jline.utils.AttributedStyle;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.core.MethodParameter;
+import org.springframework.shell2.MethodTarget;
+import org.springframework.shell2.ParameterDescription;
+import org.springframework.shell2.ParameterResolver;
+import org.springframework.shell2.Shell;
+import org.springframework.shell2.ShellComponent;
+import org.springframework.shell2.ShellMethod;
+import org.springframework.shell2.ShellOption;
+
+/**
+ * A command to display help about all available commands.
+ *
+ * @author Eric Bottard
+ */
+@ShellComponent
+public class Help {
+
+ private final List parameterResolvers;
+
+ private Shell shell;
+
+ @Autowired
+ public Help(List parameterResolvers) {
+ this.parameterResolvers = parameterResolvers;
+ }
+
+ @Autowired // ctor injection impossible b/c of circular dependency
+ public void setShell(Shell shell) {
+ this.shell = shell;
+ }
+
+ @ShellMethod(help = "Display help about available commands.", prefix = "-")
+ public CharSequence help(
+ @ShellOption(defaultValue = ShellOption.NULL,
+ value = {"C", "-command"},
+ help = "The command to obtain help for.") String command) throws IOException {
+ if (command == null) {
+ return listCommands();
+ }
+ else {
+ return documentCommand(command);
+ }
+
+ }
+
+ /**
+ * Return a description of a specific command. Uses a layout inspired by *nix man pages.
+ */
+ private CharSequence documentCommand(String command) {
+ MethodTarget methodTarget = shell.listCommands().get(command);
+ if (methodTarget == null) {
+ throw new IllegalArgumentException("Unknown command '" + command + "'");
+ }
+
+ // NAME
+ AttributedStringBuilder result = new AttributedStringBuilder().append("\n\n");
+ result.append("NAME", AttributedStyle.BOLD).append("\n\t");
+ result.append(command).append(" - ").append(methodTarget.getHelp()).append("\n\n");
+
+ // SYNOPSYS
+ result.append("SYNOPSYS", AttributedStyle.BOLD).append("\n\t");
+ result.append(command, AttributedStyle.BOLD);
+ result.append(" ");
+
+ List parameterDescriptions = getParameterDescriptions(methodTarget);
+
+ for (ParameterDescription description : parameterDescriptions) {
+
+ if (description.defaultValue().isPresent()) {
+ result.append("[");
+ }
+ if (!description.mandatoryKey()) {
+ result.append("[");
+ }
+ result.append(description.keys().iterator().next(), AttributedStyle.BOLD);
+ if (!description.mandatoryKey()) {
+ result.append("]");
+ if (!description.formal().isEmpty()) {
+ result.append(" ");
+ }
+ }
+ appendUnderlinedFormal(result, description);
+ if (description.defaultValue().isPresent()) {
+ result.append("]");
+ }
+ result.append(" ");
+ }
+ result.append("\n\n");
+
+ // OPTIONS
+ result.append("OPTIONS", AttributedStyle.BOLD).append("\n");
+ for (ParameterDescription description : parameterDescriptions) {
+ result.append("\t").append(description.keys().stream().collect(Collectors.joining(" or ")), AttributedStyle.BOLD);
+ if (description.formal().length() > 0) {
+ result.append(" ");
+ appendUnderlinedFormal(result, description);
+ result.append("\n\t");
+ }
+ else if (description.keys().size() > 1) {
+ result.append("\n\t");
+ }
+ result.append("\t");
+ result.append(description.help());
+ if (description.defaultValue().isPresent()) {
+ result
+ .append(" [Optional, default = ", AttributedStyle.BOLD)
+ .append(description.defaultValue().get(), AttributedStyle.BOLD.italic())
+ .append("]", AttributedStyle.BOLD);
+ } else {
+ result.append(" [Mandatory]", AttributedStyle.BOLD);
+ }
+ result.append("\n\n");
+ }
+
+ // ALSO KNOWN AS
+ Set aliases = shell.listCommands().entrySet().stream()
+ .filter(e -> e.getValue().equals(methodTarget))
+ .map(e -> e.getKey())
+ .filter(c -> !command.equals(c))
+ .collect(toCollection(TreeSet::new));
+
+ if (!aliases.isEmpty()) {
+ result.append("ALSO KNOWN AS", AttributedStyle.BOLD).append("\n");
+ for (String alias : aliases) {
+ result.append('\t').append(alias).append('\n');
+ }
+ }
+
+ result.append("\n");
+ return result;
+ }
+
+ private CharSequence listCommands() {
+ Map> groupedByMethodTarget = shell.listCommands().entrySet().stream()
+ .collect(Collectors.groupingBy(e -> e.getValue().getHelp(), // Use help() as the grouping key
+ mapping(Map.Entry::getKey, toCollection(TreeSet::new)))); // accumulate the command 'names' into a sorted set
+
+ // Then display commands, sorted alphabetically by their first alias
+ AttributedStringBuilder result = new AttributedStringBuilder();
+ result.append("AVAILABLE COMMANDS\n\n", AttributedStyle.BOLD);
+
+ groupedByMethodTarget.entrySet().stream()
+ .sorted(sortByFirstElement())
+ .forEach(e -> result.append("\t")
+ .append(e.getValue().stream().collect(Collectors.joining(", ")), AttributedStyle.BOLD)
+ .append(": ")
+ .append(e.getKey())
+ .append('\n')
+ );
+ return result.append("\n");
+ }
+
+ private Comparator>> sortByFirstElement() {
+ return (e1, e2) -> e1.getValue().iterator().next().compareTo(e2.getValue().iterator().next());
+ }
+
+ private void appendUnderlinedFormal(AttributedStringBuilder result, ParameterDescription description) {
+ for (char c : description.formal().toCharArray()) {
+ if (c != ' ') {
+ result.append("" + c, AttributedStyle.DEFAULT.underline());
+ }
+ else {
+ result.append(c);
+ }
+ }
+ }
+
+ private List getParameterDescriptions(MethodTarget methodTarget) {
+ Parameter[] parameters = methodTarget.getMethod().getParameters();
+ List parameterDescriptions = new ArrayList<>();
+ for (int i = 0, parametersLength = parameters.length; i < parametersLength; i++) {
+ for (ParameterResolver parameterResolver : parameterResolvers) {
+ MethodParameter methodParameter = new MethodParameter(methodTarget.getMethod(), i);
+ if (parameterResolver.supports(methodParameter)) {
+ parameterDescriptions.add(parameterResolver.describe(methodParameter));
+ break;
+ }
+ }
+ }
+ return parameterDescriptions;
+ }
+
+}
diff --git a/src/main/java/org/springframework/shell2/jcommander/JCommanderParameterResolver.java b/src/main/java/org/springframework/shell2/jcommander/JCommanderParameterResolver.java
index a6105ed6..ac29542f 100644
--- a/src/main/java/org/springframework/shell2/jcommander/JCommanderParameterResolver.java
+++ b/src/main/java/org/springframework/shell2/jcommander/JCommanderParameterResolver.java
@@ -1,3 +1,19 @@
+/*
+ * Copyright 2015 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
package org.springframework.shell2.jcommander;
import java.lang.annotation.Annotation;
@@ -13,6 +29,7 @@ import com.beust.jcommander.ParametersDelegate;
import org.springframework.beans.BeanUtils;
import org.springframework.core.MethodParameter;
+import org.springframework.shell2.ParameterDescription;
import org.springframework.shell2.ParameterResolver;
import org.springframework.util.ReflectionUtils;
@@ -60,4 +77,8 @@ public class JCommanderParameterResolver implements ParameterResolver {
return pojo;
}
+ @Override
+ public ParameterDescription describe(MethodParameter parameter) {
+ throw new UnsupportedOperationException();
+ }
}
diff --git a/src/main/java/org/springframework/shell2/legacy/LegacyMethodTargetResolver.java b/src/main/java/org/springframework/shell2/legacy/LegacyMethodTargetResolver.java
index 67917c84..03d9708c 100644
--- a/src/main/java/org/springframework/shell2/legacy/LegacyMethodTargetResolver.java
+++ b/src/main/java/org/springframework/shell2/legacy/LegacyMethodTargetResolver.java
@@ -1,3 +1,19 @@
+/*
+ * Copyright 2015 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
package org.springframework.shell2.legacy;
import java.util.HashMap;
@@ -12,7 +28,10 @@ import org.springframework.stereotype.Component;
import org.springframework.util.ReflectionUtils;
/**
- * Created by ericbottard on 09/12/15.
+ * A {@link MethodTargetResolver} that discovers methods annotated with {@link CliCommand} on beans
+ * implementing the {@link CommandMarker} marker interface.
+ * @author Eric Bottard
+ * @author Florent Biville
*/
@Component
public class LegacyMethodTargetResolver implements MethodTargetResolver {
@@ -20,13 +39,13 @@ public class LegacyMethodTargetResolver implements MethodTargetResolver {
@Override
public Map resolve(ApplicationContext context) {
Map methodTargets = new HashMap<>();
- Map beansOfType = context.getBeansOfType(CommandMarker.class);
- for (Object bean : beansOfType.values()) {
+ Map beans = context.getBeansOfType(CommandMarker.class);
+ for (Object bean : beans.values()) {
Class> clazz = bean.getClass();
ReflectionUtils.doWithMethods(clazz, method -> {
- CliCommand shellMapping = method.getAnnotation(CliCommand.class);
- for (String key : shellMapping.value()) {
- methodTargets.put(key, new MethodTarget(method, bean, shellMapping.help()));
+ CliCommand cliCommand = method.getAnnotation(CliCommand.class);
+ for (String key : cliCommand.value()) {
+ methodTargets.put(key, new MethodTarget(method, bean, cliCommand.help()));
}
}, method -> method.getAnnotation(CliCommand.class) != null);
}
diff --git a/src/main/java/org/springframework/shell2/legacy/LegacyParameterResolver.java b/src/main/java/org/springframework/shell2/legacy/LegacyParameterResolver.java
index 3914cbad..9516c8fb 100644
--- a/src/main/java/org/springframework/shell2/legacy/LegacyParameterResolver.java
+++ b/src/main/java/org/springframework/shell2/legacy/LegacyParameterResolver.java
@@ -1,3 +1,19 @@
+/*
+ * Copyright 2015 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
package org.springframework.shell2.legacy;
import java.util.ArrayList;
@@ -14,6 +30,7 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.MethodParameter;
import org.springframework.shell.core.Converter;
import org.springframework.shell.core.annotation.CliOption;
+import org.springframework.shell2.ParameterDescription;
import org.springframework.shell2.ParameterResolver;
import org.springframework.stereotype.Component;
import org.springframework.util.Assert;
@@ -24,8 +41,6 @@ import org.springframework.util.Assert;
@Component
public class LegacyParameterResolver implements ParameterResolver {
- public static final Object UNSPECIFIED = new Object();
-
@Autowired(required = false)
private Collection> converters = new ArrayList<>();
@@ -61,6 +76,11 @@ public class LegacyParameterResolver implements ParameterResolver {
}
}
+ @Override
+ public ParameterDescription describe(MethodParameter parameter) {
+ throw new UnsupportedOperationException();
+ }
+
private Map parseOptions(List words) {
Map values = new HashMap<>();
for (int i = 0; i < words.size(); i++) {
@@ -68,7 +88,7 @@ public class LegacyParameterResolver implements ParameterResolver {
if (word.startsWith("--")) {
String key = word.substring("--".length());
// If next word doesn't exist or starts with '--', this is an unary option. Store null
- String value = i < words.size() - 1 && !words.get(i+1).startsWith("--") ? words.get(++i) : null;
+ String value = i < words.size() - 1 && !words.get(i + 1).startsWith("--") ? words.get(++i) : null;
Assert.isTrue(!values.containsKey(key), String.format("Option --%s has already been set", key));
values.put(key, value);
} // Must be the 'anonymous' option
diff --git a/src/main/resources/logback.xml b/src/main/resources/logback.xml
new file mode 100644
index 00000000..6ff6a9a4
--- /dev/null
+++ b/src/main/resources/logback.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/src/test/java/org/springframework/shell2/DefaultParameterResolverTest.java b/src/test/java/org/springframework/shell2/DefaultParameterResolverTest.java
index 4f89315b..1a9d5acc 100644
--- a/src/test/java/org/springframework/shell2/DefaultParameterResolverTest.java
+++ b/src/test/java/org/springframework/shell2/DefaultParameterResolverTest.java
@@ -1,3 +1,19 @@
+/*
+ * Copyright 2015 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
package org.springframework.shell2;
import org.junit.Rule;
diff --git a/src/test/java/org/springframework/shell2/Remote.java b/src/test/java/org/springframework/shell2/Remote.java
index f41a5e94..3241de4e 100644
--- a/src/test/java/org/springframework/shell2/Remote.java
+++ b/src/test/java/org/springframework/shell2/Remote.java
@@ -1,3 +1,19 @@
+/*
+ * Copyright 2015 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
package org.springframework.shell2;
/**
diff --git a/src/test/java/org/springframework/shell2/UtilsTest.java b/src/test/java/org/springframework/shell2/UtilsTest.java
new file mode 100644
index 00000000..3a7018c9
--- /dev/null
+++ b/src/test/java/org/springframework/shell2/UtilsTest.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2015 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.shell2;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import org.junit.Test;
+
+/**
+ * Tests for {@link Utils}.
+ *
+ * @author Eric Bottard
+ */
+public class UtilsTest {
+
+ @Test
+ public void testUnCamelify() throws Exception {
+ assertThat(Utils.unCamelify("HelloWorld")).isEqualTo("hello-world");
+ assertThat(Utils.unCamelify("helloWorld")).isEqualTo("hello-world");
+ assertThat(Utils.unCamelify("helloWorldHowAreYou")).isEqualTo("hello-world-how-are-you");
+ assertThat(Utils.unCamelify("URL")).isEqualTo("url");
+ }
+}
diff --git a/src/test/java/org/springframework/shell2/commands/HelpTest.java b/src/test/java/org/springframework/shell2/commands/HelpTest.java
new file mode 100644
index 00000000..3e929ee8
--- /dev/null
+++ b/src/test/java/org/springframework/shell2/commands/HelpTest.java
@@ -0,0 +1,157 @@
+/*
+ * Copyright 2015 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.shell2.commands;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.lang.reflect.Method;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.assertj.core.api.Assertions;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TestName;
+import org.junit.runner.RunWith;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.core.convert.support.DefaultConversionService;
+import org.springframework.core.io.ClassPathResource;
+import org.springframework.shell2.DefaultParameterResolver;
+import org.springframework.shell2.MethodTarget;
+import org.springframework.shell2.ParameterResolver;
+import org.springframework.shell2.Shell;
+import org.springframework.shell2.ShellComponent;
+import org.springframework.shell2.ShellMethod;
+import org.springframework.shell2.ShellOption;
+import org.springframework.test.context.ContextConfiguration;
+import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
+import org.springframework.util.FileCopyUtils;
+import org.springframework.util.ReflectionUtils;
+
+/**
+ * Tests for the {@link Help} command.
+ *
+ * @author Eric Bottard
+ */
+@RunWith(SpringJUnit4ClassRunner.class)
+@ContextConfiguration(classes = HelpTest.Config.class)
+public class HelpTest {
+
+ @Autowired
+ private Help help;
+
+ @Rule
+ public TestName testName = new TestName();
+
+ @Test
+ public void testCommandHelp() throws Exception {
+ CharSequence help = this.help.help("first-command").toString();
+ Assertions.assertThat(help).isEqualTo(sample());
+ }
+
+ @Test
+ public void testCommandList() throws Exception {
+ String list = this.help.help(null).toString();
+ Assertions.assertThat(list).isEqualTo(sample());
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testUnknownCommand() throws Exception {
+ this.help.help("some unknown command");
+ }
+
+ private String sample() throws IOException {
+ InputStream is = new ClassPathResource(HelpTest.class.getSimpleName() + "-" + testName.getMethodName() + ".txt", HelpTest.class).getInputStream();
+ return FileCopyUtils.copyToString(new InputStreamReader(is, "UTF-8")).replace("&", "");
+ }
+
+ @Configuration
+ static class Config {
+
+ @Bean
+ public Help help() {
+ return new Help(Collections.singletonList(parameterResolver()));
+ }
+
+ @Bean
+ public Shell shell() {
+ return () -> {
+ Map result = new HashMap<>();
+ Method method = ReflectionUtils.findMethod(Commands.class, "firstCommand", boolean.class, boolean.class, int.class, float[].class);
+ MethodTarget methodTarget = new MethodTarget(method, commands(), "A rather extensive description of some command.");
+ result.put("first-command", methodTarget);
+ result.put("1st-command", methodTarget);
+
+ method = ReflectionUtils.findMethod(Commands.class, "secondCommand");
+ methodTarget = new MethodTarget(method, commands(), "The second command. This one is known under several aliases as well.");
+ result.put("second-command", methodTarget);
+ result.put("yet-another-command", methodTarget);
+
+ method = ReflectionUtils.findMethod(Commands.class, "thirdCommand");
+ methodTarget = new MethodTarget(method, commands(), "The last command.");
+ result.put("third-command", methodTarget);
+
+ return result;
+ };
+ }
+
+ @Bean
+ public ParameterResolver parameterResolver() {
+ return new DefaultParameterResolver(new DefaultConversionService());
+ }
+
+ @Bean
+ public Object commands() {
+ return new Commands();
+ }
+
+ }
+
+ @ShellComponent
+ static class Commands {
+
+ @ShellMethod(prefix = "-")
+ public void firstCommand(
+ // Single key and arity = 0. Help displayed on same line
+ @ShellOption(help = "Whether to delete recursively", arity = 0) boolean r,
+ // Multiple keys and arity 0. Help displayed on next line
+ @ShellOption(help = "Do not ask for confirmation. YOLO", arity = 0, value = {"f", "-force"}) boolean force,
+ // Single key, arity >= 1. Help displayed on next line. Optional
+ @ShellOption(help = "The answer to everything", defaultValue = "42") int n,
+ // Single key, arity > 1.
+ @ShellOption(help = "Some other parameters", arity = 3) float[] o
+ ) {
+
+ }
+
+ @ShellMethod
+ public void secondCommand() {
+
+ }
+
+ @ShellMethod
+ public void thirdCommand() {
+
+ }
+
+ }
+}
diff --git a/src/test/java/org/springframework/shell2/jcommander/FieldCollins.java b/src/test/java/org/springframework/shell2/jcommander/FieldCollins.java
index 541a9f17..a5243dd1 100644
--- a/src/test/java/org/springframework/shell2/jcommander/FieldCollins.java
+++ b/src/test/java/org/springframework/shell2/jcommander/FieldCollins.java
@@ -1,3 +1,19 @@
+/*
+ * Copyright 2015 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
package org.springframework.shell2.jcommander;
import java.util.ArrayList;
diff --git a/src/test/java/org/springframework/shell2/jcommander/JCommanderParameterResolverTest.java b/src/test/java/org/springframework/shell2/jcommander/JCommanderParameterResolverTest.java
index 7688d33e..e89d1411 100644
--- a/src/test/java/org/springframework/shell2/jcommander/JCommanderParameterResolverTest.java
+++ b/src/test/java/org/springframework/shell2/jcommander/JCommanderParameterResolverTest.java
@@ -1,3 +1,19 @@
+/*
+ * Copyright 2015 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
package org.springframework.shell2.jcommander;
import org.junit.Test;
diff --git a/src/test/java/org/springframework/shell2/jcommander/MyLordCommands.java b/src/test/java/org/springframework/shell2/jcommander/MyLordCommands.java
index 2158c8bb..d47bc86b 100644
--- a/src/test/java/org/springframework/shell2/jcommander/MyLordCommands.java
+++ b/src/test/java/org/springframework/shell2/jcommander/MyLordCommands.java
@@ -1,3 +1,19 @@
+/*
+ * Copyright 2015 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
package org.springframework.shell2.jcommander;
/**
diff --git a/src/test/java/org/springframework/shell2/legacy/ArtifactType.java b/src/test/java/org/springframework/shell2/legacy/ArtifactType.java
index 0ea4058a..e8c5849f 100644
--- a/src/test/java/org/springframework/shell2/legacy/ArtifactType.java
+++ b/src/test/java/org/springframework/shell2/legacy/ArtifactType.java
@@ -1,3 +1,19 @@
+/*
+ * Copyright 2015 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
package org.springframework.shell2.legacy;
/**
diff --git a/src/test/java/org/springframework/shell2/legacy/LegacyCommands.java b/src/test/java/org/springframework/shell2/legacy/LegacyCommands.java
index 7debca35..39b3e5d9 100644
--- a/src/test/java/org/springframework/shell2/legacy/LegacyCommands.java
+++ b/src/test/java/org/springframework/shell2/legacy/LegacyCommands.java
@@ -1,3 +1,19 @@
+/*
+ * Copyright 2015 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
package org.springframework.shell2.legacy;
import java.lang.reflect.Method;
@@ -37,7 +53,7 @@ public class LegacyCommands implements CommandMarker {
return String.format(("Successfully registered module '%s:%s'"), type, name);
}
- @CliCommand("sum")
+ @CliCommand(value = "sum", help = "adds two numbers")
public int sum(@CliOption(key = "v1", unspecifiedDefaultValue = "38") int a, @CliOption(key = "v2", specifiedDefaultValue = "42") int b) {
return a + b;
}
diff --git a/src/test/java/org/springframework/shell2/legacy/LegacyMethodTargetResolverTest.java b/src/test/java/org/springframework/shell2/legacy/LegacyMethodTargetResolverTest.java
index afedd76e..907e0c25 100644
--- a/src/test/java/org/springframework/shell2/legacy/LegacyMethodTargetResolverTest.java
+++ b/src/test/java/org/springframework/shell2/legacy/LegacyMethodTargetResolverTest.java
@@ -1,3 +1,19 @@
+/*
+ * Copyright 2015 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
package org.springframework.shell2.legacy;
diff --git a/src/test/java/org/springframework/shell2/legacy/LegacyParameterResolverTest.java b/src/test/java/org/springframework/shell2/legacy/LegacyParameterResolverTest.java
index 526c28ce..210a091d 100644
--- a/src/test/java/org/springframework/shell2/legacy/LegacyParameterResolverTest.java
+++ b/src/test/java/org/springframework/shell2/legacy/LegacyParameterResolverTest.java
@@ -1,3 +1,19 @@
+/*
+ * Copyright 2015 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
package org.springframework.shell2.legacy;
import org.junit.Rule;
diff --git a/src/test/resources/org/springframework/shell2/commands/HelpTest-testCommandHelp.txt b/src/test/resources/org/springframework/shell2/commands/HelpTest-testCommandHelp.txt
new file mode 100644
index 00000000..9d96be34
--- /dev/null
+++ b/src/test/resources/org/springframework/shell2/commands/HelpTest-testCommandHelp.txt
@@ -0,0 +1,23 @@
+&
+&
+NAME&
+ first-command - A rather extensive description of some command.&
+&
+SYNOPSYS&
+ first-command [-r] [-f] [[-n] int] [-o] float float float &
+&
+OPTIONS&
+ -r Whether to delete recursively [Mandatory]&
+&
+ -f or --force&
+ Do not ask for confirmation. YOLO [Mandatory]&
+&
+ -n int&
+ The answer to everything [Optional, default = 42]&
+&
+ -o float float float&
+ Some other parameters [Mandatory]&
+&
+ALSO KNOWN AS&
+ 1st-command&
+&
diff --git a/src/test/resources/org/springframework/shell2/commands/HelpTest-testCommandList.txt b/src/test/resources/org/springframework/shell2/commands/HelpTest-testCommandList.txt
new file mode 100644
index 00000000..edfba6ca
--- /dev/null
+++ b/src/test/resources/org/springframework/shell2/commands/HelpTest-testCommandList.txt
@@ -0,0 +1,6 @@
+AVAILABLE COMMANDS&
+&
+ 1st-command, first-command: A rather extensive description of some command.&
+ second-command, yet-another-command: The second command. This one is known under several aliases as well.&
+ third-command: The last command.&
+&