Merge pull request #858 from PowerShell/options

Re-write host code command parser engine to support both Linux and Windows style options.  Also, add support for -noninteractive and -encodedcommand.
This commit is contained in:
Andy Schwartzmeyer
2016-04-18 13:00:33 -07:00
3 changed files with 280 additions and 170 deletions
@@ -16,7 +16,7 @@ namespace Microsoft.PowerShell.CoreConsoleHost
public MyHost(Listener Listener)
{
this.Listener = Listener;
this.myHostUserInterface = new MyHostUserInterface(Listener.HasUI);
this.myHostUserInterface = new MyHostUserInterface(Listener.HasUI, Listener.Interactive);
}
/// <summary>
+126 -42
View File
@@ -14,6 +14,30 @@ namespace Microsoft.PowerShell.CoreConsoleHost
public static class Program
{
private enum Options {Help, File, Command, NoProfile, NonInteractive, EncodedCommand, Invalid};
private class CommandOptions
{
public Options opt;
public string optString;
public CommandOptions(Options o, string s)
{
opt = o;
optString = s;
}
}
private static CommandOptions[] commandOptions = new CommandOptions[]
{
new CommandOptions(Options.Help, "help"),
new CommandOptions(Options.Help, "?"),
new CommandOptions(Options.File, "file"),
new CommandOptions(Options.Command, "command"),
new CommandOptions(Options.NoProfile, "noprofile"),
new CommandOptions(Options.NonInteractive, "noninteractive"),
new CommandOptions(Options.EncodedCommand, "encodedcommand")
};
/// <summary>
/// Creates and initiates the listener instance.
/// </summary>
@@ -25,9 +49,11 @@ namespace Microsoft.PowerShell.CoreConsoleHost
// here eliminates the need for a custom native host.
PowerShellAssemblyLoadContextInitializer.SetPowerShellAssemblyLoadContext(string.Empty);
// Argument parsing
string initialScript = null;
bool loadProfiles = true;
bool interactive = true;
if (args.Length > 0)
{
@@ -37,15 +63,33 @@ namespace Microsoft.PowerShell.CoreConsoleHost
bool hasNext = (i+1) < args.Length;
string nextArg = hasNext ? args[i+1] : string.Empty;
// --help was specified
if (arg == "--help" || arg == "-h")
// options start with "-" or "--"
string option;
if (arg.StartsWith("-"))
{
Console.WriteLine(@"
usage: powershell[.exe] [ (--help | -h) ]
if (arg.StartsWith("--"))
{
option = arg.Remove(0, 2);
}
else
{
option = arg.Remove(0, 1);
}
Options bestOption = FindOption(option);
switch (bestOption)
{
case Options.Help:
Console.WriteLine(@"
usage: powershell[.exe] [ (--help | -h | -?) ]
[ (--file | -f) <filePath> ]
[ <script>.ps1 ]
[ (--command | -c) <string> ]
[ --noprofile ]
[ --noprofile | -nop ]
[ --noninteractive | -non ]
[ (--encodedcommand | -e) <base64Command> ]
SYNOPSIS
@@ -65,46 +109,63 @@ OPTIONS
(--command | -c) <string>
Will execute given string as a PowerShell script.
--noprofile
(--noprofile | -nop)
Disables parsing of PowerShell profiles.
(--help | -h)
(--help | -h | -?)
Prints this text.
(--noninteractive | -non)
Does not present an interactive prompt to the user.
(--encodedcommand | -e) <base64Command>
Accepts a base-64-encoded string version of a command.
");
return;
}
else if (arg == "--noprofile")
{
loadProfiles = false;
}
// lone argument is a script
else if (!hasNext && arg.EndsWith(".ps1"))
{
initialScript = Path.GetFullPath(arg);
}
// lone argument is an inline script
else if (!hasNext)
{
initialScript = arg;
}
// --file <filePath> was specified
else if (hasNext && (arg == "--file" || arg == "-f"))
{
initialScript = Path.GetFullPath(nextArg);
++i;
}
// --command <string> was specified
else if (hasNext && (arg == "--command" || arg == "-c"))
{
if (nextArg == "-")
{
initialScript = "\"TODO: read stdin using Console.OpenStandardInput\"";
return;
case Options.NoProfile:
loadProfiles = false;
break;
case Options.File:
initialScript = Path.GetFullPath(nextArg);
++i;
break;
case Options.Command:
if (nextArg == "-")
{
initialScript = "\"TODO: read stdin using Console.OpenStandardInput\"";
}
else
{
initialScript = nextArg;
}
++i;
break;
case Options.NonInteractive:
interactive = false;
break;
case Options.EncodedCommand:
byte[] data = Convert.FromBase64String(nextArg);
initialScript = Encoding.UTF8.GetString(data);
++i;
break;
default:
Console.WriteLine("Invalid command-line option: {0}. Use '-help' option to get a list of valid options.", arg);
return;
}
else
}
else // not an option parameter
{
// lone argument is a script
if (!hasNext && arg.EndsWith(".ps1"))
{
initialScript = nextArg;
initialScript = Path.GetFullPath(arg);
}
// lone argument is an inline script
else if (!hasNext)
{
initialScript = arg;
}
++i;
}
}
}
@@ -114,7 +175,7 @@ OPTIONS
ConsoleColor InitialBackgroundColor = Console.BackgroundColor;
// Create the listener and run it
Listener listener = new Listener(initialScript, loadProfiles);
Listener listener = new Listener(initialScript, loadProfiles, interactive);
// only run if there was no script file passed in
if (initialScript == null)
@@ -129,6 +190,23 @@ OPTIONS
// The exit code is set in the host by the MyHost.SetShouldExit() method.
System.Environment.Exit(listener.ExitCode);
}
private static Options FindOption(string option)
{
int matches = 0;
int index = -1;
for (int i=0; i<commandOptions.Length; ++i)
{
if (commandOptions[i].optString.StartsWith(option, StringComparison.OrdinalIgnoreCase))
{
matches++;
index = i;
}
}
return (matches == 1) ? commandOptions[index].opt : Options.Invalid;
}
}
internal class Listener
@@ -205,13 +283,19 @@ OPTIONS
}
/// <summary>
/// Gets or sets a value indicating whether UI should exit.
/// Gets or sets a value indicating whether UI exists (display prompt)
/// </summary>
public bool HasUI;
public Listener(string initialScript, bool loadProfiles)
/// <summary>
/// Gets or sets a value indicating whether session is interactive (allow popup)
/// </summary>
public bool Interactive;
public Listener(string initialScript, bool loadProfiles, bool interactive)
{
HasUI = (initialScript == null) ? true : false;
this.HasUI = (initialScript == null) ? true : false;
this.Interactive = interactive;
// Create the host and runspace instances for this interpreter.
// Note that this application does not support console files so
+153 -127
View File
@@ -20,7 +20,7 @@ namespace Microsoft.PowerShell.CoreConsoleHost
/// <summary>
/// Public constructor
/// </summary>
public MyHostUserInterface(bool hasUI)
public MyHostUserInterface(bool hasUI, bool interactive)
{
myRawUi = (hasUI) ? new MyRawUserInterface() : null;
}
@@ -39,6 +39,11 @@ namespace Microsoft.PowerShell.CoreConsoleHost
get { return this.myRawUi; }
}
/// <summary>
/// Whether user pop-ups are allowed
/// </summary>
public bool Interactive;
/// <summary>
/// Prompts the user for input.
/// <param name="caption">The caption or title of the prompt.</param>
@@ -52,26 +57,33 @@ namespace Microsoft.PowerShell.CoreConsoleHost
string message,
Collection<FieldDescription> descriptions)
{
this.Write(
ConsoleColor.White,
Console.BackgroundColor,
caption + System.Environment.NewLine + message + " ");
Dictionary<string, PSObject> results =
new Dictionary<string, PSObject>();
foreach (FieldDescription fd in descriptions)
if (Interactive)
{
string[] label = GetHotkeyAndLabel(fd.Label);
this.WriteLine(label[1]);
string userData = Console.ReadLine();
if (userData == null)
this.Write(
ConsoleColor.White,
Console.BackgroundColor,
caption + System.Environment.NewLine + message + " ");
Dictionary<string, PSObject> results =
new Dictionary<string, PSObject>();
foreach (FieldDescription fd in descriptions)
{
return null;
string[] label = GetHotkeyAndLabel(fd.Label);
this.WriteLine(label[1]);
string userData = Console.ReadLine();
if (userData == null)
{
return null;
}
results[fd.Name] = PSObject.AsPSObject(userData);
}
results[fd.Name] = PSObject.AsPSObject(userData);
return results;
}
else
{
throw new PSInvalidOperationException("Cannot prompt user when invoked with --noninteractive option.");
}
return results;
}
/// <summary>
@@ -93,57 +105,64 @@ namespace Microsoft.PowerShell.CoreConsoleHost
Collection<ChoiceDescription> choices,
int defaultChoice)
{
// Write the caption and message strings in Blue.
this.WriteLine(
ConsoleColor.Blue,
Console.BackgroundColor,
caption + System.Environment.NewLine + message + System.Environment.NewLine);
// Convert the choice collection into something that is
// easier to work with. See the BuildHotkeysAndPlainLabels
// method for details.
string[,] promptData = BuildHotkeysAndPlainLabels(choices);
// Format the overall choice prompt string to display.
StringBuilder sb = new StringBuilder();
for (int element = 0; element < choices.Count; element++)
if (Interactive)
{
// Write the caption and message strings in Blue.
this.WriteLine(
ConsoleColor.Blue,
Console.BackgroundColor,
caption + System.Environment.NewLine + message + System.Environment.NewLine);
// Convert the choice collection into something that is
// easier to work with. See the BuildHotkeysAndPlainLabels
// method for details.
string[,] promptData = BuildHotkeysAndPlainLabels(choices);
// Format the overall choice prompt string to display.
StringBuilder sb = new StringBuilder();
for (int element = 0; element < choices.Count; element++)
{
sb.Append(String.Format(
CultureInfo.CurrentCulture,
"|{0}> {1} ",
promptData[0, element],
promptData[1, element]));
}
sb.Append(String.Format(
CultureInfo.CurrentCulture,
"|{0}> {1} ",
promptData[0, element],
promptData[1, element]));
}
CultureInfo.CurrentCulture,
"[Default is ({0}]",
promptData[0, defaultChoice]));
sb.Append(String.Format(
CultureInfo.CurrentCulture,
"[Default is ({0}]",
promptData[0, defaultChoice]));
// Read prompts until a match is made, the default is
// chosen, or the loop is interrupted with ctrl-C.
while (true)
{
this.WriteLine(ConsoleColor.Cyan, Console.BackgroundColor, sb.ToString());
string data = Console.ReadLine().Trim().ToUpper();
// If the choice string was empty, use the default selection.
if (data.Length == 0)
// Read prompts until a match is made, the default is
// chosen, or the loop is interrupted with ctrl-C.
while (true)
{
return defaultChoice;
}
this.WriteLine(ConsoleColor.Cyan, Console.BackgroundColor, sb.ToString());
string data = Console.ReadLine().Trim().ToUpper();
// See if the selection matched and return the
// corresponding index if it did.
for (int i = 0; i < choices.Count; i++)
{
if (promptData[0, i] == data)
// If the choice string was empty, use the default selection.
if (data.Length == 0)
{
return i;
return defaultChoice;
}
}
this.WriteErrorLine("Invalid choice: " + data);
// See if the selection matched and return the
// corresponding index if it did.
for (int i = 0; i < choices.Count; i++)
{
if (promptData[0, i] == data)
{
return i;
}
}
this.WriteErrorLine("Invalid choice: " + data);
}
}
else
{
throw new PSInvalidOperationException("Cannot prompt user when invoked with --noninteractive option.");
}
}
@@ -167,87 +186,94 @@ namespace Microsoft.PowerShell.CoreConsoleHost
Collection<ChoiceDescription> choices,
IEnumerable<int> defaultChoices)
{
// Write the caption and message strings in Blue.
this.WriteLine(
ConsoleColor.Blue,
Console.BackgroundColor,
caption + System.Environment.NewLine + message + System.Environment.NewLine);
// Convert the choice collection into something that is
// easier to work with. See the BuildHotkeysAndPlainLabels
// method for details.
string[,] promptData = BuildHotkeysAndPlainLabels(choices);
// Format the overall choice prompt string to display.
StringBuilder sb = new StringBuilder();
for (int element = 0; element < choices.Count; element++)
if (Interactive)
{
sb.Append(String.Format(
CultureInfo.CurrentCulture,
"|{0}> {1} ",
promptData[0, element],
promptData[1, element]));
}
// Write the caption and message strings in Blue.
this.WriteLine(
ConsoleColor.Blue,
Console.BackgroundColor,
caption + System.Environment.NewLine + message + System.Environment.NewLine);
Collection<int> defaultResults = new Collection<int>();
if (defaultChoices != null)
{
int countDefaults = 0;
foreach (int defaultChoice in defaultChoices)
// Convert the choice collection into something that is
// easier to work with. See the BuildHotkeysAndPlainLabels
// method for details.
string[,] promptData = BuildHotkeysAndPlainLabels(choices);
// Format the overall choice prompt string to display.
StringBuilder sb = new StringBuilder();
for (int element = 0; element < choices.Count; element++)
{
++countDefaults;
defaultResults.Add(defaultChoice);
sb.Append(String.Format(
CultureInfo.CurrentCulture,
"|{0}> {1} ",
promptData[0, element],
promptData[1, element]));
}
if (countDefaults != 0)
Collection<int> defaultResults = new Collection<int>();
if (defaultChoices != null)
{
sb.Append(countDefaults == 1 ? "[Default choice is " : "[Default choices are ");
int countDefaults = 0;
foreach (int defaultChoice in defaultChoices)
{
sb.AppendFormat(
CultureInfo.CurrentCulture,
"\"{0}\",",
promptData[0, defaultChoice]);
++countDefaults;
defaultResults.Add(defaultChoice);
}
sb.Remove(sb.Length - 1, 1);
sb.Append("]");
if (countDefaults != 0)
{
sb.Append(countDefaults == 1 ? "[Default choice is " : "[Default choices are ");
foreach (int defaultChoice in defaultChoices)
{
sb.AppendFormat(
CultureInfo.CurrentCulture,
"\"{0}\",",
promptData[0, defaultChoice]);
}
sb.Remove(sb.Length - 1, 1);
sb.Append("]");
}
}
this.WriteLine(
ConsoleColor.Cyan,
Console.BackgroundColor,
sb.ToString());
// Read prompts until a match is made, the default is
// chosen, or the loop is interrupted with ctrl-C.
Collection<int> results = new Collection<int>();
while (true)
{
ReadNext:
string prompt = string.Format(CultureInfo.CurrentCulture, "Choice[{0}]:", results.Count);
this.Write(ConsoleColor.Cyan, Console.BackgroundColor, prompt);
string data = Console.ReadLine().Trim().ToUpper();
// If the choice string was empty, no more choices have been made.
// If there were no choices made, return the defaults
if (data.Length == 0)
{
return (results.Count == 0) ? defaultResults : results;
}
// See if the selection matched and return the
// corresponding index if it did.
for (int i = 0; i < choices.Count; i++)
{
if (promptData[0, i] == data)
{
results.Add(i);
goto ReadNext;
}
}
this.WriteErrorLine("Invalid choice: " + data);
}
}
this.WriteLine(
ConsoleColor.Cyan,
Console.BackgroundColor,
sb.ToString());
// Read prompts until a match is made, the default is
// chosen, or the loop is interrupted with ctrl-C.
Collection<int> results = new Collection<int>();
while (true)
else
{
ReadNext:
string prompt = string.Format(CultureInfo.CurrentCulture, "Choice[{0}]:", results.Count);
this.Write(ConsoleColor.Cyan, Console.BackgroundColor, prompt);
string data = Console.ReadLine().Trim().ToUpper();
// If the choice string was empty, no more choices have been made.
// If there were no choices made, return the defaults
if (data.Length == 0)
{
return (results.Count == 0) ? defaultResults : results;
}
// See if the selection matched and return the
// corresponding index if it did.
for (int i = 0; i < choices.Count; i++)
{
if (promptData[0, i] == data)
{
results.Add(i);
goto ReadNext;
}
}
this.WriteErrorLine("Invalid choice: " + data);
throw new PSInvalidOperationException("Cannot prompt user when invoked with --noninteractive option.");
}
}