Archives for the Month of July, 2005

Ideas to Cut Off Duplicate Questions in Forums

Josh Ledgard recently wrote an excellent post: Two Ideas to Cut off Duplicate Questions in Online Forums. It deals with a problem that clogs almost every technical community: some users treat the group as their personal support oracle. This happens in online forums, mailing lists, IRC, blogs, and more. Heck, I'd say that 70% of the questions posted to our internal Microsoft mailing lists could have been solved with a few minutes of research. Got an error message? Post something to the group. Computer looking at you funny? Post something to the group. Not sure what to have for lunch? Post something to the group.

The community researchers that answer questions on Google Answers make their money off of exactly this type of ignorance. Although some of the answers require above-average searching abilities, others turn up on the first page of search results.

Hence the exhasperated shouts of RTFM, Just Google It, and others. Those don't help, though. They show you to be a pompous jerk who likes to belittle others. The solution is to either gracefully help change the user's habits (so that they do more homework before asking a question,) or seamlessly integrate the research into their habits.

Josh's idea covers the latter. He suggests that sites (such as the MSDN Forums) integrate a little live search feed to the right of the "Post a Question" form. It's a great idea.

Now, here's a little compressed version of the Microsoft product cycle. Being a Program Manager at Microsoft, he was nice enough to even draft a rough mock-up.

Now, being a developer at Microsoft, the next step in a project is to mock up a hack-and-slash technical prototype. At this point, you're really just trying to prove to yourself (and other technical folks) that the idea is possible. These prototypes also give you a basis from which to make a development time estimate for the feature. I've posted it here, although there was really no question that this specific feature is technically possible.

Next, you might want to construct a hi-fidelity prototype to show others. The guts are still hack-and-slash, but the interface is much more functional. Usability tests, and executive demos are a common target for this class of prototypes. I've posted it here.  HTML is a great prototyping medium for UI, as is Macromedia Flash.

When you understand enough about the feature to know what you want, the ball again sits in the Program Manager's court for detailed functional specification. From there, it goes to the UI team for detailed colour and layout specification. Finally, it goes to development and testing for the full-fledged implementation.

Another way to get Monad — WinFX

For those of you that haven't yet downloaded Monad through BetaPlace, you now have another option -- Beta1 of the WinFX SDK!  This is not an NDA release, unlike BetaPlace.  Here's a direct link to the download page.

"The WinFX SDK contains documentation, samples and tools designed to help you develop managed applications and libraries using WinFX, which is the set of next-generation managed APIs provided by Microsoft."

If you decide to install only portions of the SDK, Monad is one of the products installed in the "Tools and Build Environment" part of the package.

Let me say one thing, though.  We take your input very seriously.  The comments and feedback that you provide to us in the BetaPlace newsgroup helps us make the product better for you, and future customers.  If you install via the WinFX SDK, please still make the effort to let us know what you think via email, and comments in our blogs.

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

System Administrator Appreciation Day

Today is System Administrator Appreciation DayWikipedia summarizes it as:

System Administrator Appreciation Day, also known as Sysadmin Day or SAAD (as in Happy SAAD!), falls on the last Friday in July. It exists solely to show appreciation to sysadmins and people with other similar occupations.
[...]
Typical observances of this holiday are to present gifts to your Sysadmin. These gifts include chocolate, beer, wine, electronic toys, video games, and cake & ice cream.

So, reward your local SysAdmin!  Open your Monad prompt, type get-process a few times, then go buy yourself chocolate, beer, wine, electronic toys, video games, cake, and ice cream.

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

Command-line shortcut for repetitive operations

There are times when you might want to do the same thing many times at the command line.  You normally would use a counted for loop:

MSH:19 C:\temp\monad > for($x = 0; $x -lt 5; $x++) { do-something }

But here's a neat little trick to save some typing, if you don't care which iteration of the loop you're in:

MSH:19 C:\temp\monad > 1..5 | foreach { do-something }

This is a bloated and slow way to do a for loop, though, so don't use it in scripts.

The 1..5 expression creates an array of 5 elements, using the numbers 1 through 5.  Then, we pipe it to foreach-object -- which then performs "do-something" for each element in the array.

For more neat things you can do with arrays, type 
   get-help about_Array
at your prompt.

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

"May I please test Longhorn Beta 1?"

You've got to love the enthusiasm surrounding Longhorn.  In the words of one of its beta coordinators:

"This is the first time in 10+ years involvement with customer beta programs that I’ve received an unsolicited bribe to get on the program– two pounds of inscribed chocolate showed up at my office via Fedex today."

The chocolate was shared far and wide -- I'll bet it satisfied at least 1 or 2 mid-afternoon sugar cravings 🙂

(To be clear, this was not from the manufacturer of said sugar-buzz-inducing-decadence.)

[ Edit: Adam also blogged about this here ]

Regular Expressions in Monad

SteveX has a Monad forum on his site, and one of the first topics posted (by Ayende) was an example script on using regular expressions in Monad.

This is helpful example of how well Monad integrates with the .Net framework.  You can call out to [System.Text.RegularExpressions.Regex], and port your C# regular expression almost effortlessly.

But wait, it only gets better.

Regular Expressions are a glorious beast of burden in scripting languages, as you might have noticed if you've read much Perl.  Because of that, we've given them first-class language support via the "-match" expression.

Let's start with a simple example:

MSH:40 C:\Temp >"Hello World" -match "hello"
True

The match evaluates to $true if the match was successful, $false otherwise.  Notice that regular expressions (via the -match expression) are case insensitive by default.  If you want case sensitive operation, the -cmatch expression is the one you want to look toward:

MSH:41 C:\Temp >"Hello World" -cmatch "hello"
False

Once the match expression evaluates your regular expression, it places the resulting matches (called groups) in an appropriately named variable, called $matches:

MSH:43 C:\Temp >$matches

Key                            Value
---                            -----
0                              Hello

MSH:46 C:\Temp >"Hello World" -match "hello.*or"
True
MSH:47 C:\Temp >$matches

Key                            Value
---                            -----
0                              Hello Wor

As with the .Net method, $matches[0] always holds the entire match of your regular expression.  Regular expression matches can get much more complex -- such as when they contain multiple capture groups, or even named ones:

MSH:50 C:\Temp >"Hello World" -match "h(ell)o(?<named1>.*)(?<named2>or)"
True
MSH:51 C:\Temp >$matches

Key                            Value
---                            -----
named2                         or
named1                          W
1                              ell
0                              Hello Wor

This is how Monad (via .Net) chose those groups:

  • Match '0', as always, is the largest substring match of the regular expression.
  • Match '1' is the un-named capture at the beginning of the regular expression.
  • Match 'named1' is the first named capture in the middle of the regular expression.
  • Match 'named2' is the second named capture in the middle of the regular expression.

Monad uses .Net's regular expression facilities, so keep this great MSDN Regex Reference handy.  From there, memorize obscure minutea such as the following to dazzle family and soon-to-be-ex-friends:

Named captures are numbered sequentially, based on the left-to-right order of the opening parenthesis (like unnamed captures), but numbering of named captures starts after all unnamed captures have been counted.

This becomes a pretty powerful mechanism for slicing and dicing text content.  How about a command-line address book?

MSH:79 C:\Temp >get-content address_book.csv
Joe,555-1212
Frank,555-1234
Ella,555-314159265359

MSH:80 C:\Temp >$outputBlock = { "Name: " + $matches["name"] + ", Number: " + $matches["number"] }
MSH:81 C:\Temp >get-content address_book.csv | `
>> foreach-object { if ($_ -match "(?<name>Ella),(?<number>.*)") `
>> { & $outputBlock } }
>>
Name: Ella, Number: 555-314159265359

So, to close the loop -- we can write the original script:

if($args.Count -lt 2)
{
   "Arguments are: "
   break
}

$file = [string]$args[0]
$pattern = [string]$args[1]
$re = new-object System.Text.RegularExpressions.Regex($pattern)
foreach ($line in $(Get-Content $file))
{
   $match = $re.Match($line);
   if($match.Success)
   {
      $match.Value
   }
}

like this, using Monad's built-in language support.

param(
   [string] $filename = $(throw "Please specify a filename."),
   [string] $regex = $(throw "Please specify a regular expression.")
     )

foreach ($line in (get-content $filename))
{
   if($line -match $regex)
   {
      $matches[0]
   }
}

Pretty nice!

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

Param statement, and new-object

Vivek recently posted a helpful script to get the contents of a web page.

 

As is common with .Net and Monad, there is more than one way to do it.  In addition, there are also two Monad facilities we can take advantage of to make the script more readable: new-object, and the param statement.

 

First of all, the easy one.  The new-object cmdlet creates a new object.  In the current drop, it only creates .Net objects, but COM support is coming soon.  This cmdlet replaces the calls to [Activator]::CreateInstance in Vivek's script.

 

Next, the param statement.  I used it in my new-calendarItem script, and it looks like this:

 

## test-params.msh

## Test the param facility

 

param(

   [string] $importantParameter = $(throw "Please specify the important parameter!"),

   [int] $countOfThings,

   [DateTime] $recordDate = [DateTime]::MaxValue

   )

  

"ImportantParameter was $importantParameter"

"CountOfThings was $countOfThings"

"RecordDate was $recordDate"

 

It looks similar to a function definition in many languages, but offers more benefits.  The param statement allows you to define the parameters to your script.  In the simplest case, your user supplies values for all of your parameters.  When they do this, Monad interprets them positionally for you.  That is, each parameter gets assigned from the value passed at that position on the command line.  For example:

 

MSH:130 C:\Temp >test-params First 123 "Tuesday, July 12, 2005"

ImportantParameter was First

CountOfThings was 123

RecordDate was 7/12/2005 12:00:00 AM

 

Notice how Monad coerces the data types, if it can -- as with the DateTime value in the third position.

 

In your script, you simply refer to your parameters by the names you gave them: $importantParameter, $countOfThings, and $recordDate.

 

For some parameters, you might want to supply your user with sensible defaults.  In that case, Monad allows you to specify a default value for your parameter, as with $recordDate above:

 

MSH:135 C:\Temp >test-params First 123

ImportantParameter was First

CountOfThings was 123

RecordDate was 12/31/9999 11:59:59 PM

 

However, your users don't always know how to interact with your script.  You might have parameters that should always be populated, such as $importantParameter in the example above.  In that case, we specify a special default value for that parameter -- an exception that tells them how to resolve the problem:

 

MSH:136 C:\Temp >test-params

 : Please specify the important parameter!

At d:\lee\tools\test-params.msh:5 char:42

+    [string] $importantParameter = $(throw  <<<< "Please specify the important parameter!"),

 

The param statement also gives you support of named parameters for free.  Named parameters let your user specify the parameters by name, rather than position.  Your parameter's variable name becomes the parameter name:

 

MSH:141 C:\Temp >test-params -RecordDate:$([DateTime]::MinValue) `

>>    -ImportantParameter:"Important!" `

>>    -CountOfThings:123

>> 

ImportantParameter was Important!

CountOfThings was 123

RecordDate was 1/1/0001 12:00:00 AM

 

As with regular cmdlets, you need only to specify enough of the parameter to differentiate it from the others:

 

MSH:152 C:\Temp >test-params -r:$([DateTime]::MinValue) -i:"Important!" -c:10

ImportantParameter was Important!

CountOfThings was 10

RecordDate was 1/1/0001 12:00:00 AM

 

This also helps Monad disambiguate between multiple optional parameters:

 

MSH:153 C:\Temp >test-params "Important!" -r:$([DateTime]::MinValue)

ImportantParameter was Important!

CountOfThings was 0

RecordDate was 1/1/0001 12:00:00 AM

 

MSH:154 C:\Temp >test-params "Important!" -c:123

ImportantParameter was Important!

CountOfThings was 123

RecordDate was 12/31/9999 11:59:59 PM

 

So, given this new-found knowledge, this is how we might rewrite Vivek's example.  First, the System.Net.WebClient way, as newly implemented in V2 of the framework:

 

## get-uri.msh

## Get a web page

 

param(

   [string] $uri = $(throw "Please specify an URI to retieve"),

   [string] $userAgent = "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.2; .NET CLR 1.0.3705;)"

   )

 

$wc = new-object System.Net.WebClient

$wc.Headers.Add("user-agent", $userAgent)

$wc.DownloadString($uri)

 

and the System.Net.HttpWebRequest way:

 

## get-uri.msh

## Get a web page

 

param(

   [string] $uri = $(throw "Please specify an URI to retieve"),

   [string] $userAgent = "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.2; .NET CLR 1.0.3705;)"

   )

 

$request = [System.Net.WebRequest]::Create($uri)

$request.Headers.Add("user-agent", $userAgent)

$response = $request.GetResponse()

$requestStream = $response.GetResponseStream()

$readStream = new-object System.IO.StreamReader $requestStream

$readStream.ReadToEnd()

$readStream.Close()

$response.Close()

 

If we need the content split into an array (as with Vivek's script,) we can write:

 

MSH:158 C:\Temp >$lines = (get-uri http://www.microsoft.com).Split("`n")

MSH:159 C:\Temp >$lines[0]

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" >

 

For the purpose we need it, I much prefer the System.Net.WebClient way.

 

[Edit: Thanks, Toby, for pointing out the WebClient / HttpWebRequest switch]

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

Monad Hosting Part II — Stop acting like cmd.exe!

Travis recently posted a good comment:

... I would *love* to be able to configure Monad's tab completion behavior! ...

So would we.

Tab completion is just one of the many things that would be great to change about the default Monad console host.  Other things that drive us crazy are the lack of standard CUA bindings (ie: Copy, Paste, keyboard-based selection,) and full Unicode support, to name a few.  It's comes down to a matter of priority.

As I mentioned in an earlier post, Monad can be hosted by any managed application that wishes to do so.  Hosting applications need only to implement a fairly simple interface in order to host the engine.  Our default host, msh.exe, owes nearly all of its functionality to the fact that we use the same infrastructure that cmd.exe does.  With relatively little code, we get passable tab completion, history cycling, and a bevy of function keys that are pretty useful.  Changing any of this means writing huge gobs of code.

We did write some special code to tab-complete cmdlet names, but that was a fairly isolated change.

For this release, we've focused our effort on really fleshing out the underlying Monad engine, APIs, and infrastructure.  These are the things that hurt the most when they are missing, and ultimately empower many, many more of you.  Writing a more functional host is on our list of things to do in a future version.

That said, this is the type of thing that the Monad community could easily astound us with.  Several BetaPlace members started writing a GUI host during our much earlier betas, but I haven't seen much activity on it lately.  A Microsoft empoyee wrote a neat WinForms application that hosts Monad in a tabbed interface.  Do you want transparency?  Tabs?  Background images? Awesome tab completion?  Go for it!  Our team (via the BetaPlace newsgroups) loves to help, should you want it or need it.

 

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

A History Browsing Prompt

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.]

** WARNING ** MSH may make you _too_ efficient

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.]