mirror of
https://github.com/nsnail/spectre.console.git
synced 2025-04-16 17:02:51 +08:00
Custom mask for secret (#970)
* Masking Character added, not yet used. * Setting the masking character can be chained with other extensions. * Added string extension for masking, and replaced hardcoded asterisks. * Check if mask is null first. * Fixed Typo in previous test and added new test for custom masks. * Added tests for masking with null character * Added docs and example. * Adjusted extensions so that Mask is integrated into Secret extension. Updated Exampls and Tests accordingly
This commit is contained in:
parent
088db165ed
commit
eb02c3d534
@ -65,6 +65,23 @@ What's the secret number? _
|
|||||||
Enter password: ************_
|
Enter password: ************_
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Masks
|
||||||
|
|
||||||
|
<?# Example symbol="M:Prompt.Program.AskPasswordWithCustomMask" project="Prompt" /?>
|
||||||
|
|
||||||
|
|
||||||
|
```text
|
||||||
|
Enter password: ------------_
|
||||||
|
```
|
||||||
|
|
||||||
|
You can utilize a null character to completely hide input.
|
||||||
|
|
||||||
|
<?# Example symbol="M:Prompt.Program.AskPasswordWithNullMask" project="Prompt" /?>
|
||||||
|
|
||||||
|
```text
|
||||||
|
Enter password: _
|
||||||
|
```
|
||||||
|
|
||||||
## Optional
|
## Optional
|
||||||
|
|
||||||
<?# Example symbol="M:Prompt.Program.AskColor" project="Prompt" /?>
|
<?# Example symbol="M:Prompt.Program.AskColor" project="Prompt" /?>
|
||||||
|
@ -35,6 +35,12 @@ namespace Prompt
|
|||||||
WriteDivider("Secrets");
|
WriteDivider("Secrets");
|
||||||
var password = AskPassword();
|
var password = AskPassword();
|
||||||
|
|
||||||
|
WriteDivider("Mask");
|
||||||
|
var mask = AskPasswordWithCustomMask();
|
||||||
|
|
||||||
|
WriteDivider("Null Mask");
|
||||||
|
var nullMask = AskPasswordWithNullMask();
|
||||||
|
|
||||||
WriteDivider("Optional");
|
WriteDivider("Optional");
|
||||||
var color = AskColor();
|
var color = AskColor();
|
||||||
|
|
||||||
@ -49,6 +55,8 @@ namespace Prompt
|
|||||||
.AddRow("[grey]Favorite sport[/]", sport)
|
.AddRow("[grey]Favorite sport[/]", sport)
|
||||||
.AddRow("[grey]Age[/]", age.ToString())
|
.AddRow("[grey]Age[/]", age.ToString())
|
||||||
.AddRow("[grey]Password[/]", password)
|
.AddRow("[grey]Password[/]", password)
|
||||||
|
.AddRow("[grey]Mask[/]", mask)
|
||||||
|
.AddRow("[grey]Null Mask[/]", nullMask)
|
||||||
.AddRow("[grey]Favorite color[/]", string.IsNullOrEmpty(color) ? "Unknown" : color));
|
.AddRow("[grey]Favorite color[/]", string.IsNullOrEmpty(color) ? "Unknown" : color));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -147,6 +155,22 @@ namespace Prompt
|
|||||||
.Secret());
|
.Secret());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static string AskPasswordWithCustomMask()
|
||||||
|
{
|
||||||
|
return AnsiConsole.Prompt(
|
||||||
|
new TextPrompt<string>("Enter [green]password[/]?")
|
||||||
|
.PromptStyle("red")
|
||||||
|
.Secret('-'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string AskPasswordWithNullMask()
|
||||||
|
{
|
||||||
|
return AnsiConsole.Prompt(
|
||||||
|
new TextPrompt<string>("Enter [green]password[/]?")
|
||||||
|
.PromptStyle("red")
|
||||||
|
.Secret(null));
|
||||||
|
}
|
||||||
|
|
||||||
public static string AskColor()
|
public static string AskColor()
|
||||||
{
|
{
|
||||||
return AnsiConsole.Prompt(
|
return AnsiConsole.Prompt(
|
||||||
|
@ -5,7 +5,7 @@ namespace Spectre.Console;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public static partial class AnsiConsoleExtensions
|
public static partial class AnsiConsoleExtensions
|
||||||
{
|
{
|
||||||
internal static async Task<string> ReadLine(this IAnsiConsole console, Style? style, bool secret, IEnumerable<string>? items = null, CancellationToken cancellationToken = default)
|
internal static async Task<string> ReadLine(this IAnsiConsole console, Style? style, bool secret, char? mask, IEnumerable<string>? items = null, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
if (console is null)
|
if (console is null)
|
||||||
{
|
{
|
||||||
@ -61,7 +61,8 @@ public static partial class AnsiConsoleExtensions
|
|||||||
if (!char.IsControl(key.KeyChar))
|
if (!char.IsControl(key.KeyChar))
|
||||||
{
|
{
|
||||||
text += key.KeyChar.ToString();
|
text += key.KeyChar.ToString();
|
||||||
console.Write(secret ? "*" : key.KeyChar.ToString(), style);
|
var output = key.KeyChar.ToString();
|
||||||
|
console.Write(secret ? output.Mask(mask) : output, style);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -186,4 +186,27 @@ public static class StringExtensions
|
|||||||
return text.Contains(value, StringComparison.Ordinal);
|
return text.Contains(value, StringComparison.Ordinal);
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// "Masks" every character in a string.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="value">String value to mask.</param>
|
||||||
|
/// <param name="mask">Character to use for masking.</param>
|
||||||
|
/// <returns>Masked string.</returns>
|
||||||
|
public static string Mask(this string value, char? mask)
|
||||||
|
{
|
||||||
|
var output = string.Empty;
|
||||||
|
|
||||||
|
if (mask is null)
|
||||||
|
{
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var c in value)
|
||||||
|
{
|
||||||
|
output += mask;
|
||||||
|
}
|
||||||
|
|
||||||
|
return output;
|
||||||
|
}
|
||||||
}
|
}
|
@ -30,6 +30,12 @@ public sealed class TextPrompt<T> : IPrompt<T>
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public bool IsSecret { get; set; }
|
public bool IsSecret { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the character to use while masking
|
||||||
|
/// a secret prompt.
|
||||||
|
/// </summary>
|
||||||
|
public char? Mask { get; set; } = '*';
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the validation error message.
|
/// Gets or sets the validation error message.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -119,14 +125,15 @@ public sealed class TextPrompt<T> : IPrompt<T>
|
|||||||
|
|
||||||
while (true)
|
while (true)
|
||||||
{
|
{
|
||||||
var input = await console.ReadLine(promptStyle, IsSecret, choices, cancellationToken).ConfigureAwait(false);
|
var input = await console.ReadLine(promptStyle, IsSecret, Mask, choices, cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
// Nothing entered?
|
// Nothing entered?
|
||||||
if (string.IsNullOrWhiteSpace(input))
|
if (string.IsNullOrWhiteSpace(input))
|
||||||
{
|
{
|
||||||
if (DefaultValue != null)
|
if (DefaultValue != null)
|
||||||
{
|
{
|
||||||
console.Write(IsSecret ? "******" : converter(DefaultValue.Value), promptStyle);
|
var defaultValue = converter(DefaultValue.Value);
|
||||||
|
console.Write(IsSecret ? defaultValue.Mask(Mask) : defaultValue, promptStyle);
|
||||||
console.WriteLine();
|
console.WriteLine();
|
||||||
return DefaultValue.Value;
|
return DefaultValue.Value;
|
||||||
}
|
}
|
||||||
@ -202,12 +209,13 @@ public sealed class TextPrompt<T> : IPrompt<T>
|
|||||||
appendSuffix = true;
|
appendSuffix = true;
|
||||||
var converter = Converter ?? TypeConverterHelper.ConvertToString;
|
var converter = Converter ?? TypeConverterHelper.ConvertToString;
|
||||||
var defaultValueStyle = DefaultValueStyle?.ToMarkup() ?? "green";
|
var defaultValueStyle = DefaultValueStyle?.ToMarkup() ?? "green";
|
||||||
|
var defaultValue = converter(DefaultValue.Value);
|
||||||
|
|
||||||
builder.AppendFormat(
|
builder.AppendFormat(
|
||||||
CultureInfo.InvariantCulture,
|
CultureInfo.InvariantCulture,
|
||||||
" [{0}]({1})[/]",
|
" [{0}]({1})[/]",
|
||||||
defaultValueStyle,
|
defaultValueStyle,
|
||||||
IsSecret ? "******" : converter(DefaultValue.Value));
|
IsSecret ? defaultValue.Mask(Mask) : defaultValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
var markup = builder.ToString().Trim();
|
var markup = builder.ToString().Trim();
|
||||||
|
@ -288,6 +288,25 @@ public static class TextPromptExtensions
|
|||||||
return obj;
|
return obj;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Replaces prompt user input with mask in the console.
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">The prompt type.</typeparam>
|
||||||
|
/// <param name="obj">The prompt.</param>
|
||||||
|
/// <param name="mask">The masking character to use for the secret.</param>
|
||||||
|
/// <returns>The same instance so that multiple calls can be chained.</returns>
|
||||||
|
public static TextPrompt<T> Secret<T>(this TextPrompt<T> obj, char? mask)
|
||||||
|
{
|
||||||
|
if (obj is null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(obj));
|
||||||
|
}
|
||||||
|
|
||||||
|
obj.IsSecret = true;
|
||||||
|
obj.Mask = mask;
|
||||||
|
return obj;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Sets the function to create a display string for a given choice.
|
/// Sets the function to create a display string for a given choice.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -0,0 +1 @@
|
|||||||
|
Favorite fruit? (------): ------
|
@ -0,0 +1 @@
|
|||||||
|
Favorite fruit? ():
|
@ -233,7 +233,7 @@ public sealed class TextPromptTests
|
|||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
[Expectation("SecretDefaultValue")]
|
[Expectation("SecretDefaultValue")]
|
||||||
public Task Should_Chose_Masked_Default_Value_If_Nothing_Is_Entered_And_Prompt_Is_Secret()
|
public Task Should_Choose_Masked_Default_Value_If_Nothing_Is_Entered_And_Prompt_Is_Secret()
|
||||||
{
|
{
|
||||||
// Given
|
// Given
|
||||||
var console = new TestConsole();
|
var console = new TestConsole();
|
||||||
@ -249,6 +249,42 @@ public sealed class TextPromptTests
|
|||||||
return Verifier.Verify(console.Output);
|
return Verifier.Verify(console.Output);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
[Expectation("SecretDefaultValueCustomMask")]
|
||||||
|
public Task Should_Choose_Custom_Masked_Default_Value_If_Nothing_Is_Entered_And_Prompt_Is_Secret_And_Mask_Is_Custom()
|
||||||
|
{
|
||||||
|
// Given
|
||||||
|
var console = new TestConsole();
|
||||||
|
console.Input.PushKey(ConsoleKey.Enter);
|
||||||
|
|
||||||
|
// When
|
||||||
|
console.Prompt(
|
||||||
|
new TextPrompt<string>("Favorite fruit?")
|
||||||
|
.Secret('-')
|
||||||
|
.DefaultValue("Banana"));
|
||||||
|
|
||||||
|
// Then
|
||||||
|
return Verifier.Verify(console.Output);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
[Expectation("SecretDefaultValueNullMask")]
|
||||||
|
public Task Should_Choose_Empty_Masked_Default_Value_If_Nothing_Is_Entered_And_Prompt_Is_Secret_And_Mask_Is_Null()
|
||||||
|
{
|
||||||
|
// Given
|
||||||
|
var console = new TestConsole();
|
||||||
|
console.Input.PushKey(ConsoleKey.Enter);
|
||||||
|
|
||||||
|
// When
|
||||||
|
console.Prompt(
|
||||||
|
new TextPrompt<string>("Favorite fruit?")
|
||||||
|
.Secret(null)
|
||||||
|
.DefaultValue("Banana"));
|
||||||
|
|
||||||
|
// Then
|
||||||
|
return Verifier.Verify(console.Output);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
[Expectation("NoSuffix")]
|
[Expectation("NoSuffix")]
|
||||||
public Task Should_Not_Append_Questionmark_Or_Colon_If_No_Choices_Are_Set()
|
public Task Should_Not_Append_Questionmark_Or_Colon_If_No_Choices_Are_Set()
|
||||||
|
Loading…
x
Reference in New Issue
Block a user