diff --git a/pom.xml b/pom.xml index 5b97f307..60e56ec5 100644 --- a/pom.xml +++ b/pom.xml @@ -59,6 +59,13 @@ org.springframework.boot spring-boot-maven-plugin + + org.apache.maven.plugins + maven-surefire-plugin + + true + + diff --git a/src/main/java/org/springframework/shell2/Bootstrap.java b/src/main/java/org/springframework/shell2/Bootstrap.java index 8a1ea7d3..dadb1aaa 100644 --- a/src/main/java/org/springframework/shell2/Bootstrap.java +++ b/src/main/java/org/springframework/shell2/Bootstrap.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.springframework.boot.SpringApplication; diff --git a/src/main/java/org/springframework/shell2/DefaultMethodTargetResolver.java b/src/main/java/org/springframework/shell2/DefaultMethodTargetResolver.java index 15d3e062..406aaa65 100644 --- a/src/main/java/org/springframework/shell2/DefaultMethodTargetResolver.java +++ b/src/main/java/org/springframework/shell2/DefaultMethodTargetResolver.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.HashMap; @@ -15,15 +31,15 @@ public class DefaultMethodTargetResolver implements MethodTargetResolver { @Override public Map resolve(ApplicationContext applicationContext) { - Map methodTargets = new HashMap<>(); + Map methodTargets = new HashMap<>(); Map 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.& +&