diff --git a/src/chocolatey.console/Program.cs b/src/chocolatey.console/Program.cs index 5340aef396..73b72ab7f6 100644 --- a/src/chocolatey.console/Program.cs +++ b/src/chocolatey.console/Program.cs @@ -100,13 +100,13 @@ that chocolatey.licensed.dll exists at if (config.RegularOutput) { - "logfile".Log().Info(() => "".PadRight(60, '=')); + "LogFileOnly".Log().Info(() => "".PadRight(60, '=')); #if DEBUG "chocolatey".Log().Info(ChocolateyLoggers.Important, () => "{0} v{1}{2} (DEBUG BUILD)".format_with(ApplicationParameters.Name, config.Information.ChocolateyProductVersion, license.is_licensed_version() ? " {0}".format_with(license.LicenseType) : string.Empty)); #else if (config.Information.ChocolateyVersion == config.Information.ChocolateyProductVersion && args.Any()) { - "logfile".Log().Info(() => "{0} v{1}{2}".format_with(ApplicationParameters.Name, config.Information.ChocolateyProductVersion, license.is_licensed_version() ? " {0}".format_with(license.LicenseType) : string.Empty)); + "LogFileOnly".Log().Info(() => "{0} v{1}{2}".format_with(ApplicationParameters.Name, config.Information.ChocolateyProductVersion, license.is_licensed_version() ? " {0}".format_with(license.LicenseType) : string.Empty)); } else { diff --git a/src/chocolatey.tests/infrastructure/commandline/InteractivePromptSpecs.cs b/src/chocolatey.tests/infrastructure/commandline/InteractivePromptSpecs.cs index 0b3c4eaf66..792c22b8c4 100644 --- a/src/chocolatey.tests/infrastructure/commandline/InteractivePromptSpecs.cs +++ b/src/chocolatey.tests/infrastructure/commandline/InteractivePromptSpecs.cs @@ -437,5 +437,208 @@ public void should_error_when_any_choice_not_available_is_given() console.Verify(c => c.ReadLine(), Times.AtLeast(8)); } } + + public class when_prompting_short_with_interactivePrompt_guard_errors : InteractivePromptSpecsBase + { + private Func prompt; + + public override void Because() + { + console.Setup(c => c.ReadLine()).Returns(""); //Enter pressed + prompt = () => InteractivePrompt.prompt_for_confirmation(prompt_value, choices, defaultChoice: null, requireAnswer:true, shortPrompt: true); + } + + [Fact] + public void should_error_when_the_choicelist_is_null() + { + choices = null; + bool errored = false; + console.Setup(c => c.ReadLine()).Returns(""); //Enter pressed + try + { + prompt(); + } + catch (Exception) + { + errored = true; + } + + errored.ShouldBeTrue(); + console.Verify(c => c.ReadLine(), Times.Never); + } + + [Fact] + public void should_error_when_the_choicelist_is_empty() + { + choices = new List(); + bool errored = false; + string errorMessage = string.Empty; + console.Setup(c => c.ReadLine()).Returns(""); //Enter pressed + try + { + prompt(); + } + catch (Exception ex) + { + errored = true; + errorMessage = ex.Message; + } + + errored.ShouldBeTrue(); + errorMessage.ShouldContain("No choices passed in."); + console.Verify(c => c.ReadLine(), Times.Never); + } + + [Fact] + public void should_error_when_the_prompt_input_is_null() + { + choices = new List { "bob" }; + prompt_value = null; + bool errored = false; + string errorMessage = string.Empty; + console.Setup(c => c.ReadLine()).Returns(""); //Enter pressed + try + { + prompt(); + } + catch (Exception ex) + { + errored = true; + errorMessage = ex.Message; + } + + errored.ShouldBeTrue(); + errorMessage.ShouldContain("Value for prompt cannot be null."); + console.Verify(c => c.ReadLine(), Times.Never); + } + + [Fact] + public void should_error_when_the_choicelist_contains_empty_values() + { + choices = new List { "bob", "" }; + bool errored = false; + string errorMessage = string.Empty; + console.Setup(c => c.ReadLine()).Returns(""); //Enter pressed + try + { + prompt(); + } + catch (Exception ex) + { + errored = true; + errorMessage = ex.Message; + } + + errored.ShouldBeTrue(); + errorMessage.ShouldContain("Some choices are empty."); + console.Verify(c => c.ReadLine(), Times.Never); + } + + [Fact] + public void should_error_when_the_choicelist_has_multiple_items_with_same_first_letter() + { + choices = new List {"sally", "suzy"}; + bool errored = false; + string errorMessage = string.Empty; + console.Setup(c => c.ReadLine()).Returns(""); //Enter pressed + try + { + prompt(); + } + catch (Exception ex) + { + errored = true; + errorMessage = ex.Message; + } + + errored.ShouldBeTrue(); + errorMessage.ShouldContain("Multiple choices have the same first letter."); + console.Verify(c => c.ReadLine(), Times.Never); + } + } + + public class when_prompting_short_with_interactivePrompt : InteractivePromptSpecsBase + { + private Func prompt; + + public override void Because() + { + prompt = () => InteractivePrompt.prompt_for_confirmation(prompt_value, choices, defaultChoice: null, requireAnswer: true, shortPrompt: true); + } + + public override void AfterObservations() + { + base.AfterObservations(); + should_have_called_Console_ReadLine(); + } + + [Fact] + public void should_error_when_no_answer_given() + { + bool errored = false; + + console.Setup(c => c.ReadLine()).Returns(""); //Enter pressed + try + { + prompt(); + } + catch (Exception) + { + errored = true; + } + errored.ShouldBeTrue(); + console.Verify(c => c.ReadLine(), Times.AtLeast(8)); + } + + [Fact] + public void should_return_yes_when_yes_is_given() + { + console.Setup(c => c.ReadLine()).Returns("yes"); + var result = prompt(); + result.ShouldEqual("yes"); + } + + [Fact] + public void should_return_yes_when_y_is_given() + { + console.Setup(c => c.ReadLine()).Returns("y"); + var result = prompt(); + result.ShouldEqual("yes"); + } + + [Fact] + public void should_return_no_choice_when_no_is_given() + { + console.Setup(c => c.ReadLine()).Returns("no"); + var result = prompt(); + result.ShouldEqual("no"); + } + + [Fact] + public void should_return_no_choice_when_n_is_given() + { + console.Setup(c => c.ReadLine()).Returns("n"); + var result = prompt(); + result.ShouldEqual("no"); + } + + [Fact] + public void should_error_when_any_choice_not_available_is_given() + { + bool errored = false; + + console.Setup(c => c.ReadLine()).Returns("yup"); //Enter pressed + try + { + prompt(); + } + catch (Exception) + { + errored = true; + } + errored.ShouldBeTrue(); + console.Verify(c => c.ReadLine(), Times.AtLeast(8)); + } + } } } \ No newline at end of file diff --git a/src/chocolatey/infrastructure.app/runners/GenericRunner.cs b/src/chocolatey/infrastructure.app/runners/GenericRunner.cs index 43f78e6150..7a8c0583a1 100644 --- a/src/chocolatey/infrastructure.app/runners/GenericRunner.cs +++ b/src/chocolatey/infrastructure.app/runners/GenericRunner.cs @@ -189,7 +189,12 @@ require admin rights. Only advanced users should run choco w/out an elevated shell. When you open the command shell, you should ensure that you do so with ""Run as Administrator"" selected. - Do you want to continue?", new[] { "yes", "no" }, defaultChoice: null, requireAnswer: true); + Do you want to continue?", new[] { "yes", "no" }, + defaultChoice: null, + requireAnswer: true, + allowShortAnswer: true, + shortPrompt: true + ); if (selection.is_equal_to("no")) { @@ -199,5 +204,5 @@ require admin rights. Only advanced users should run choco w/out an } } -} - +} + diff --git a/src/chocolatey/infrastructure.app/services/AutomaticUninstallerService.cs b/src/chocolatey/infrastructure.app/services/AutomaticUninstallerService.cs index 941d1559b3..81f3691415 100644 --- a/src/chocolatey/infrastructure.app/services/AutomaticUninstallerService.cs +++ b/src/chocolatey/infrastructure.app/services/AutomaticUninstallerService.cs @@ -143,7 +143,14 @@ public void run(PackageResult packageResult, ChocolateyConfiguration config) var skipUninstaller = true; if (config.PromptForConfirmation) { - var selection = InteractivePrompt.prompt_for_confirmation("Uninstall may not be silent (could not detect). Proceed?", new[] {"yes", "no"}, defaultChoice: null, requireAnswer: true); + var selection = InteractivePrompt.prompt_for_confirmation( + "Uninstall may not be silent (could not detect). Proceed?", + new[] { "yes", "no" }, + defaultChoice: null, + requireAnswer: true, + allowShortAnswer: true, + shortPrompt: true + ); if (selection.is_equal_to("yes")) skipUninstaller = false; } diff --git a/src/chocolatey/infrastructure.app/services/ChocolateyPackageService.cs b/src/chocolatey/infrastructure.app/services/ChocolateyPackageService.cs index a6fd2efa08..978b5269dc 100644 --- a/src/chocolatey/infrastructure.app/services/ChocolateyPackageService.cs +++ b/src/chocolatey/infrastructure.app/services/ChocolateyPackageService.cs @@ -925,7 +925,14 @@ private void rollback_previous_version(ChocolateyConfiguration config, PackageRe var rollback = true; if (config.PromptForConfirmation) { - var selection = InteractivePrompt.prompt_for_confirmation(" Unsuccessful operation for {0}.{1} Do you want to rollback to previous version (package files only)?".format_with(packageResult.Name, Environment.NewLine), new[] { "yes", "no" }, defaultChoice: null, requireAnswer: true); + var selection = InteractivePrompt.prompt_for_confirmation( + " Unsuccessful operation for {0}.{1} Rollback to previous version (package files only)?".format_with(packageResult.Name, Environment.NewLine), + new[] { "yes", "no" }, + defaultChoice: null, + requireAnswer: true, + allowShortAnswer: true, + shortPrompt: true + ); if (selection.is_equal_to("no")) rollback = false; } diff --git a/src/chocolatey/infrastructure.app/services/NugetService.cs b/src/chocolatey/infrastructure.app/services/NugetService.cs index 1c92b3a4f8..45ca8c131c 100644 --- a/src/chocolatey/infrastructure.app/services/NugetService.cs +++ b/src/chocolatey/infrastructure.app/services/NugetService.cs @@ -1060,7 +1060,11 @@ public ConcurrentDictionary uninstall_run(ChocolateyConfi choices.Add(allVersionsChoice); } - var selection = InteractivePrompt.prompt_for_confirmation("Which version of {0} would you like to uninstall?".format_with(packageName), choices, defaultChoice: null, requireAnswer: true); + var selection = InteractivePrompt.prompt_for_confirmation("Which version of {0} would you like to uninstall?".format_with(packageName), + choices, + defaultChoice: null, + requireAnswer: true, + allowShortAnswer: false); if (string.IsNullOrWhiteSpace(selection)) continue; if (selection.is_equal_to(abortChoice)) continue; diff --git a/src/chocolatey/infrastructure.app/services/PowershellService.cs b/src/chocolatey/infrastructure.app/services/PowershellService.cs index 5b54194c00..80308c7a53 100644 --- a/src/chocolatey/infrastructure.app/services/PowershellService.cs +++ b/src/chocolatey/infrastructure.app/services/PowershellService.cs @@ -234,14 +234,26 @@ public bool run_action(ChocolateyConfiguration configuration, PackageResult pack this.Log().Info(ChocolateyLoggers.Important, () => @"Note: To confirm automatically next time, use '-y' or consider setting 'allowGlobalConfirmation'. Run 'choco feature -h' for more details."); - var selection = InteractivePrompt.prompt_for_confirmation(@"Do you want to run the script?", new[] {"yes", "no", "print"}, defaultChoice: null, requireAnswer: true); + var selection = InteractivePrompt.prompt_for_confirmation(@"Do you want to run the script?", + new[] {"yes", "no", "print"}, + defaultChoice : null, + requireAnswer : true, + allowShortAnswer : true, + shortPrompt: true + ); if (selection.is_equal_to("print")) { this.Log().Info(ChocolateyLoggers.Important, "------ BEGIN SCRIPT ------"); this.Log().Info(() => "{0}{1}{0}".format_with(Environment.NewLine, chocoPowerShellScriptContents.escape_curly_braces())); this.Log().Info(ChocolateyLoggers.Important, "------- END SCRIPT -------"); - selection = InteractivePrompt.prompt_for_confirmation(@"Do you want to run this script?", new[] { "yes", "no" }, defaultChoice: null, requireAnswer: true); + selection = InteractivePrompt.prompt_for_confirmation(@"Do you want to run this script?", + new[] { "yes", "no" }, + defaultChoice : null, + requireAnswer : true, + allowShortAnswer : true, + shortPrompt: true + ); } if (selection.is_equal_to("yes")) shouldRun = true; diff --git a/src/chocolatey/infrastructure/adapters/Console.cs b/src/chocolatey/infrastructure/adapters/Console.cs index 6a1ceeeafd..b7af6fc49a 100644 --- a/src/chocolatey/infrastructure/adapters/Console.cs +++ b/src/chocolatey/infrastructure/adapters/Console.cs @@ -48,5 +48,15 @@ public void Write(object value) { System.Console.Write(value.to_string()); } + + public void WriteLine() + { + System.Console.WriteLine(); + } + + public void WriteLine(object value) + { + System.Console.WriteLine(value); + } } } diff --git a/src/chocolatey/infrastructure/adapters/IConsole.cs b/src/chocolatey/infrastructure/adapters/IConsole.cs index a43d493c69..a3e071d8d7 100644 --- a/src/chocolatey/infrastructure/adapters/IConsole.cs +++ b/src/chocolatey/infrastructure/adapters/IConsole.cs @@ -70,6 +70,21 @@ public interface IConsole /// An I/O error occurred. /// 1 void Write(object value); + + /// + /// Writes the current line terminator to the standard output stream. + /// + /// An I/O error occurred. + /// 1 + void WriteLine(); + + /// + /// Writes the text representation of the specified object, followed by the current line terminator, to the standard output stream. + /// + /// The value to write. + /// An I/O error occurred. + /// 1 + void WriteLine(object value); } // ReSharper restore InconsistentNaming diff --git a/src/chocolatey/infrastructure/commandline/InteractivePrompt.cs b/src/chocolatey/infrastructure/commandline/InteractivePrompt.cs index f97c694f47..b137762d82 100644 --- a/src/chocolatey/infrastructure/commandline/InteractivePrompt.cs +++ b/src/chocolatey/infrastructure/commandline/InteractivePrompt.cs @@ -40,7 +40,7 @@ private static IConsole Console get { return _console.Value; } } - public static string prompt_for_confirmation(string prompt, IEnumerable choices, string defaultChoice, bool requireAnswer, int repeat = 10) + public static string prompt_for_confirmation(string prompt, IEnumerable choices, string defaultChoice, bool requireAnswer, bool allowShortAnswer = true, bool shortPrompt = false, int repeat = 10) { if (repeat < 0) throw new ApplicationException("Too many bad attempts. Stopping before application crash."); Ensure.that(() => prompt).is_not_null(); @@ -50,6 +50,7 @@ public static string prompt_for_confirmation(string prompt, IEnumerable .meets( c => c.Count() > 0, (name, value) => { throw new ApplicationException("No choices passed in. Please ensure you pass choices"); }); + if (defaultChoice != null) { Ensure @@ -59,18 +60,57 @@ public static string prompt_for_confirmation(string prompt, IEnumerable (name, value) => { throw new ApplicationException("Default choice value must be one of the given choices."); }); } - "chocolatey".Log().Info(ChocolateyLoggers.Important, prompt); + if (allowShortAnswer) + { + Ensure + .that(() => choices) + .meets( + c => !c.Any(String.IsNullOrWhiteSpace), + (name, value) => { throw new ApplicationException("Some choices are empty. Please ensure you provide no empty choices."); }); + + Ensure + .that(() => choices) + .meets( + c => c.Select(entry => entry.FirstOrDefault()).Distinct().Count() == c.Count(), + (name, value) => { throw new ApplicationException("Multiple choices have the same first letter. Please ensure you pass choices with different first letters."); }); + } + + if (shortPrompt) + { + Console.Write(prompt + "("); + + } + + "chocolatey".Log().Info(shortPrompt ? ChocolateyLoggers.LogFileOnly : ChocolateyLoggers.Important, prompt); int counter = 1; IDictionary choiceDictionary = new Dictionary(); foreach (var choice in choices.or_empty_list_if_null()) { choiceDictionary.Add(counter, choice); - "chocolatey".Log().Info(" {0}) {1}{2}".format_with(counter, choice.to_string(), choice == defaultChoice ? " [Default - Press Enter]" : "")); + "chocolatey".Log().Info(shortPrompt ? ChocolateyLoggers.LogFileOnly : ChocolateyLoggers.Normal," {0}) {1}{2}".format_with(counter, choice.to_string(), choice.is_equal_to(defaultChoice) ? " [Default - Press Enter]" : "")); + if (shortPrompt) + { + var choicePrompt = choice.is_equal_to(defaultChoice) ? + shortPrompt ? + "[[{0}]{1}]".format_with(choice.Substring(0, 1).ToUpperInvariant(), choice.Substring(1, choice.Length - 1)) : + "[{0}]".format_with(choice.ToUpperInvariant()) + : + shortPrompt ? + "[{0}]{1}".format_with(choice.Substring(0,1).ToUpperInvariant(), choice.Substring(1, choice.Length - 1)) : + choice; + + if (counter != 1) Console.Write("/"); + Console.Write(choicePrompt); + } + counter++; } + Console.Write(shortPrompt ? "): " : "> "); + var selection = Console.ReadLine(); + if (shortPrompt) Console.WriteLine(); if (string.IsNullOrWhiteSpace(selection) && defaultChoice != null) { @@ -78,17 +118,16 @@ public static string prompt_for_confirmation(string prompt, IEnumerable } int selected = -1; - if (!int.TryParse(selection, out selected) || selected <= 0 || selected > (counter - 1)) { // check to see if value was passed var selectionFound = false; foreach (var pair in choiceDictionary) { - if (pair.Value.is_equal_to(selection)) + if (pair.Value.is_equal_to(selection) || (allowShortAnswer && pair.Value.Substring(0, 1).is_equal_to(selection))) { selected = pair.Key; - selectionFound = true; + selectionFound = true; break; } } @@ -99,7 +138,7 @@ public static string prompt_for_confirmation(string prompt, IEnumerable if (requireAnswer) { "chocolatey".Log().Warn(ChocolateyLoggers.Important, "You must select an answer"); - return prompt_for_confirmation(prompt, choices, defaultChoice, requireAnswer, repeat - 1); + return prompt_for_confirmation(prompt, choices, defaultChoice, requireAnswer, allowShortAnswer, shortPrompt, repeat - 1); } return null; } diff --git a/src/chocolatey/infrastructure/logging/ChocolateyLoggers.cs b/src/chocolatey/infrastructure/logging/ChocolateyLoggers.cs index dbd59b4de6..6e7deffde9 100644 --- a/src/chocolatey/infrastructure/logging/ChocolateyLoggers.cs +++ b/src/chocolatey/infrastructure/logging/ChocolateyLoggers.cs @@ -20,5 +20,7 @@ public enum ChocolateyLoggers Normal, Verbose, Important, + // Used to output prompt results in log file, but not in the console + LogFileOnly, } } \ No newline at end of file diff --git a/src/chocolatey/infrastructure/logging/log4net.config.xml b/src/chocolatey/infrastructure/logging/log4net.config.xml index 223ddca318..4f71ea826e 100644 --- a/src/chocolatey/infrastructure/logging/log4net.config.xml +++ b/src/chocolatey/infrastructure/logging/log4net.config.xml @@ -127,7 +127,7 @@ - + diff --git a/src/chocolatey/infrastructure/logging/log4net.mono.config.xml b/src/chocolatey/infrastructure/logging/log4net.mono.config.xml index 7f74bf57e4..0b2e34b880 100644 --- a/src/chocolatey/infrastructure/logging/log4net.mono.config.xml +++ b/src/chocolatey/infrastructure/logging/log4net.mono.config.xml @@ -112,7 +112,7 @@ - +