using System; using System.Collections.Generic; using System.Text.RegularExpressions; using System.Threading; using System.Timers; namespace RobvanderWoude { class SecondChoice { static string progver = "1.02"; #region Global Variables static bool cancel = false; static int rc = 0; static string choices = "YN"; static string defaultchoice = String.Empty; #endregion Global Variables static int Main( string[] args ) { #region Capture Control+Break // Code to gracefully exit on Ctrl+Break by Alois Kraus // http://www.codeproject.com/Articles/16164/Managed-Application-Shutdown // // Note that this will NOT prevent passing on the Ctrl+Break to the hosting process, the // only purpose for this code is to make this program exit wit errorlevel 0 on Ctrl+Break Console.CancelKeyPress += delegate( object sender, ConsoleCancelEventArgs e ) { if ( e.SpecialKey == ConsoleSpecialKey.ControlBreak ) { e.Cancel = true; new Thread( delegate( ) { Environment.Exit( 0 ); } ).Start( ); } }; #endregion Capture Control+Break #region Local variables bool casesensitive = false; bool hidechoices = false; bool usedefault = false; bool usetimeout = false; bool waitingforchoices = false; bool waitingfordefault = false; bool waitingformessage = false; bool waitingfortimeout = false; string message = String.Empty; int timeout = -1; #endregion Local variables #region Command Line Parsing foreach ( string arg in args ) { if ( arg == "/?" ) { return ShowHelp( ); } } if ( args.Length > 10 ) { return ShowHelp( "Too many command line arguments" ); } foreach ( string arg in args ) { switch ( arg.ToUpper( ) ) { // Check for command line switches separates by spaces case "/C": if ( choices.ToUpper( ) != "YN" || waitingforchoices ) { return ShowHelp( "Duplicate command line switch /C" ); } if ( waitingfordefault ) { return ShowHelp( "/D is used, but no default choice is specified" ); } if ( waitingformessage ) { return ShowHelp( "/M is used, but no message is specified" ); } if ( waitingfortimeout ) { return ShowHelp( "/T is used, but no timeout is specified" ); } waitingforchoices = true; break; case "/CS": if ( casesensitive ) { return ShowHelp( "Duplicate command line switch /CS" ); } casesensitive = true; break; case "/D": if ( usedefault || waitingfordefault ) { return ShowHelp( "Duplicate command line switch /D" ); } if ( waitingforchoices ) { return ShowHelp( "/C is used, but no choices are specified" ); } if ( waitingformessage ) { return ShowHelp( "/M is used, but no message is specified" ); } if ( waitingfortimeout ) { return ShowHelp( "/T is used, but no timeout is specified" ); } usedefault = true; waitingfordefault = true; break; case "/M": if ( waitingformessage ) { return ShowHelp( "Duplicate command line switch /D" ); } if ( waitingforchoices ) { return ShowHelp( "/C is used, but no choices are specified" ); } if ( waitingfordefault ) { return ShowHelp( "/D is used, but no default choice is specified" ); } if ( waitingfortimeout ) { return ShowHelp( "/T is used, but no timeout is specified" ); } waitingformessage = true; break; case "/N": if ( hidechoices ) { return ShowHelp( "Duplicate command line switch /N" ); } hidechoices = true; break; case "/T": if ( usetimeout || waitingfortimeout ) { return ShowHelp( "Duplicate command line switch /T" ); } if ( waitingforchoices ) { return ShowHelp( "/C is used, but no choices are specified" ); } if ( waitingfordefault ) { return ShowHelp( "/D is used, but no default choice is specified" ); } if ( waitingformessage ) { return ShowHelp( "/M is used, but no message is specified" ); } usetimeout = true; waitingfortimeout = true; break; default: // Check for values belonging to a previous switch if ( waitingforchoices ) { choices = arg; waitingforchoices = false; } else if ( waitingfordefault ) { defaultchoice = arg; waitingfordefault = false; } else if ( waitingformessage ) { message = arg; waitingformessage = false; } else if ( waitingfortimeout ) { try { timeout = Convert.ToInt32( arg ); waitingfortimeout = false; } catch ( Exception ) { return ShowHelp( "Invalid timeout value \"{0}\"", arg ); } } else { // Check for arguments with values immediately appended, with colon or equal sign as separator if ( arg.Length > 3 && arg[0] == '/' && "CDMNTcdmnt".IndexOf( arg[1] ) != -1 && ( arg[2] == ':' || arg[2] == '=' ) ) { switch ( arg.ToUpper( ).Substring( 0, 2 ) ) { case "/C": if ( choices.ToUpper( ) != "YN" || waitingforchoices ) { return ShowHelp( "Duplicate command line switch /C" ); } choices = arg.Substring( 3 ); break; case "/D": if ( usedefault || waitingfordefault ) { return ShowHelp( "Duplicate command line switch /D" ); } usedefault = true; defaultchoice = arg.Substring( 3 ); break; case "/M": if ( waitingformessage ) { return ShowHelp( "Duplicate command line switch /D" ); } message = arg.Substring( 3 ); break; case "/T": if ( usetimeout || waitingfortimeout ) { return ShowHelp( "Duplicate command line switch /T" ); } try { usetimeout = true; timeout = Convert.ToInt32( arg.Substring( 3 ) ); } catch ( Exception ) { return ShowHelp( "Invalid timeout value \"{0}\"", arg ); } break; } } else { return ShowHelp( "Invalid command line argument \"{0}\"", arg ); } } break; } } #endregion Command Line Parsing #region Command Line Validation // Timeout requires a default value if ( usetimeout && !usedefault ) { return ShowHelp( "If timeout is specified, default value must be specified too" ); } // If a default is specified, it must be a single character, found in the list of choices if ( usedefault && ( defaultchoice.Length != 1 || choices.IndexOfAny( defaultchoice.ToCharArray( ) ) == -1 ) ) { return ShowHelp( "Default specified by /D must be a single character\n\tfrom the list of choices specified by /C" ); } // Less than 1 choice is useless if ( choices.Length < 2 ) { return ShowHelp( "Specify at least 2 choices" ); } // Check for duplicates in list of choices Regex regex; string pattern = @"([\w ]).*\1"; if ( casesensitive ) { regex = new Regex( pattern ); } else { regex = new Regex( pattern, RegexOptions.IgnoreCase ); } if ( regex.IsMatch( choices ) ) { return ShowHelp( "All choices specified by /C must be unique" ); } if ( !casesensitive ) { if ( usedefault ) { // Show the list of choices in lower case, with the default choice in upper case choices = choices.ToLower( ).Replace( defaultchoice.ToLower( ), defaultchoice.ToUpper( ) ); } else { // Show the list of choices in upper case choices = choices.ToUpper( ); } } if ( usetimeout && timeout > 0 ) { // Check if specified timeout is in accepted range if ( timeout < 0 || timeout > 86400 ) { return ShowHelp( "Timeout must be in range 0..86400 seconds (0..24h)" ); } System.Timers.Timer timer = new System.Timers.Timer( ); timer.Elapsed += new ElapsedEventHandler( timer_Elapsed ); timer.Interval = timeout * 1000; timer.Start( ); } #endregion Command Line Validation #region Show Prompt string prompt = String.Empty; if ( hidechoices ) { if ( !String.IsNullOrWhiteSpace( message ) ) { prompt = String.Format( "{0}: ", message ); } } else { prompt = String.Format( "{0} [{1}]: ", message, choices ).TrimStart( ); } #endregion Show Prompt #region Handle Response Console.Write( prompt ); // If timeout is zero, return default immediately if ( usetimeout && timeout == 0 ) { Console.WriteLine( defaultchoice ); rc = choices.IndexOf( defaultchoice[0] ) + 1; return rc; } // Tell console to capture Ctrl+C as input Console.TreatControlCAsInput = true; // Wait for response ConsoleKeyInfo response; while ( !cancel ) { response = Console.ReadKey( true ); string key = response.KeyChar.ToString( ); bool ctrlkey = ( ( ( response.Modifiers & ConsoleModifiers.Control ) != 0 ) && ( ( response.Modifiers & ConsoleModifiers.Alt ) == 0 ) && ( ( response.Modifiers & ConsoleModifiers.Shift ) == 0 ) ); bool shiftkey = ( ( ( response.Modifiers & ConsoleModifiers.Shift ) != 0 ) && ( ( response.Modifiers & ConsoleModifiers.Alt ) == 0 ) && ( ( response.Modifiers & ConsoleModifiers.Control ) == 0 ) ); bool nomodifiers = ( response.Modifiers == 0 ); bool escapekey = ( ( response.Key == ConsoleKey.Escape ) && nomodifiers ); bool enterkey = ( ( response.Key == ConsoleKey.Enter ) && nomodifiers ); // Ctrl+C if ( ctrlkey && ( key == ( (char) 3 ).ToString( ) ) ) { rc = 0; cancel = true; break; } // Escape if ( escapekey ) { rc = 0; cancel = true; break; } // Case sensitive? if ( shiftkey || !casesensitive ) { key = key.ToUpper( ); } // Default on Enter if ( enterkey && usedefault ) { rc = choices.IndexOf( defaultchoice[0] ) + 1; if ( rc > 0 ) { Console.WriteLine( defaultchoice ); break; } } // Check if valid key pressed rc = choices.IndexOf( key[0] ) + 1; if ( rc > 0 ) { Console.WriteLine( key ); break; } } #endregion Handle Response return rc; } static int ShowHelp( params string[] errmsg ) { #region Help Text /* SecondChoice.exe, Version 1.02 Alternative to Windows' native CHOICE command. Like CHOICE, SecondChoice allows users to select one item from a list of choices and returns the index of the selected choice. Unlike CHOICE, SecondChoice also returns the default value on pressing the Enter key (besides after timeout expiration). And unlike CHOICE, SecondChoice accepts a space in the list of choices if that list is in doublequotes. Usage: SECONDCHOICE [ options ] Options: /C choices Specifies the list of Choices to be created. Default list is "YN". /CS Enables Case-Sensitive choices to be selected. By default, the utility is case-insensitive. /D default Specifies the Default choice after the timeout expires or after pressing Enter. The default character must be in the list of choices specified by /C option. /M message Specifies the Message to be displayed before the prompt. If not specified, the utility displays only a prompt. /N Hides the list of choices in the prompt. The message before the prompt is displayed and the choices are still enabled. /T timeout The number of seconds to pause before a default choice is made. Acceptable values are from 0 to 86400 (24 hours). If 0 is specified, there will be no pause and the default choice is selected. Notes: The ERRORLEVEL environment variable is set to the index of the key that was selected from the set of choices. The first choice listed returns a value of 1, the second a value of 2, and so on. Invalid choices are ignored. If SecondChoice detects an error, it returns ERRORLEVEL 255, if the user presses ESC, CTRL+C or CTRL+BREAK, the ERRORLEVEL will be 0. Pressing CTRL+BREAK will also be passed on to the hosting process. Besides spaces between switches and values, colons or equal signs are accepted too, e.g. /C choices or /C:choices or /C=choices are all accepted. SecondChoice also accepts a space as a valid choice; make sure you place the list of choices in doublequotes if you want to use a space in the list of choices, e.g. /C " ABC" or /C:" ABC" or /C=" ABC". Examples: SECONDCHOICE /C YNC /M "Press Y for Yes, N for No or C for Cancel." SECONDCHOICE /T=10 /C=YNC /D=C /M="Y=yes, N=no, C or timeout=cancel" SECONDCHOICE /C YN /N /D Y /M "Press Enter or Y for Yes, or N for No" SECONDCHOICE /C:AB /M:"Select A for option 1 and B for option 2." SECONDCHOICE /C 12 /N /M "Select 1 for option 1 and 2 for option 2." Credits: Code to gracefully exit on Ctrl+Break by Alois Kraus http://codeproject.com/Articles/16164/Managed-Application-Shutdown Written by Rob van der Woude http://www.robvanderwoude.com */ if ( errmsg.Length > 0 ) { List errargs = new List( errmsg ); errargs.RemoveAt( 0 ); Console.Error.WriteLine( ); Console.ForegroundColor = ConsoleColor.Red; Console.Error.Write( "ERROR:\t" ); Console.ForegroundColor = ConsoleColor.White; Console.Error.WriteLine( errmsg[0], errargs.ToArray( ) ); Console.ResetColor( ); } Console.Error.WriteLine( ); Console.Error.WriteLine( "SecondChoice.exe, Version {0}", progver ); Console.Error.WriteLine( "Alternative to Windows' native CHOICE command." ); Console.Error.WriteLine( "Like CHOICE, SecondChoice allows users to select one item from" ); Console.Error.WriteLine( "a list of choices and returns the index of the selected choice." ); Console.Error.WriteLine( "Unlike CHOICE, SecondChoice also returns the default value on" ); Console.Error.WriteLine( "pressing the Enter key (besides after timeout expiration)." ); Console.Error.WriteLine( "And unlike CHOICE, SecondChoice accepts a space in the list of" ); Console.Error.WriteLine( "choices if that list is in doublequotes." ); Console.Error.WriteLine( ); Console.Error.Write( "Usage: " ); Console.ForegroundColor = ConsoleColor.White; Console.Error.WriteLine( "SECONDCHOICE [ options ]" ); Console.ResetColor( ); Console.Error.WriteLine( ); Console.Error.Write( "Options: " ); Console.ForegroundColor = ConsoleColor.White; Console.Error.Write( "/C choices" ); Console.ResetColor( ); Console.Error.Write( " Specifies the list of " ); Console.ResetColor( ); Console.ForegroundColor = ConsoleColor.White; Console.Error.Write( "C" ); Console.ResetColor( ); Console.Error.WriteLine( "hoices to be created." ); Console.Error.WriteLine( " Default list is \"YN\"." ); Console.ForegroundColor = ConsoleColor.White; Console.Error.Write( " /CS" ); Console.ResetColor( ); Console.Error.Write( " Enables " ); Console.ForegroundColor = ConsoleColor.White; Console.Error.Write( "C" ); Console.ResetColor( ); Console.Error.Write( "ase-" ); Console.ForegroundColor = ConsoleColor.White; Console.Error.Write( "S" ); Console.ResetColor( ); Console.Error.WriteLine( "ensitive choices to be selected." ); Console.Error.WriteLine( " By default, the utility is case-insensitive." ); Console.ForegroundColor = ConsoleColor.White; Console.Error.Write( " /D default" ); Console.ResetColor( ); Console.Error.Write( " Specifies the " ); Console.ForegroundColor = ConsoleColor.White; Console.Error.Write( "D" ); Console.ResetColor( ); Console.Error.WriteLine( "efault choice after the timeout" ); Console.Error.WriteLine( " expires or after pressing Enter." ); Console.Error.WriteLine( " The default character must be in the list of" ); Console.Error.Write( " choices specified by " ); Console.ForegroundColor = ConsoleColor.White; Console.Error.Write( "/C" ); Console.ResetColor( ); Console.Error.WriteLine( " option." ); Console.ForegroundColor = ConsoleColor.White; Console.Error.Write( " /M message" ); Console.ResetColor( ); Console.Error.Write( " Specifies the " ); Console.ForegroundColor = ConsoleColor.White; Console.Error.Write( "M" ); Console.ResetColor( ); Console.Error.WriteLine( "essage to be displayed before" ); Console.Error.WriteLine( " the prompt. If not specified, the utility" ); Console.Error.WriteLine( " displays only a prompt." ); Console.ForegroundColor = ConsoleColor.White; Console.Error.Write( " /N" ); Console.ResetColor( ); Console.Error.WriteLine( " Hides the list of choices in the prompt." ); Console.Error.WriteLine( " The message before the prompt is displayed" ); Console.Error.WriteLine( " and the choices are still enabled." ); Console.ForegroundColor = ConsoleColor.White; Console.Error.Write( " /T timeout" ); Console.ResetColor( ); Console.Error.WriteLine( " The number of seconds to pause before a default" ); Console.Error.WriteLine( " choice is made. Acceptable values are from 0 to" ); Console.Error.WriteLine( " 86400 (24 hours). If 0 is specified, there will" ); Console.Error.WriteLine( " be no pause and the default choice is selected." ); Console.Error.WriteLine( ); Console.Error.WriteLine( "Notes: The ERRORLEVEL environment variable is set to the index of the" ); Console.Error.WriteLine( " key that was selected from the set of choices. The first choice" ); Console.Error.WriteLine( " listed returns a value of 1, the second a value of 2, and so on." ); Console.Error.WriteLine( " Invalid choices are ignored." ); Console.Error.WriteLine( " If SecondChoice detects an error, it returns ERRORLEVEL 255, if the" ); Console.Error.WriteLine( " user presses ESC, CTRL+C or CTRL+BREAK, the ERRORLEVEL will be 0." ); Console.Error.WriteLine( " Pressing CTRL+BREAK will also be passed on to the hosting process." ); Console.Error.WriteLine( " Besides spaces between switches and values, colons or equal signs" ); Console.Error.Write( " are accepted too, e.g. " ); Console.ForegroundColor = ConsoleColor.White; Console.Error.Write( "/C choices" ); Console.ResetColor( ); Console.Error.Write( " or " ); Console.ForegroundColor = ConsoleColor.White; Console.Error.Write( "/C:choices" ); Console.ResetColor( ); Console.Error.Write( " or " ); Console.ForegroundColor = ConsoleColor.White; Console.Error.Write( "/C=choices" ); Console.ResetColor( ); Console.Error.WriteLine( " are" ); Console.Error.WriteLine( " all accepted." ); Console.Error.WriteLine( " SecondChoice also accepts a space as a valid choice; make sure you" ); Console.Error.WriteLine( " place the list of choices in doublequotes if you want to use a space" ); Console.Error.Write( " in the list of choices, e.g. " ); Console.ForegroundColor = ConsoleColor.White; Console.Error.Write( "/C \" ABC\"" ); Console.ResetColor( ); Console.Error.Write( " or " ); Console.ForegroundColor = ConsoleColor.White; Console.Error.Write( "/C:\" ABC\"" ); Console.ResetColor( ); Console.Error.Write( " or " ); Console.ForegroundColor = ConsoleColor.White; Console.Error.Write( "/C=\" ABC\"" ); Console.ResetColor( ); Console.Error.WriteLine( "." ); Console.Error.WriteLine( ); Console.Error.Write( "Examples: " ); Console.ForegroundColor = ConsoleColor.White; Console.Error.WriteLine( "SECONDCHOICE /C YNC /M \"Press Y for Yes, N for No or C for Cancel.\"" ); Console.Error.WriteLine( " SECONDCHOICE /T=10 /C=YNC /D=C /M=\"Y=yes, N=no, C or timeout=cancel\"" ); Console.Error.WriteLine( " SECONDCHOICE /C YN /N /D Y /M \"Press Enter or Y for Yes, or N for No\"" ); Console.Error.WriteLine( " SECONDCHOICE /C:AB /M:\"Select A for option 1 and B for option 2.\"" ); Console.Error.WriteLine( " SECONDCHOICE /C 12 /N /M \"Select 1 for option 1 and 2 for option 2.\"" ); Console.ResetColor( ); Console.Error.WriteLine( ); Console.Error.WriteLine( "Credits: Code to gracefully exit on Ctrl+Break by Alois Kraus" ); Console.ForegroundColor = ConsoleColor.DarkGray; Console.Error.WriteLine( " http://codeproject.com/Articles/16164/Managed-Application-Shutdown" ); Console.ResetColor( ); Console.Error.WriteLine( ); Console.Error.WriteLine( "Written by Rob van der Woude" ); Console.Error.WriteLine( "http://www.robvanderwoude.com" ); #endregion Help Text return 255; } #region Event Handlers public static void timer_Elapsed( object sender, System.EventArgs e ) { Console.WriteLine( defaultchoice ); rc = choices.IndexOf( defaultchoice ) + 1; Environment.Exit( rc ); } #endregion Event Handlers } }