PowerShell Cookbook

Search

Categories

 

On this page

A History Browsing Prompt
** WARNING ** MSH may make you _too_ efficient
Working with the new Monad beta -- getting settled in
Jeffrey Snover's Monad TechEd Presentation ... now by Webcast
Monad, RSS, XML, and other cool tricks
Creating SQL's "join"-like functionality in MSH
MSH and YubNub -- A community commandline
Fixing the headphones on a Nomad Zen Xtra
Updated Monad (Microsoft SHell) released to BetaPlace!
A Web-enabled, Monad front end: Monad hosting.

Archive

Blogroll

Disclaimer
I work for Microsoft.

The opinions expressed herein are my own personal opinions and do not represent my employer's view in any way.

RSS 2.0 | Atom 1.0 | CDF

Send mail to the author(s) E-mail

Total Posts: 220
This Year: 20
This Month: 0
This Week: 0
Comments: 533

Sign In

 Friday, July 08, 2005
Friday, July 08, 2005 7:23:07 AM (Pacific Daylight Time, UTC-07:00) ( )

Now that we've come up to speed with the new beta, it's time to continue our journey into customizing the prompt. It’s been almost a month since I promised it, and a lot of you are already doing great things with your custom profile.

For reference sake, this is our current prompt function, as defined in profile-custom.msh:

function prompt
{
   $currentDirectory = get-location
   "MSH $currentDirectory >"
}

That prompt looks like:

MSH C:\Documents and Settings\Lee >

Ideally, we want it to act more like this:

MSH:1 C:\Documents and Settings\Lee ># some command
MSH:2 C:\Documents and Settings\Lee ># some other command
MSH:3 C:\Documents and Settings\Lee >"Hello World"
Hello World
MSH:4 C:\Documents and Settings\Lee ># yet another command
MSH:5 C:\Documents and Settings\Lee >invoke-history 3
"Hello World"
Hello World

By giving the history ID in the prompt, you can re-invoke those commands without having to use your keyboard’s arrow keys to cycle through the history buffer. Notice how I was able to use the "invoke-history" cmdlet to execute the command I saw on that line.

So how do we go about updating the prompt to include the line number?

Largely, the secret lays in the "get-history" cmdlet. Try it now, and see what it shows:

MSH:6 C:\Documents and Settings\Lee >get-history
Id $_ CommandLine
-- -- -----------
1 C # some command
2 C # some other command
3 C "Hello World"
4 C # yet another command
5 C "Hello World"

At first blush, it seems as though the simplest way to get your history count would be something like this, knowing that get-history returns a list of history items. We already know how to get the count of items in a list:

MSH:7 C:\Documents and Settings\Lee >(get-history).Count
6

That’s a great first guess, but it unfortunately suffers from two major flaws, and a minor flaw.

Major Flaw 1: get-history’s -count parameter.

By default, get-history only returns the last 30 items in your history. You could work around that problem by specifying some huge value (ie: 9999) for the count parameter, but there is a cleaner solution: leverage the Id of the last command executed.

Major Flaw 2: get-history doesn’t always return an array.

Since cmdlets can write any number of objects to the pipeline, the infrastructure decides how to output the results on a case-by-case basis. The first time you call get-history from a clean shell, you have no history. In that situation, the cmdlet returns no items – so the shell doesn’t write anything out. The second time you call get-history (from the clean shell,) your history contains only one item. In that case, the shell writes the individual object out. From the third time on, your history contains two or more items. The get-history cmdlet dutifully writes them out, and the shell finally returns them as an array:

MSH:1 C:\Documents and Settings\Lee >get-history
MSH:2 C:\Documents and Settings\Lee >get-history
Id $_ CommandLine
-- -- -----------
1 C get-history

MSH:3 C:\Documents and Settings\Lee >get-history
Id $_ CommandLine
-- -- -----------
1 C get-history
2 C get-history

Of course, we have a way of working around this as well, or I wouldn’t bring it up!

Minor Flaw 3: We need to display a history Id from the future.

The Id of the last command (or total history count) isn’t exactly what you want. If you use the Id of the last command (or total history count,) the number will not represent the command you are about to execute. In that case, you won’t be able to use the numbers as parameters to invoke-history.

Here’s the final solution that works around these problems, heavily commented for clarity:

function prompt
{
   ## Get the history. Since the history may be either empty,
   ## a single item or an array, the @() syntax ensures
   ## that Monad treats it as an array
   $history = @(get-history)
  

   ## If there are any items in the history, find out the
   ## Id of the final one.
   ## Monad defaults the $lastId variable to '0' if this
   ## code doesn't execute.
   if($history.Count -gt 0)
   {
      $lastItem = $history[$history.Count - 1]
      $lastId = $lastItem.Id
   }

   ## The command that we're currently entering on the prompt
   ## will be next in the history. Because of that, we'll
   ## take the last history Id and add one to it.
   $nextCommand = $lastId + 1

   ## Get the current location
   $currentDirectory = get-location

   ## And create a prompt that shows the command number,
   ## and current location
   "MSH:$nextCommand $currentDirectory >"
}

That gets you a very functional history-browsing prompt, and is just about the one that I use on all of my machines. In an upcoming post, I’ll talk about some other things you might want to do with your prompt.

And for those of you who are becoming more familiar with the shell, one of my older questions still stands:

What idioms from other languages / shells do you find the hardest to un-train yourself from? For example, "cd.." is a valid command in DOS, but not MSH. What have you found the hardest to discover? What did you do to try and discover it? I'm looking for things like,

"I couldn't figure out how to use a hash table, so I typed get-command *hashtable*"

[Edit: Monad has now been renamed to Windows PowerShell. This script or discussion may require slight adjustments before it applies directly to newer builds.]

Comments [9] | | # 
 Friday, July 01, 2005
Friday, July 01, 2005 11:53:20 PM (Pacific Daylight Time, UTC-07:00) ( )

Warning: New research suggests that using Monad (MSH) may make you too efficient.  In a recent study, animals that were provided chronic access to MSH worked harder, and ate less.

It's a fun shell, and a fun language -- but please, everything in moderation.

[Edit: Monad has now been renamed to Windows PowerShell. This script or discussion may require slight adjustments before it applies directly to newer builds.]

Comments [0] | | # 
Friday, July 01, 2005 11:43:36 PM (Pacific Daylight Time, UTC-07:00) ( )

As I mentioned earlier, we posted a new MSH beta to BetaPlace.  It's a lot more polished than the one we posted 9 months ago, so I'll be working with that version from now on.  The articles so far haven't delved very deeply into code just yet (how fortunate!) so there aren't many breaking changes we need to comply with.  However, there have been  few -- so let's go over them.

 

First, you'll notice that we've significantly timmed down the default profile.  Much of the functionality was rolled into the engine, but a lot of it was simply code that very few people use.  Our journey so far has used a few of them, though.  For example, we used the "$MyDocuments" variable to help us load (dot-source) our custom profile.  However, we're in good shape.  Even if you put tons of code in your custom profile, it's in a separate file and will not be affected by changes to the default profile.  So, let's fix that. 

 

First, verify that you have the current profile.  It should be about 66 lines.  

MSH D:\Lee\MSH >(get-content profile.msh).Count

66

If you see over 300, then that's the old profile.  If you've still got the old profile, then you should really start with a clean slate.  Uninstall "Microsoft Command Shell Preview," uninstall "Windows Command Shell Preview," and delete the old profile.  Then, reinstall "Microsoft Command Shell Preview."

 

Now, open the profile with your text editor of choice.  Re-add the $MyDocuments variable, and re-add the custom profile loading:

...

    }

}

 

$myDocuments = [Environment]::GetFolderPath("MyDocuments")

. (combine-path $myDocuments "MSH\profile-custom.msh")

Save it, and reload your shell. 

 

If you liked the $profile variable, you might want to add that to your profile-custom.msh:

$profile = combine-path $myDocuments "MSH\profile-custom.msh"

If you aliased 'cls' to 'clear-host,' the default profile does that for you now.  Feel free to remove it from profile-custom.msh.  All told, this is my profile-custom.msh, as it stands right now. 

## profile-custom.msh

## Stores customizations to the default profile.

 

$profile = combine-path $myDocuments "MSH\profile-custom.msh"

$tools = combine-path $myDocuments "tools"

 

function prompt

{

   $currentDirectory = get-location

   "MSH $currentDirectory >"

}

Notice that I made the prompt function a little more readable, by temporarily storing the current location in a variable.  It will soon get ungainly should we try to stuff it all in a single line.  Also, I've added a variable to hold my tools directory.  My tools directory holds scripts that I've written, and other generally-useful utility programs.  You'll want to add it to your path, so you have 2 options:

 

1) Use System Properties | Advanced | Environment Variables.  Create Path if it doesn't exist.  If it does exist, append the value of $tools to it.

2) Do it from a MSH prompt:

MSH C:\Temp >$newPath = $tools

MSH C:\Temp >if((get-property HKCU:\Environment).Path)

>> { $newPath = (get-property HKCU:\Environment).Path + ";" + $newPath }

>> set-property -Path HKCU:\Environment -Property Path -Value $newPath

>> 

MSH C:\Temp >(get-property HKCU:\Environment).Path

d:\lee\tools

Now, restart your MSH shell.  Your $tools directory is now part of your path.  If you haven't created the directory already, now is a good time to do so:

MSH C:\Temp >md $tools

As we create custom scripts, we'll place them in $tools to allow us to run them from wherever we like.

 

[Edit: Monad has now been renamed to Windows PowerShell. This script or discussion may require slight adjustments before it applies directly to newer builds.]

Comments [4] | | # 
 Monday, June 27, 2005
Tuesday, June 28, 2005 6:35:45 AM (Pacific Daylight Time, UTC-07:00) ( )

If you were one of the many that missed Jeffrey's standing-room-only presentations at TechEd, the kind folks at TechNet feel your pain. They've decided to host a series of "Best of TechEd" webcasts, and our two presentations are among them. If you want to learn more about the capabilities of Monad, be sure to check these out.

Session 1: Next Generation Command Line Scripting with Monad (Part 1 of 2) (Level 300)
This session provides an architectural overview of Monad, and begins to introduce many of its powerful features.
http://msevents.microsoft.com/CUI/WebCastEventDetails.aspx?EventID=1032277850&EventCategory=4&culture=en-US&CountryCode=US

Session 2: Next Generation Command Line Scripting with Monad (Part 2 of 2) (Level 300)
This session discusses some of the advanced scripting constructs, such as script blocks, dedicated streams, ubiquitous parameters, errors, exceptions, debugging, and tracing.
http://msevents.microsoft.com/CUI/WebCastEventDetails.aspx?EventID=1032277852&EventCategory=4&culture=en-US&CountryCode=US

 

Now, I would be remiss without including something for the hard-core geeks that already have Monad installed:

MSH C:\Temp >$organizer = "http://go.microsoft.com/fwlink/?linkid=41781"
MSH C:\Temp >$summary = "Monad TechNet Webcast"
MSH C:\Temp >$description = "Next Generation Command Line Scripting with Monad (Part 1 of 2)"
MSH C:\Temp >$location = "
http://msevents.microsoft.com/CUI/WebCastEventDetails.aspx?EventID=1032277850&EventCategory=4&culture=en-US&CountryCode=US"
MSH C:\Temp >$start = [DateTime]::Parse("Wednesday, August 03, 2005 09:30 AM")
MSH C:\Temp >$end = $start.AddHours(1).AddMinutes(30)
MSH C:\Temp >.\new-calendaritem $organizer $start $end $summary $description $location -out:TechEd_1.ics
MSH C:\Temp >
MSH C:\Temp >$description = "Next Generation Command Line Scripting with Monad (Part 2 of 2)"
MSH C:\Temp >$start = [DateTime]::Parse("Wednesday, August 10, 2005 9:30 AM ")
MSH C:\Temp >$end = $start.AddHours(1).AddMinutes(30)
MSH C:\Temp >$location = "
http://msevents.microsoft.com/CUI/WebCastEventDetails.aspx?EventID=1032277852&EventCategory=4&culture=en-US&CountryCode=US"
MSH C:\Temp >.\new-calendaritem $organizer $start $end $summary $description $location -out:TechEd_2.ics
MSH C:\Temp >
MSH C:\Temp >.\TechEd_1.ics; .\TechEd_2.ics


## new-calendaritem.msh
## Create a new calendar item using the iCal (.ics) calendar format
## new-calendaritem -organizer:"
http://go.microsoft.com/fwlink/?linkid=41781" -start:$([DateTime]::Now) -summary:"Monad TechNet Webcast" -description:"Monad TechNet Webcast: Next Generation Command Line Scripting with Monad (Part 1 of 2) (Level 300)" -location:"http://msevents.microsoft.com/CUI/WebCastEventDetails.aspx?EventID=1032277850&EventCategory=4&culture=en-US&CountryCode=US" -out:output.ics

param(
 [string] $organizer = "",
 [DateTime] $start = $(throw "Please specify a start time."),
 [DateTime] $end = $start.AddMinutes(30),
 [string] $summary = $(throw "Please provide a meeting summary."),
 [string] $description = "",
 [string] $location = $(throw "Please provide a meeting location."),
 [string] $out
     )


## We want to convert the times to Universal, so that we can be unequivocal about
## when they start.  The RFC supports appointments in the local time zone (ie: lunch,)
## but that's not very helpful when we're sharing most appointments.
## All input times will be assumed to be local times
$utcStart = $start.ToUniversalTime()
$utcEnd = $end.ToUniversalTime()

## Convert them to the ICS format
$dtStart = $utcStart.ToString("yyyyMMddTHHmmssZ")
$dtEnd = $utcEnd.ToString("yyyyMMddTHHmmssZ")

## A very simple calendar item
$template = @"
BEGIN:VCALENDAR
BEGIN:VEVENT
ORGANIZER:$organizer
DTSTART:$dtStart
DTEND:$dtEnd
SUMMARY:$summary
DESCRIPTION:$description
LOCATION:$location
END:VEVENT
END:VCALENDAR
"@

## Dump it out to the file or the screen
if($out)
{
   $template | out-file -encoding:ascii $out
}
else
{
   $template
}

 

[Edit: Monad has now been renamed to Windows PowerShell. This script or discussion may require slight adjustments before it applies directly to newer builds.]

Comments [0] | | # 
Tuesday, June 28, 2005 3:33:46 AM (Pacific Daylight Time, UTC-07:00) ( )

Adam has been fleshing out a great use of Monad -- by creating a mini command-line RSS reader!  It's touched on registry modification, XML parsing, internet access, and more.

Check it out, if any of those buzz words interest you :)  All we need to do is throw AJAX and Web 2.0 into the mix, and we'll have a home-run IPO!

[Edit: Monad has now been renamed to Windows PowerShell. This script or discussion may require slight adjustments before it applies directly to newer builds.]

Comments [0] | | # 
 Tuesday, June 21, 2005
Wednesday, June 22, 2005 6:45:53 AM (Pacific Daylight Time, UTC-07:00) ( )

An interesting thread came up on our BetaPlace newsgroup today, asking if MSH supports a SQL-like join command.  After all, we do already support the select-object cmdlet, which allows you to pick individual properties for display.  Given our display model (that often looks and feels like a SQL grid output,) this is a totally great question.  It's a feature I've always wanted in a shell, too.

We don't support the feature natively, but the MSH language sure does.

Although a true SQL join combines the colums of the tables you select, we don't really need that in the command line.  Instead, we'll implement the set-intersection capabilities of the join.  Given two sets of items, find the ones that meet some common criteria.  For those that match, output a result.

Let's look at a simple example:

Set1
Name, Number
Lee, 555-1212
Joe, 555-5555
Sam, 555-1234

Set2
Name, Number
Lee's Sister, 555-1212
Frank, 555-9876
Jim, 555-1029
Jed, 555-1234

Now, using SQL, let's see what numbers are being shared:

SELECT Number FROM Set1 WHERE Set1.Number = Set2.Number

Would give us the results:

Result
Number
555-1212
555-1234

Now, let's do that in MSH.  We'll pick all the items from our system32 directory that are also in the windows root:

MSH:171 C:\temp > $dirset1 = (get-childitem c:\winnt\system32)
MSH:172 C:\temp > $dirset2 = (get-childitem c:\winnt\)
MSH:173 C:\temp > join-object $dirset1 $dirset2 -where:{$firstItem.Name -eq $secondItem.Name}


    Directory: FileSystem::C:\winnt\system32


Mode    LastWriteTime            Length Name
----    -------------            ------ ----
-a---   Mar 25  2003                  2 desktop.ini
-a---   Mar 24 17:08              68608 notepad.exe
-a---   Mar 25  2003               8704 winhlp32.exe
d----   Jun 21 21:52                    config
d----   Apr 05 05:00                    IME
d----   Apr 05 12:05                    mui
d----   Apr 05 16:20                    SoftwareDistribution

As is the bane of a SQL join, this takes a very long time to complete on large data sets.  However, it works suprisingly well.  Here's the script:

## join-object.msh
## Outputs the intersection of two lists, based on a given property
##
## Parameters:
##    -First:  The first set to join with.  Defaults to the pipeline if not specified
##    -Second: The second set to join with.
##    -Where:  A script block by which to compare the elements of the two sets.
##       -$firstItem refers to the element from 'First'
##       -$secondItem refers to the element from 'Second'
##    -Output: An expression to execute when the 'Where' expression evaluates to 'True".
##             Defaults to outputting $firstItem if not specified.
##
## Examples:
## "Hello","World" | join-object -second:"World"
## join-object -first:"A","E","I" -second:"BAT","BUG","BIG" -where:{$secondItem.Contains($firstItem)} -output:{$secondItem}
##
## $dirset1 = (get-childitem c:\winnt)
## $dirset2 = (get-childitem c:\winnt\system32)
## join-object $dirset1 $dirset2 -where:{$firstItem.Name -eq $secondItem.Name}

param($first=@(), $second = $(throw "Please specify a target to join"), $where={$firstItem -eq $secondItem}, $output={$firstItem})

if(-not $first)
{
    foreach($element in $input) { $first += $element }
}

foreach($firstItem in $first)
{
   foreach($secondItem in $second)
   {
      if(& $where) { & $output }
   }
}

First, we check if the user has specified the 'First' dataset to use.  If not, we collect the data from the pipeline.  Then, we simply go through every item in the 'First' dataset.  For each of those items, we go through every item in the 'Second' dataset.  If our 'Where' clause evaluates to true, then we output as desired. 

What's mind-blowing is that the script could literally be written as a one-liner if you removed the error handling and comments.

[Edit: Monad has now been renamed to Windows PowerShell. This script or discussion may require slight adjustments before it applies directly to newer builds.]

Comments [1] | | # 
 Monday, June 20, 2005
Monday, June 20, 2005 7:34:09 AM (Pacific Daylight Time, UTC-07:00) ( )

The new YubNub service looks fairly interesting.  It's a concept that's been hashed out a million times by individual, isolated programs: use one search interface as a gateway to many others.  Given a keyword and a search term (ie: gim porsche 911,) the gateway will format your search term for use in a Google Images search.

Internet Explorer supports this via its SearchUrl feature.  MSN Desktop Search supports it through its Deskbar Shortcuts (community version here,) and I even wrote a small Javascript application called SearchPad to do the same.  The difference is that YubNub keywords are contributed by anybody, and available to anybody. 

That's a big difference.

In any case, John Ludwig recently lamented in a software roundup entry on Monad, "Wish you could plug yubnub commands into it."

Ask, and ye shall receive.  The script's name is a little awkward, so you'll probably want to alias it in your custom profile:

## search-yubnub.msh
## Search yubnub from your Monad shell
## For help, use "search-yubnub ge"

## Load the System.Web assembly
[void] [Reflection.Assembly]::LoadWithPartialName("System.Web")

$url = "http://www.yubnub.org/parser/parse?command={0}" -f [System.Web.HttpUtility]::UrlEncode($args)
[Diagnostics.Process]::Start($url)

It would be technically possible to implement a "community scripting" feature that would alias keywords to community-contributed MSH scripts, but I can't think of why you would want to open your system up to that kind of threat.

[Edit: Monad has now been renamed to Windows PowerShell. This script or discussion may require slight adjustments before it applies directly to newer builds.]

Comments [1] | | # 
 Saturday, June 18, 2005
Saturday, June 18, 2005 8:23:52 AM (Pacific Daylight Time, UTC-07:00) ( )

On a recent road trip to Calgary, I started to notice the left channel of my Nomad Zen Xtra becoming flaky.  Since I use it in my car stereo, my unlucky passenger usually got the duty of contorting the connection wire "just so" in order to let us hear both channels of sound.

Since I already voided my warranty by replacing the hard drive (30gb upgraded to 80gb,) I decided to look into fixing it myself.  I remember hearing about headphone problems on the internet, so I searched and stumbled upon this wonderful how-to guide on fixing the headphone problems.

Well, add another happy customer :)

I've got to really emphasize the point of using a wire to solder the left channel back in place. I used a wire approximately 1.5 inches long. Don't try simply re-seating the headphone jack with more solder, you'll give yourself more trouble than you need.

To get familiar with the connection points, first use the wire to bridge the left channel's solder point with the large gold pad at the top of the headphone jack. Play some music while holding the wire in place, and your sound should come back.

Don't feel that the wire needs to go directly from the solder pad to the gold contact. I first laid my wire flat on the PCB, soldering it parallel to the PCB -- running it alongside the headphone jack. That allows for a much stronger join. When that sets, curl the wire back around towards the copper backlight wire, give it a small loop, and then run it parallel to the long edge of the LCD. That gives you nice, easy access to the gold connection point at the top of the headphone jack.

Comments [0] | | # 
 Friday, June 17, 2005
Friday, June 17, 2005 2:15:51 PM (Pacific Daylight Time, UTC-07:00) ( )
I used to have download links here, but they were for an older version of Monad.  But I'm still getting plenty of hits per day for it, and needlessly sending people on a wild goose chase.  Since Thomas has been doing a great job of keeping his Reskit.net up to date with the latest Monad Download information, I'll point you to him for the best Monad Download information.

[Edit 01/25/06: Removed Beta download links, and instead point to Thomas' continually updated download links.]

[Edit: Monad has now been renamed to Windows PowerShell. This script or discussion may require slight adjustments before it applies directly to newer builds.]

Comments [4] | | # 
 Thursday, June 16, 2005
Friday, June 17, 2005 4:53:38 AM (Pacific Daylight Time, UTC-07:00) ( )

Preston wrote a piece, pointing to an interesting Web application called "WebCmd". WebCmd makes your browser act like a console window. You type in commands, it executes them on a remote server, and returns the results. Currently, a-i-studio accomplishes the web-based interactivity via Javascript, with the heavy lifting performed by a server-side Perl script that executes commands.

It's a neat idea, and leads to even better ones (as Preston pointed out.) If we could have a fully-functioning web terminal over SSL, who needs SSH? In addition, who needs to worry about having access to a terminal emulator in order to securely administer thier machine? Although it appears that this demo gets us 90% of the way to running VI, it's really more like 0.01%

Like I said earlier, WebCmd makes the front end appear to be responsive using Javascript. You see characters as you type them, you can backspace, and you see a flashing cursor (for a few examples.) However, no interaction with the remote machine actually happens until you press the 'Enter' key. At that point, WebCmd batches your input and sends it off to the server. Then, it collects the response and displays it for you.

This falls quite short of the infrastructure required to support an interactive program such as VI. Let alone Emacs :)  There's no support for cursor positioning, keyboard polling, and much more. For the Unix world, take a gander at the feature set of the VT100 Terminal Emulation Requirements. For the Windows world, look at the Win32 Console API. To make a fully interactive web-enabled front-end, you would need to implement the equivalent of cmd.exe in Javascript. The performance would be atrocious. So in the end, it's attainable -- but at a very large cost. Once you're done writing that much Javascript, you're likely to start rocking back and forth, whilst arguing with yourself.

However, all is not lost. If we drop the requirement (and support) for the high-degree interactivity, a web-enabled front end does become a useful administration utility. SSL secures the data in transport, and protects you from man-in-the-middle attacks. By using Windows authentication (along with the appropriate permission sets on the server,) you can even prevent users from doing things they shouldn't.

This is a scenario that Monad allows you to accomplish much more cleanly than simply redirecting standard input and output from a console application. At its core, we've designed the Monad engine to be hosted by other applications. As long as the host application implements the functions from the hosting interface (below,) it can run and host most Monad cmdlets.

For example, our MSH.EXE is simply a host for Monad. Several enthusiastic members on BetaPlace have written a GUI host for Monad. Exchange 12 is built on Monad. And to bring this all full circle: one of the members in our team wrote a proof-of-concept ASP.Net host for Monad.

[Edit: Monad has now been renamed to Windows PowerShell. This script or discussion may require slight adjustments before it applies directly to newer builds.]

Comments [5] | | #