diff --git a/picocli-examples/src/main/java/picocli/examples/interactive/UserPasswordDemo.java b/picocli-examples/src/main/java/picocli/examples/interactive/UserPasswordDemo.java
new file mode 100644
index 000000000..053d3b504
--- /dev/null
+++ b/picocli-examples/src/main/java/picocli/examples/interactive/UserPasswordDemo.java
@@ -0,0 +1,34 @@
+package picocli.examples.interactive;
+
+import picocli.CommandLine;
+import picocli.CommandLine.Parameters;
+
+public class UserPasswordDemo implements Runnable {
+
+ @Parameters(index = "0", description = {"User"}, interactive = true, echo = true, prompt = "Enter your user: ")
+ String user;
+
+ @Parameters(index = "1", description = {"Password"}, interactive = true, prompt = "Enter your password: ")
+ String password;
+
+ @Parameters(index = "2", description = {"Action"})
+ String action;
+
+ public void run() {
+ // See also PasswordDemo
+ login(user, password);
+ doAction(action);
+ }
+
+ private void login(String user, String pwd) {
+ System.out.printf("User: %s, Password: %s%n", user, pwd);
+ }
+
+ private void doAction(String action) {
+ System.out.printf("Action: %s%n", action);
+ }
+
+ public static void main(String[] args) {
+ new CommandLine(new UserPasswordDemo()).execute(args);
+ }
+}
diff --git a/src/main/java/picocli/CommandLine.java b/src/main/java/picocli/CommandLine.java
index bbd294ecc..44ab5b3db 100644
--- a/src/main/java/picocli/CommandLine.java
+++ b/src/main/java/picocli/CommandLine.java
@@ -3870,7 +3870,7 @@ public enum ScopeType {
/**
* Set {@code interactive=true} if this option will prompt the end user for a value (like a password).
* Only supported for single-value options and {@code char[]} arrays (no collections, maps or other array types).
- * When running on Java 6 or greater, this will use the {@link Console#readPassword()} API to get a value without echoing input to the console.
+ * When running on Java 6 or greater and echo attribute is false, this will use the {@link Console#readPassword()} API to get a value without echoing input to the console.
*
* Best security practice is to use type {@code char[]} instead of {@code String}, and to to null out the array after use.
*
@@ -3883,6 +3883,22 @@ public enum ScopeType {
*/
boolean interactive() default false;
+ /**
+ * Use this attribute to control a user input is echo-ed to the console or not. If {@code echo=true}, a input is echo-ed to the console.
+ * This attribute is ignored when interactive attribute is false.
+ * @return whether the user input is echo-ed to the console or not
+ * @since 4.X
+ */
+ boolean echo() default false;
+
+ /**
+ * Use this attribute to customize the text displayed to the end user for an interactive option.
+ * This attribute is ignored when interactive attribute is false.
+ * @return text will be displayed to the end user
+ * @since 4.X
+ */
+ String prompt() default "";
+
/** ResourceBundle key for this option. If not specified, (and a ResourceBundle {@linkplain Command#resourceBundle() exists for this command}) an attempt
* is made to find the option description using any of the option names (without leading hyphens) as key.
* @see OptionSpec#description()
@@ -4143,12 +4159,28 @@ public enum ScopeType {
/**
* Set {@code interactive=true} if this positional parameter will prompt the end user for a value (like a password).
* Only supported for single-value positional parameters (not arrays, collections or maps).
- * When running on Java 6 or greater, this will use the {@link Console#readPassword()} API to get a value without echoing input to the console.
+ * When running on Java 6 or greater and echo attribute is false, this will use the {@link Console#readPassword()} API to get a value without echoing input to the console.
* @return whether this positional parameter prompts the end user for a value to be entered on the command line
* @since 3.5
*/
boolean interactive() default false;
+ /**
+ * Use this attribute to control a user input is echo-ed to the console or not. If {@code echo=true}, a input is echo-ed to the console.
+ * This attribute is ignored when interactive attribute is false.
+ * @return whether the user input is echo-ed to the console or not
+ * @since 4.X
+ */
+ boolean echo() default false;
+
+ /**
+ * Use this attribute to customize the text displayed to the end user for an interactive option.
+ * This attribute is ignored when interactive attribute is false.
+ * @return text will be displayed to the end user
+ * @since 4.X
+ */
+ String prompt() default "";
+
/** ResourceBundle key for this option. If not specified, (and a ResourceBundle {@linkplain Command#resourceBundle() exists for this command}) an attempt
* is made to find the positional parameter description using {@code paramLabel() + "[" + index() + "]"} as key.
*
@@ -8217,6 +8249,8 @@ public abstract static class ArgSpec {
// parser fields
private boolean required;
private final boolean interactive;
+ private final boolean echo;
+ private final String prompt;
private final String splitRegex;
private final String splitRegexSynopsisLabel;
protected final ITypeInfo typeInfo;
@@ -8256,6 +8290,8 @@ private > ArgSpec(Builder builder) {
inherited = builder.inherited;
root = builder.root == null && ScopeType.INHERIT.equals(builder.scopeType) ? this : builder.root;
interactive = builder.interactive;
+ echo = builder.echo;
+ prompt = builder.prompt;
initialValue = builder.initialValue;
hasInitialValue = builder.hasInitialValue;
initialValueState = builder.initialValueState;
@@ -8328,6 +8364,12 @@ public boolean required() {
/** Returns whether this option will prompt the user to enter a value on the command line.
* @see Option#interactive() */
public boolean interactive() { return interactive; }
+ /** Returns whether the user input is echo-ed to the console or not.
+ * @see Option#echo() */
+ public boolean echo() { return echo; }
+ /** Returns the text displayed to the end user for an interactive option.
+ * @see Option#prompt() */
+ public String prompt() { return prompt; }
/** Returns the description of this option or positional parameter, after all variables have been rendered,
* including the {@code ${DEFAULT-VALUE}} and {@code ${COMPLETION-CANDIDATES}} variables.
@@ -8840,6 +8882,8 @@ abstract static class Builder> {
private String descriptionKey;
private boolean required;
private boolean interactive;
+ private boolean echo;
+ private String prompt;
private String paramLabel;
private boolean hideParamSyntax;
private String splitRegex;
@@ -8874,6 +8918,8 @@ abstract static class Builder> {
descriptionKey = original.descriptionKey;
required = original.required;
interactive = original.interactive;
+ echo = original.echo;
+ prompt = original.prompt;
paramLabel = original.paramLabel;
hideParamSyntax = original.hideParamSyntax;
splitRegex = original.splitRegex;
@@ -8920,6 +8966,8 @@ abstract static class Builder> {
hideParamSyntax = option.hideParamSyntax();
interactive = option.interactive();
+ echo = option.echo();
+ prompt = option.prompt();
description = option.description();
descriptionKey = option.descriptionKey();
splitRegex = option.split();
@@ -8953,6 +9001,8 @@ abstract static class Builder> {
hideParamSyntax = parameters.hideParamSyntax();
interactive = parameters.interactive();
+ echo = parameters.echo();
+ prompt = parameters.prompt();
description = parameters.description();
descriptionKey = parameters.descriptionKey();
splitRegex = parameters.split();
@@ -8994,6 +9044,12 @@ private static String inferLabel(String label, String fieldName, ITypeInfo typeI
/** Returns whether this option prompts the user to enter a value on the command line.
* @see Option#interactive() */
public boolean interactive() { return interactive; }
+ /** Returns whether the user input is echo-ed to the console or not.
+ * @see Option#echo() */
+ public boolean echo() { return echo; }
+ /** Returns the text displayed to the end user for an interactive option.
+ * @see Option#prompt() */
+ public String prompt() { return prompt; }
/** Returns the description of this option, used when generating the usage documentation.
* @see Option#description() */
@@ -9121,6 +9177,12 @@ private static String inferLabel(String label, String fieldName, ITypeInfo typeI
/** Sets whether this option prompts the user to enter a value on the command line, and returns this builder. */
public T interactive(boolean interactive) { this.interactive = interactive; return self(); }
+ /** Sets whether the user input is echo-ed to the console or not. */
+ public T echo(boolean echo) { this.echo = echo; return self(); }
+
+ /** Sets the text displayed to the end user for an interactive option. */
+ public T prompt(String prompt) { this.prompt = prompt; return self(); }
+
/** Sets the description of this option, used when generating the usage documentation, and returns this builder.
* @see Option#description() */
public T description(String... description) { this.description = Assert.notNull(description, "description").clone(); return self(); }
@@ -13302,11 +13364,11 @@ private int applyValueToSingleValuedField(ArgSpec argSpec,
consumed = 0;
}
}
- // if argSpec is interactive, we may need to read the password from the console:
+ // if argSpec is interactive and echo is false, we may need to read the password from the console:
// - if arity = 0 : ALWAYS read from console
// - if arity = 0..1: ONLY read from console if user specified a non-option value
if (argSpec.interactive() && (arity.max == 0 || !optionalValueExists)) {
- interactiveValue = readPassword(argSpec);
+ interactiveValue = readUserInput(argSpec);
consumed = 0;
}
}
@@ -13326,8 +13388,13 @@ private int applyValueToSingleValuedField(ArgSpec argSpec,
} else {
consumed = 1;
if (interactiveValue != null) {
- initValueMessage = "Setting %s to *** (masked interactive value) for %4$s on %5$s%n";
- overwriteValueMessage = "Overwriting %s value with *** (masked interactive value) for %s on %5$s%n";
+ if (argSpec.echo()) {
+ initValueMessage = "Setting %s to %3$s (interactive value) for %4$s on %5$s%n";
+ overwriteValueMessage = "Overwriting %s value with %3$s (interactive value) for %s on %5$s%n";
+ } else {
+ initValueMessage = "Setting %s to *** (masked interactive value) for %4$s on %5$s%n";
+ overwriteValueMessage = "Overwriting %s value with *** (masked interactive value) for %s on %5$s%n";
+ }
}
if (!char[].class.equals(cls) && !char[].class.equals(argSpec.type())) {
if (interactiveValue != null) {
@@ -13339,7 +13406,7 @@ private int applyValueToSingleValuedField(ArgSpec argSpec,
if (interactiveValue == null) { // setting command line arg to char[] field
newValue = actualValue.toCharArray();
} else {
- actualValue = "***"; // mask interactive value
+ actualValue = getMaskedValue(argSpec, new String(interactiveValue)); // mask interactive value if echo is false
newValue = interactiveValue;
}
}
@@ -13636,7 +13703,7 @@ private List