PowerShell V3 CTP2 Provides Better Argument Passing to EXEs

Within PowerShell it has always been easy to pass “simple” arguments to an EXE e.g.:

C:\PS> ipconfig -all

However passing arguments to certain exes can become surprising difficult when their command line parameter syntax is complex i.e. they require quotes and use special PowerShell characters such as @ $ ;.  A lot of these problems can be solved by placing single or double quotes in the right places or by escaping PowerShell’s special characters e.g.:

C:\PS> tf.exe status . /workspace:HILLR1;hillr /r
There are no pending changes.
The term 'hillr' is not recognized as the name of a cmdlet, function, script file, or operable program. Check the
spelling of the name, or if a path was included, verify that the path is correct and try again.
At line:1 char:35
+ tf.exe status . /workspace:HILLR1;hillr /r
+                                   ~~~~~
    + CategoryInfo          : ObjectNotFound: (hillr:String) [], CommandNotFoundException
    + FullyQualifiedErrorId : CommandNotFoundException

Note that in the command line above the “/workspace” parameter value is specified using a special syntax that TF.exe recognizes i.e. <workspace_name>;<username>.  Unfortunately the semicolon is a statement separator in PowerShell which means that TF.exe only sees the parameters before the semicolon.  We can use the ECHOARGS.exe utility from the PowerShell Community Extensions to verify this:

C:\PS> echoargs.exe status . /workspace:HILLR1;hillr /r
Arg 0 is <status>
Arg 1 is <.>
Arg 2 is </workspace:HILLR1>

In this case, the solution is simple – just escape the semicolon e.g.:

C:\PS> tf.exe status . /r /workspace:HILLR1`;hillr
File name     Change Local path
------------- ------ -----------------------------------------
$/Foo/Trunk/Tools/Bin
TfsTools.psm1 edit   C:\Tfs\Foo\Trunk\Tools\Bin\TfsTools.psm1

1 change(s)

This works up to the point where you get quite frustrated figuring out which characters to escape and which parameter/argument pairs need to be quoted and whether you should use single quotes or double quotes.  Fortunately, it looks like we will get a way to tell the PowerShell argument parser to stop doing so much work for us and just pass the args through “as-is”.  In other words, you can tell PowerShell to become a “dumber” command line parser.  This mode is invoked using the character sequence: –% and it works from the point it appears on the command line to the end of that line.  Note that the character sequence may change or the feature could be completely removed before V3 ships.

Given this new feature, here’s how you use it.  Take this example of a problematic set of command line parameters:

C:\PS> sqlcmd -S .\SQLEXPRESS -v lname="Gates" -Q "SELECT FirstName,LastName FROM
AdventureWorks.Person.Contact WHERE LastName = '$(lname)'"
The term 'lname' is not recognized as the name of a cmdlet, function, script
file, or operable program. Check the spelling of the name, or if a path was
included, verify that the path is correct and try again.
At line:1 char:126
+ ...  LastName = '$(lname)'"
+                    ~~~~~
    + CategoryInfo          : ObjectNotFound: (lname:String) [], CommandNotFou
   ndException
    + FullyQualifiedErrorId : CommandNotFoundException

In this case the V2 solution is to escape the $ character in the last part of the command line e.g.: ‘`$(lname)’ but if you don’t want to spend the time to figure this out you can easily use –% like so:

C:\PS> sqlcmd --% -S .\SQLEXPRESS -v lname="Gates" -Q "SELECT FirstName,LastName F
ROM AdventureWorks.Person.Contact WHERE LastName = '$(lname)'"
FirstName                                          LastName

---------------------------------- -----------------------------------
Janet                              Gates

(1 rows affected)

You can put the –% later in the parameter list if you want.  You might want to do this if you need to use PowerShell variable expansion in some of the arguments.  Just note that once you specify –% the rest of the command line will be parsed “dumbly”.  You will get no PowerShell variable expansion or grouping expressions and you won’t be able to escape newlines.  One thing you can do in this special parsing mode is expand environment variables using the batch syntax of %ENV_VAR% e.g.:

C:\PS> $env:colname = "LastName"
C:\PS> sqlcmd -S .\SQLEXPRESS -v lname="Gates" --% -Q "SELECT FirstName,LastName F
ROM AdventureWorks.Person.Contact WHERE %colname% = '$(lname)'"
FirstName                                          LastName

---------------------------------- -----------------------------------
Janet                              Gates

(1 rows affected)

I believe this new command line parsing feature will greatly simplify interacting with exes that have a complex command line parameter syntax.  Thanks to the PowerShell team for listening to the community feedback on this issue and providing a solution.

Posted in PowerShell 3.0 | Tagged , , | 3 Comments

Microsoft Windows PowerShell V3 CTP2 Available for Download

You can grab the bits from here. If you have V3 CTP1 installed, please uninstall it first or you can get your machine into a bad state.

So far my favorite two features new to this drop are both in the Integrated Scripting Editor (ISE). The first is the “most recently opened files list” on the File menu and second is the switch to a two pane ISE (combines the output and command panes into one). Oh yeah, there isn’t much in the help system until you run Update-Help from an elevated prompt.

Posted in PowerShell, PowerShell 3.0 | 2 Comments

Windows PowerShell Version 3 Simplified Syntax

Windows PowerShell version 3 introduces a simplified syntax for the Where-Object and Foreach-Object cmdlets.  The simplified syntax shown below, eliminates the curly braces as well as the need for the special variable $_.

C:\PS> Get-Process | Where PM -gt 100MB
...
C:\PS> Get-Process | Foreach Name
...

The intent of this “syntax” is to make it easier for folks get started with PowerShell.  Compared to the commands below, I can see the value of the simplified syntax:

C:\PS> Get-Process | Where {$_.PM -gt 100MB}
...
C:\PS> Get-Process | Foreach {$_.Name}
...

When folks are first learning PowerShell, the special variable $_ is one of those mental model hurdles they have to get over.  The simplified syntax feature of V3 seems to generate a fair amount of controversy (is it really necessary, doesn’t this just complicate things more, etc).  Regardless of where you stand on the simplified syntax it is useful to understand how it works.

Given that it appears to be a simplified expression syntax you might think this required a change to the PowerShell parser’s grammar but you would be wrong.  It turns out that the simplified syntax is implemented by additional parameter sets – lots of additional parameter sets. In fact, for every operator supported, there is an additional parameter set to support that operator.  Let’s see this with the Where-Object cmdlet by listing out all of its parameter set names:

C:\PS> Get-Command Where-Object | Select -Expand ParameterSets | Format-Table Name

Name
----
EqualSet
ScriptBlockSet
CaseSensitiveGreaterThanSet
CaseSensitiveNotEqualSet
LessThanSet
CaseSensitiveEqualSet
NotEqualSet
GreaterThanSet
CaseSensitiveLessThanSet
GreaterOrEqualSet
CaseSensitiveGreaterOrEqualSet
LessOrEqualSet
CaseSensitiveLessOrEqualSet
LikeSet
CaseSensitiveLikeSet
NotLikeSet
CaseSensitiveNotLikeSet
MatchSet
CaseSensitiveMatchSet
NotMatchSet
CaseSensitiveNotMatchSet
ContainsSet
CaseSensitiveContainsSet
NotContainsSet
CaseSensitiveNotContainsSet
InSet
CaseSensitiveInSet
NotInSet
CaseSensitiveNotInSet
IsSet
IsNotSet
 

Most of these correspond to the operators you are already familiar with such as: –GT, –LT, –GE, –LE, –LIKE, –MATCH, –NOTMATCH, –CONTAINS, –NOTCONTAINS, etc.  Note however there are two new operators in PowerShell V3: –In and –NotIn which you can use like so:

C:\PS> 1 -In 1..10
True
C:\PS> 20 -NotIn 1..10
True

Let’s look at the interesting parameters on these operator specific parameter sets.  Let’s look at the EqualsSet parameter set:

C:\PS> Get-Command Where-Object | Select -Expand ParameterSets | Where Name -eq EqualSet |
           Select -Expand Parameters | Where Position -ge 0 |
           Format-Table Name,Position,IsMandatory -AutoSize

Name     Position IsMandatory
----     -------- -----------
Property        0        True
Value           1       False

As it turns out, these results are the same for all the operator oriented parameter sets.  At the very minimum, the Property parameter is required and is always the first positional parameter.  And as you would expect, if you don’t provide it, you get prompted for a value:

C:\PS> Get-Process | Where -eq

cmdlet Where-Object at command pipeline position 2
Supply values for the following parameters:
Property:

Now even though Value parameter is specified as not mandatory, in many cases if you don’t provide it you will get a terminating error e.g.:

C:\PS> Get-Process | Where Name -eq
Where-Object : The specified operator requires both the -Property and -Value parameters. Supply both parameters and
retry.
At line:1 char:15
+ Get-Process | Where Name -eq
+               ~~~~~~~~~~~~~~
    + CategoryInfo          : InvalidArgument: (:) [Where-Object], PSArgumentException
    + FullyQualifiedErrorId : ValueNotSpecifiedForWhereObject,Microsoft.PowerShell.Commands.WhereObjectCommand

There are some cases where you don’t have to provide the value nor the operator e.g.:

C:\PS> Get-Process | Where Responding

Handles  NPM(K)    PM(K)      WS(K) VM(M)   CPU(s)     Id ProcessName
-------  ------    -----      ----- -----   ------     -- -----------
    216      10     3560       2896    73            4000 atieclxx
    130       7     2380       1028    33            1020 atiesrxx
    157      11    17288      13344    49            7876 audiodg
     28       6     1256        420    42     0.06   2752 BluetoothHeadsetProxy
...

This works because A) the EqualsSet parameter set is the default parameter set and B) the Where-Object implementation appears to coerce the property specified (Responding in this case) to Boolean. If the result is $true then the object is output by Where-Object and sent on its way down the pipeline.

So all this simplified syntax really is, is a bunch of operator specific parameter sets on Where-Object that have a positional and mandatory Property parameter of type [string] and a positional Value parameter of type [object].  In the case of Foreach-Object it is one extra parameter set called PropertyAndMethodSet which has one mandatory, positional parameter called MemberName.  And as with any cmdlet, you provide the parameter values and the cmdlet determines how to interpret them.  In fact, given standard parameter parsing behavior the below is as valid as the conventional notation:

C:\PS> Get-Process | Where -GT PM 100MB
...
C:\PS> Get-Process | Where PM 100MB -GT
...
C:\PS> Get-Process | Where -Value 100MB -Property PM -GT
...

Now where this syntax can lead you astray if you don’t understand how it works, is if you make the assumption that this is a parsed expression.  In that case, folks might expect this to work:
 
C:\PS> Get-Process | Where Threads.Count -GT 100

There is a Threads collection on each Process object.  We might think that we can access a property on that collection but in effect, what happens is that the Where-Object Property parameter gets the value “Threads.Count” and there is no property on a Process object called “Threads.Count”. This silently fails which might lead you to believe there are no processes with greater than 100 threads.  But reverting back to the standard syntax we see that isn’t the case:
 
C:\PS> Get-Process | Where {$_.Threads.Count -GT 100}

Handles  NPM(K)    PM(K)      WS(K) VM(M)   CPU(s)     Id ProcessName
-------  ------    -----      ----- -----   ------     -- -----------
   2080     126   155680     139928   531   113.68   4920 msnmsgr
   1087       0      312       8800    15               4 System

So when you are using the simplified syntax be sure to keep in mind that you can only specify property names and you cannot access sub-properties.  Keep your property names simple and you should be copasetic with the new, simplified syntax.  While I’m a little unsure about the new simplified syntax given how quickly you can fall off the “simple” path into the sharp rocks and lava below, I will say this.  As I wrote this blog post, I used the simplified syntax quite a bit and I have to say that it is growing on me.
 
One final item to mention about simplified syntax.  It turns out that some folks have a hard time grokking $_ but when they’re presented with $PSItem it apparently makes more sense to them.  So in PowerShell v3, wherever you can use $_ you can also use $PSItem$PSItem is not an alias. It seems to be a duplicate variable defined in all the same scopes as $_ and its value tracks that of $_ e.g.:
 
C:\PS> 1 | Foreach {Get-Variable _,psitem; $_ = 4; Get-Variable _,psitem}

Name                           Value
----                           -----
_                              1
PSItem                         1
_                              4
PSItem                         4
Posted in PowerShell 3.0 | 2 Comments

PSCX 2.1 Beta 1 Available for Download

I just uploaded beta 1 for the PowerShell Community Extensions version 2.1.  This beta drop adds better support for Windows PowerShell V3 that is in the Windows 8 Developer Preview.  There are a number of bug fixes in this drop:

  • 28023 Read-Archive : Cannot bind parameter ‘Path’. Cannot convert the … value of type “System.String” to type “Pscx.IO.PscxPathInfo”.
  • 28198 Test-XML not validating xml against schema correctly
  • 28964 Get-FileTail access conflict
  • 29255 Get-HttpResource Timeout Bug
  • 29598 String – PscxPathInfo ParameterBindingException
  • 30169 Invoke-Ternary example doesn’t work
  • 30921 Invoke-Elevated demands arguments

You can download the beta from here.

Posted in PowerShell, PowerShell 3.0, PSCX | 1 Comment

MVP Summit 2011

Testing out my first WordPress blog post after the switch from Windows Live Spaces (sniff, I will miss you) to WordPress.  Regarding the MVP Summit last week, I can’t really talk about much due to just about everything being NDA, NDA, NDA!  I will say that I’m excited about the future of PowerShell!  Probably the most fun part was hanging out with the other PowerShell MVPs for a week.

It seems to me that Microsoft still values their relationship with the MVPs as evidenced by the party they arranged for MVPs last Wednesday:

MVP Summit 20110302-DSC_0052

Yep, that is SafeCo field in Seattle where the Mariners play.  They rented the whole stadium out for the evening!  There where a couple of bands – one called The Beatniks played out near centerfield.  You could run the bases, bat some balls up into the stands. Yeah, it was an awesome party.

Posted in PowerShell | Leave a comment

Make-PS1ExeWrapper

Occasionally folks want to be able to create an EXE from PoweShell.  PowerShell can’t do this by itself but this can be done with PowerShell script.  Essentially what you can do is create a simple console EXE program that embeds the script as a resource and the EXE, upon loading retrieves the script and throws it at a PowerShell runspace to execute.  Here’s the script for a feasibility test of doing this very thing.

Note that this script depends on Write-GZip from the PowerShell Community Extensions.

Update 6-21-2011: The migration from Windows Live Spaces to WordPress seems to have messed with the formatting of the script.  You can now download the script from my SkyDrive.

#requires -version 2.0
<#
.SYNOPSIS
Creates an EXE wrapper from a PowerShell script by compressing the script and embedding into
a newly generated assembly.
.DESCRIPTION
Creates an EXE wrapper from a PowerShell script by compressing the script and embedding into
a newly generated assembly.
.PARAMETER Path
The path to the .
.PARAMETER LiteralPath
Specifies a path to one or more locations. Unlike Path, the value of LiteralPath is used exactly as it
is typed. No characters are interpreted as wildcards. If the path includes escape characters, enclose
it in single quotation marks. Single quotation marks tell Windows PowerShell not to interpret any
characters as escape sequences.
.PARAMETER OutputAssembly
The name (including path) of the EXE to generate.
.PARAMETER IconPath
The path to an optional icon to be embedded as the application icon for the EXE.
.EXAMPLE
C:\PS> .\Make-PS1ExeWrapper.ps1 .\MyScript.ps1 .\MyScript.exe .\app.ico
This creates an console application called MyScript.exe that internally hosts the PowerShell
engine and runs the script specified by MyScript.ps1.  Optionally the file app.ico is
embedded into the EXE as the application’s icon.
.NOTES
Author: Keith Hill
Date:   Aug 7, 2010
Issues: This implementation is more of a feasibility test and isn’t fully functional.  It doesn’t
support an number of PSHostUserInterface members as well as a number of PSHostRawUserInterface
members.  This approach also suffers from the same problem of running script “interactively”
and not loading it from a file. That is, the entire script output is run through Out-Default
and PowerShell gets confused.  It formats the first types it sees correctly but after that the
formatting is off.  To correct this, you have to append | Out-Default where you script outputs
to the host without using a Write-* cmdlet e.g.:

MyScript.ps1:
——————————-
Get-Process svchost
Get-Date | Out-Default
Dir C:\  | Out-Default
Dir c:\idontexist | Out-Default
$DebugPreference = ‘Continue’
$VerbosePreference = ‘Continue’
Write-Host    “host”
Write-Warning “warning”
Write-Verbose “verbose”
Write-Debug   “debug”
Write-Error   “error”
#>
[CmdletBinding(DefaultParameterSetName="Path")]
param(
[Parameter(Mandatory=$true, Position=0, ParameterSetName="Path",
ValueFromPipeline=$true, ValueFromPipelineByPropertyName=$true,
HelpMessage="Path to bitmap file")]
[ValidateNotNullOrEmpty()]
[string[]]
$Path,

[Alias("PSPath")]
[Parameter(Mandatory=$true, Position=0, ParameterSetName="LiteralPath",
ValueFromPipelineByPropertyName=$true,
HelpMessage="Path to bitmap file")]
[ValidateNotNullOrEmpty()]
[string[]]
$LiteralPath,

    [Parameter(Mandatory = $true, Position = 1)]
[string]
$OutputAssembly,

[Parameter(Position = 2)]
[string]
$IconPath
)

Begin {
Set-StrictMode -Version latest

$src = @’
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Globalization;
using System.IO;
using System.IO.Compression;
using System.Management.Automation;
using System.Management.Automation.Host;
using System.Management.Automation.Runspaces;
using System.Reflection;
using System.Security;
using System.Text;
using System.Threading;

namespace PS1ToExeTemplate
{
class Program
{
private static object _powerShellLock = new object();
private static readonly Host _host = new Host();
private static PowerShell _powerShellEngine;

        static void Main(string[] args)
{
Console.CancelKeyPress += Console_CancelKeyPress;
Console.TreatControlCAsInput = false;

            string script = GetScript();
RunScript(script, args, null);
}

        private static string GetScript()
{
string script = String.Empty;

            Assembly assembly = Assembly.GetExecutingAssembly();
using (Stream stream = assembly.GetManifestResourceStream(“Resources.Script.ps1.gz”))
{
var gZipStream = new GZipStream(stream, CompressionMode.Decompress, true);
var streamReader = new StreamReader(gZipStream);
script = streamReader.ReadToEnd();
}

            return script;
}

        private static void RunScript(string script, string[] args, object input)
{
lock (_powerShellLock)
{
_powerShellEngine = PowerShell.Create();
}

            try
{
_powerShellEngine.Runspace = RunspaceFactory.CreateRunspace(_host);
_powerShellEngine.Runspace.Open();
_powerShellEngine.AddScript(script);
_powerShellEngine.AddCommand(“Out-Default”);
_powerShellEngine.Commands.Commands[0].MergeMyResults(PipelineResultTypes.Error, PipelineResultTypes.Output);

                if (input != null)
{
_powerShellEngine.Invoke(new[] { input });
}
else
{
_powerShellEngine.Invoke();
}
}
finally
{
lock (_powerShellLock)
{
_powerShellEngine.Dispose();
_powerShellEngine = null;
}
}
}

        private static void Console_CancelKeyPress(object sender, ConsoleCancelEventArgs e)
{
try
{
lock (_powerShellLock)
{
if (_powerShellEngine != null && _powerShellEngine.InvocationStateInfo.State == PSInvocationState.Running)
{
_powerShellEngine.Stop();
}
}
e.Cancel = true;
}
catch (Exception ex)
{
_host.UI.WriteErrorLine(ex.ToString());
}
}
}

    class Host : PSHost
{
private PSHostUserInterface _psHostUserInterface = new HostUserInterface();

        public override void SetShouldExit(int exitCode)
{
Environment.Exit(exitCode);
}

        public override void EnterNestedPrompt()
{
throw new NotImplementedException();
}

        public override void ExitNestedPrompt()
{
throw new NotImplementedException();
}

        public override void NotifyBeginApplication()
{
}

        public override void NotifyEndApplication()
{
}

        public override string Name
{
get { return “PSCX-PS1ToExeHost”; }
}

        public override Version Version
{
get { return new Version(1, 0); }
}

        public override Guid InstanceId
{
get { return new Guid(“E4673B42-84B6-4C43-9589-95FAB8E00EB2″); }
}

        public override PSHostUserInterface UI
{
get { return _psHostUserInterface; }
}

        public override CultureInfo CurrentCulture
{
get { return Thread.CurrentThread.CurrentCulture; }
}

        public override CultureInfo CurrentUICulture
{
get { return Thread.CurrentThread.CurrentUICulture; }
}
}

    class HostUserInterface : PSHostUserInterface, IHostUISupportsMultipleChoiceSelection
{
private PSHostRawUserInterface _psRawUserInterface = new HostRawUserInterface();

        public override PSHostRawUserInterface RawUI
{
get { return _psRawUserInterface; }
}

        public override string ReadLine()
{
return Console.ReadLine();
}

        public override SecureString ReadLineAsSecureString()
{
throw new NotImplementedException();
}

        public override void Write(string value)
{
string output = value ?? “null”;
Console.Write(output);
}

        public override void Write(ConsoleColor foregroundColor, ConsoleColor backgroundColor, string value)
{
string output = value ?? “null”;
var origFgColor = Console.ForegroundColor;
var origBgColor = Console.BackgroundColor;
Console.ForegroundColor = foregroundColor;
Console.BackgroundColor = backgroundColor;
Console.Write(output);
Console.ForegroundColor = origFgColor;
Console.BackgroundColor = origBgColor;
}

        public override void WriteLine(string value)
{
string output = value ?? “null”;
Console.WriteLine(output);
}

        public override void WriteErrorLine(string value)
{
string output = value ?? “null”;
var origFgColor = Console.ForegroundColor;
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine(output);
Console.ForegroundColor = origFgColor;
}

        public override void WriteDebugLine(string message)
{
WriteYellowAnnotatedLine(message, “DEBUG”);
}

        public override void WriteVerboseLine(string message)
{
WriteYellowAnnotatedLine(message, “VERBOSE”);
}

        public override void WriteWarningLine(string message)
{
WriteYellowAnnotatedLine(message, “WARNING”);
}

        private void WriteYellowAnnotatedLine(string message, string annotation)
{
string output = message ?? “null”;
var origFgColor = Console.ForegroundColor;
var origBgColor = Console.BackgroundColor;
Console.ForegroundColor = ConsoleColor.Yellow;
Console.BackgroundColor = ConsoleColor.Black;
WriteLine(String.Format(CultureInfo.CurrentCulture, “{0}: {1}”, annotation, output));
Console.ForegroundColor = origFgColor;
Console.BackgroundColor = origBgColor;
}

        public override void WriteProgress(long sourceId, ProgressRecord record)
{
throw new NotImplementedException();
}

        public override Dictionary<string, PSObject> Prompt(string caption, string message, Collection<FieldDescription> descriptions)
{
if (String.IsNullOrEmpty(caption) && String.IsNullOrEmpty(message) && descriptions.Count > 0)
{
Console.Write(descriptions[0].Name + “: “);
}
else
{
this.Write(ConsoleColor.DarkCyan, ConsoleColor.Black, caption + “\n” + message + ” “);
}
var results = new Dictionary<string, PSObject>();
foreach (FieldDescription fd in descriptions)
{
string[] label = GetHotkeyAndLabel(fd.Label);
this.WriteLine(label[1]);
string userData = Console.ReadLine();
if (userData == null)
{
return null;
}

                results[fd.Name] = PSObject.AsPSObject(userData);
}

            return results;
}

        public override PSCredential PromptForCredential(string caption, string message, string userName, string targetName)
{
throw new NotImplementedException();
}

        public override PSCredential PromptForCredential(string caption, string message, string userName, string targetName, PSCredentialTypes allowedCredentialTypes, PSCredentialUIOptions options)
{
throw new NotImplementedException();
}

        public override int PromptForChoice(string caption, string message, Collection<ChoiceDescription> choices, int defaultChoice)
{
// Write the caption and message strings in Blue.
this.WriteLine(ConsoleColor.Blue, ConsoleColor.Black, caption + “\n” + message + “\n”);

            // 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.
var 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, “[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(sb.ToString());
string data = Console.ReadLine().Trim().ToUpper(CultureInfo.CurrentCulture);

                // If the choice string was empty, use the default selection.
if (data.Length == 0)
{
return defaultChoice;
}

                // 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);
}
}

        #region IHostUISupportsMultipleChoiceSelection Members

        public Collection<int> PromptForChoice(string caption, string message, Collection<ChoiceDescription> choices, IEnumerable<int> defaultChoices)
{
this.WriteLine(ConsoleColor.Blue, ConsoleColor.Black, caption + “\n” + message + “\n”);

            string[,] promptData = BuildHotkeysAndPlainLabels(choices);

            var 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]));
}

            var defaultResults = new Collection<int>();
if (defaultChoices != null)
{
int countDefaults = 0;
foreach (int defaultChoice in defaultChoices)
{
++countDefaults;
defaultResults.Add(defaultChoice);
}

                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, ConsoleColor.Black, sb.ToString());

            var results = new Collection<int>();
while (true)
{
ReadNext:
string prompt = string.Format(CultureInfo.CurrentCulture, “Choice[{0}]:”, results.Count);
this.Write(ConsoleColor.Cyan, ConsoleColor.Black, prompt);
string data = Console.ReadLine().Trim().ToUpper(CultureInfo.CurrentCulture);

                if (data.Length == 0)
{
return (results.Count == 0) ? defaultResults : results;
}

                for (int i = 0; i < choices.Count; i++)
{
if (promptData[0, i] == data)
{
results.Add(i);
goto ReadNext;
}
}

                this.WriteErrorLine(“Invalid choice: ” + data);
}
}

        #endregion

        private static string[,] BuildHotkeysAndPlainLabels(Collection<ChoiceDescription> choices)
{
// Allocate the result array
string[,] hotkeysAndPlainLabels = new string[2, choices.Count];

            for (int i = 0; i < choices.Count; ++i)
{
string[] hotkeyAndLabel = GetHotkeyAndLabel(choices[i].Label);
hotkeysAndPlainLabels[0, i] = hotkeyAndLabel[0];
hotkeysAndPlainLabels[1, i] = hotkeyAndLabel[1];
}

            return hotkeysAndPlainLabels;
}

        private static string[] GetHotkeyAndLabel(string input)
{
string[] result = new string[] { String.Empty, String.Empty };
string[] fragments = input.Split(‘&’);
if (fragments.Length == 2)
{
if (fragments[1].Length > 0)
{
result[0] = fragments[1][0].ToString().
ToUpper(CultureInfo.CurrentCulture);
}

                result[1] = (fragments[0] + fragments[1]).Trim();
}
else
{
result[1] = input;
}

            return result;
}
}

    class HostRawUserInterface : PSHostRawUserInterface
{
public override KeyInfo ReadKey(ReadKeyOptions options)
{
throw new NotImplementedException();
}

        public override void FlushInputBuffer()
{
}

        public override void SetBufferContents(Coordinates origin, BufferCell[,] contents)
{
throw new NotImplementedException();
}

        public override void SetBufferContents(Rectangle rectangle, BufferCell fill)
{
throw new NotImplementedException();
}

        public override BufferCell[,] GetBufferContents(Rectangle rectangle)
{
throw new NotImplementedException();
}

        public override void ScrollBufferContents(Rectangle source, Coordinates destination, Rectangle clip, BufferCell fill)
{
throw new NotImplementedException();
}

        public override ConsoleColor ForegroundColor
{
get { return Console.ForegroundColor; }
set { Console.ForegroundColor = value; }
}

        public override ConsoleColor BackgroundColor
{
get { return Console.BackgroundColor; }
set { Console.BackgroundColor = value; }
}

        public override Coordinates CursorPosition
{
get { return new Coordinates(Console.CursorLeft, Console.CursorTop); }
set { Console.SetCursorPosition(value.X, value.Y); }
}

        public override Coordinates WindowPosition
{
get { return new Coordinates(Console.WindowLeft, Console.WindowTop); }
set { Console.SetWindowPosition(value.X, value.Y); }
}

        public override int CursorSize
{
get { return Console.CursorSize; }
set { Console.CursorSize = value; }
}

        public override Size BufferSize
{
get { return new Size(Console.BufferWidth, Console.BufferHeight); }
set { Console.SetBufferSize(value.Width, value.Height); }
}

        public override Size WindowSize
{
get { return new Size(Console.WindowWidth, Console.WindowHeight); }
set { Console.SetWindowSize(value.Width, value.Height); }
}

        public override Size MaxWindowSize
{
get { return new Size(Console.LargestWindowWidth, Console.LargestWindowHeight); }
}

        public override Size MaxPhysicalWindowSize
{
get { return new Size(Console.LargestWindowWidth, Console.LargestWindowHeight); }
}

        public override bool KeyAvailable
{
get { return Console.KeyAvailable; }
}

        public override string WindowTitle
{
get { return Console.Title; }
set { Console.Title = value; }
}
}
}
‘@
}   

Process {
if ($psCmdlet.ParameterSetName -eq “Path”)
{
# In the -Path (non-literal) case we may need to resolve a wildcarded path
$resolvedPaths = @($Path | Resolve-Path | Convert-Path)
}
else
{
# Must be -LiteralPath
$resolvedPaths = @($LiteralPath | Convert-Path)
}

foreach ($rpath in $resolvedPaths)
{
Write-Verbose “Processing $rpath”

        $gzItem = Get-ChildItem $rpath | Write-GZip -Quiet
$resourcePath = “$($gzItem.Directory)\Resources.Script.ps1.gz”
if (Test-Path $resourcePath) { Remove-Item $resourcePath }
Rename-Item $gzItem $resourcePath

# Configure the compiler parameters
$referenceAssemblies = ‘System.dll’,([psobject].Assembly.Location)
$outputPath = $OutputAssembly
if (![IO.Path]::IsPathRooted($outputPath))
{
$outputPath = [io.path]::GetFullPath((Join-Path $pwd $outputPath))
}
if ($rpath -eq $outputPath)
{
throw ‘Oops, you don”t really want to overwrite your script with an EXE.’
}

        $cp = new-object System.CodeDom.Compiler.CompilerParameters $referenceAssemblies,$outputPath,$true
$cp.TempFiles = new-object System.CodeDom.Compiler.TempFileCollection ([IO.Path]::GetTempPath())
$cp.GenerateExecutable = $true
$cp.GenerateInMemory   = $false
$cp.IncludeDebugInformation = $true
if ($IconPath)
{
$rIconPath = Resolve-Path $IconPath
$cp.CompilerOptions = ” /win32icon:$rIconPath”
}
[void]$cp.EmbeddedResources.Add($resourcePath)

# Create the C# codedom compiler
$dict = new-object ‘System.Collections.Generic.Dictionary[string,string]‘
$dict.Add(‘CompilerVersion’,'v3.5′)
$provider = new-object Microsoft.CSharp.CSharpCodeProvider $dict

# Compile the source and report errors
$results = $provider.CompileAssemblyFromSource($cp, $src)
if ($results.Errors.Count)
{
$errorLines = “”
foreach ($error in $results.Errors)
{
$errorLines += “`n`t” + $error.Line + “:`t” + $error.ErrorText
}
Write-Error $errorLines
}
}
}

Posted in PowerShell 2.0 | 21 Comments

Determining $ScriptDir Safely

We had some boiler plate code that we always put into our scripts to set strict mode and to compute $ScriptDir so the script can load other scripts relatively to its location.  This boiler plate code is simple:

#requires –Version 2.0
Set-StrictMode –Version 2.0
$ScriptDir = Split-Path $MyInvocation.MyCommand.Path –Parent

However lets say you do this in all your scripts and one script (Parent.ps1) dot-sources another script (PoshLib.ps1) that does the same trick e.g.:

Parent.ps1:
$ScriptDir = Split-Path $MyInvocation.MyCommand.Path –Parent
"PARENT:  Before dot-sourcing libary ScriptDir is $ScriptDir"
. $ScriptDir\Bin\PoshLib.ps1
"PARENT:  After dot-sourcing libary ScriptDir is $ScriptDir"

PoshLib.ps1
$ScriptDir = Split-Path $MyInvocation.MyCommand.Path –Parent
"POSHLIB: ScriptDir is $ScriptDir"

Seems innocent enough but check out the output of running Parent.ps1:

PS C:\Users\Keith> C:\Users\Keith\Parent.ps1
PARENT:  Before dot-sourcing libary ScriptDir is C:\Users\Keith
POSHLIB: ScriptDir is C:\Users\Keith\Bin
PARENT:  After dot-sourcing libary ScriptDir is C:\Users\Keith\Bin

What happens here is that because we are “dot sourcing” PoshLib.ps1 into Parent.ps1, its definition of the $ScriptDir variable stomps the one created in Parent.ps1.  Obviously this won’t do.  You could try to create unique variable names for each script but that isn’t ideal and not very maintainable. 

The best answer if you are on PowerShell 2.0 is to just use modules for your libraries.  Modules can have “private” variables that don’t get exported so the variable “stomping” issue never arises.  However, for various reasons, folks can’t always upgrade to PowerShell 2.0.  So here is how you can fix this issue on PowerShell 1.0. 

Essentially what you want to do is to dynamically evaluate the script’s location every time without having to use the longhand:

Split-Path $MyInvocation.MyCommand.Path –Parent

PowerShell allows us to create a shorthand for this using an anonymous scriptblock assigned to a variable like so:

Parent.ps1:
$ScriptDir = { Split-Path $MyInvocation.ScriptName –Parent }
"PARENT:  Before dot-sourcing libary ScriptDir is $(&$ScriptDir)"
. "$(&$ScriptDir)\Bin\PoshLib.ps1"
"PARENT:  After dot-sourcing libary ScriptDir is $(&$ScriptDir)"

PoshLib.ps1
$ScriptDir = { Split-Path $MyInvocation.ScriptName –Parent }
"POSHLIB: ScriptDir is $(&$ScriptDir)"

Note: that once we put the code within the scriptblock we need to switch from $MyInvocation.MyCommand.Path to $MyInvocation.ScriptName.  These changes yield the desired results:

PS C:\Users\Keith> C:\Users\Keith\Parent.ps1
PARENT:  Before dot-sourcing libary ScriptDir is C:\Users\Keith
POSHLIB: ScriptDir is C:\Users\Keith\Bin
PARENT:  After dot-sourcing libary ScriptDir is C:\Users\Keith

Posted in PowerShell | 5 Comments