Generating files with PowerShell

Link. April 9, 2008. Comments [0]. Posted in: PowerShell

Yesterday I needed to generate a bunch of small files to use as input for testing, as I needed to reproduce a bug I was tracking down. More to the point, I needed to generate 2000 files of size 781 bytes.

Update: I screwed up the code snippets on my first attempt. Fixed now!

Naturally, I turned to PowerShell and whipped this script:

$fc = new-object string ('a', 781)
1..2000 | %{ [io.file]::WriteAllText("$(pwd)\$_.txt", $fc) }

Is this the only way to do it? Certainly no, but a couple of things are worth mentioning about my specific solution. A savvy reader might ask: Why didn't you just use the redirection operator instead?

1..2000 | %{ $fc > "$_.txt" }

That is indeed shorter, but has one major drawback for my problem: The redirection operator when writing to files defaults to using UTF-16LE encoding, which meant my files would come out the wrong size.

Now, the redirection operator in this case is nothing more than a way to implicitly call the out-file cmdlet, which does provide a way to select the encoding:

1..2000 | %{ $fc | out-file "$_.txt" -enc ascii }

This is much better,  but, unfortunately, still screws up the file size because it will add a CR LF pair at the end. And it isn't remarkably shorter than my original solution using File::WriteAllText().

Async Pipelines and PipelineReader<T> Issues

Link. April 7, 2008. Comments [0]. Posted in: .NET | PowerShell

I've been spending some time this week coding some changes to a custom PowerShell PSHost for an application. One of the changes I wanted to experiment with was changing the code that executed commands so that it used Pipeline.InvokeAsync() instead of Pipeline.Invoke().

There are a couple of things that need to be handled different in this case: How you process the results from the pipeline and how you handle errors. I'll concentrate on the first one, as it is the one that caused me a bit of trouble to get right.

To process the results from an asynchronous pipeline invocation, you need to use a PipelineReader<PSObject> object, which is what the Pipeline.Output property returns. This allows you to read objects generated by the pipeline execution as they are coming out (i.e. as soon as they are available) instead of waiting until the entire pipeline has executed to grab the results, so the idea is pretty nifty.

Unfortunately, the documentation on how to use this object correctly isn't very good. For example, you can't rely on the Count or IsOpen properties as boundary checks to detect how many items to attempt to read. In particular, the Count property isn't reliable if you're using Pipeline.InvokeAsync() because it only represents how many objects are currently available in the reader, not the total count of objects returned by the pipeline (this is natural once you realize it, but still).

Instead, you should really rely on the EndOfPipeline property of PipelineReader<T> to detect when you've reached the end of the object stream generated by the pipeline execution.

The second issue that's not very obvious is that when you use Pipeline.Invoke(), but you don't need to feed inputs to the command, then the pipeline won't really start executing until you close the PipelineWriter object returned by Pipeline.Input. If you don't do this, then PipelineReader.Read() will simply block forever.

Phantom Objects

The one nasty issue I did run is what appears to be a synchronization issue inside PipelineReader<T> itself. In my original attempt to use Pipeline.InvokeAsync(), I started getting some weird results: Ghost objects were coming out of the reader.

Ghost objects?

Pretty much, yes. Let's say I executed an "ls" command on my pipeline that should return 8 items. Sometimes, I'd indeed get the expected 8 items out of the pipeline before EndOfPipeline changed to true. Other times, however, I'd see 9 items come out of it.

The last item was a "ghost" object that was empty: a PSCustomObject with no properties at all. Where was it coming from?

The only good thing about this was that if it appeared at all, it always did it as the last place in the pipeline. This gave me a clue: Could this be a marker object inserted internally by PowerShell into the object stream to mark the end of the pipeline? It sure looked like some kind of null value.

. It is, in fact, AutomationNull.Value, which, although defined in System.Management.Automation.Internal, is a public type/property.

The reason I say this problem is a synchronization issue is that, for the user of PipelineReader<T>, the use of this marker object should've been transparent. Instead, it is a leaking abstraction that sometimes (and just sometimes!) gets exposed and returned from PipelineReader.Read() when it should never happen!

In the end, I ended up rewriting my code like this to work around this problem:

PipelineReader<PSObject> results = pipeline.Output;
while ( !results.EndOfPipeline ) {
   PSObject obj = results.Read();
   // check that the object returned isn't
   // $null, signaling the end of the pipeline
   if ( obj != AutomationNull.Value )
      // do something with the object
}

Editing PowerShell Scripts with Vim

Link. March 18, 2008. Comments [0]. Posted in: PowerShell | Vim

I've been using Vim to edit my PowerShell scripts for a while. I get full syntax highlighting and indentation thanks to Peter Provost's excellent scripts:

These work great most of the time, but a couple of things had been nagging me for a while:

  1. I occasionally enable syntax-based folding (:set foldmethod=syntax), but the PS1 syntax file doesn't enable this for blocks "{...}" in PowerShell scripts.
  2. The indent file always forces comments (#...) to start at the first column.

Fortunately, both of these issues are pretty easy to fix. To enable syntax-based folding, I just modified the syntax file to add this:

" support folding for blocks
syntax region  psBlock      start="{" end="}" transparent fold

To disable the comment indentation, I edited the Indent file and remove the # character as an indent key:

setlocal cindent cinoptions& cinoptions+=+0 cinkeys-=0#

Seems to be working fine for me, and it will now stop driving me crazy :-).

PowerShell Fortune

Link. March 17, 2008. Comments [0]. Posted in: PowerShell

The first version of Linux I ever used was Slackware 2.3 running one of those pesky 1.X kernels. Since then, one of my all-time favorite utilities has been the Fortune program, which displays quotes on the console when run.

fortune

I've always missed it on windows and even build (and lost) a clone of it myself once, but I'm too lazy now to try that again. Instead, this time around I simply settled for writing a simple PowerShell script that grabs a random quote from QuoteDB. It's not fancy. It has no error checking at all. It's slow (depending on your network connection and how loaded quotedb is), but alas, it works and it's fun to use.

I give you fortune.ps1:

$wc = new-object net.webclient
$js = $wc.DownloadString('http://www.quotedb.com/quote/quote.php?action=random_quote&=&=&')

function strip-html([string] $str) {
   $val = $str -replace '<[^>]+>', ''
   $val = $val -replace '`', "'"
   return $val
}

function next-word([string] $text, [int] $start) {
   $end = $start
   for ( ; $end -lt $text.Length; $end += 1 ) {
      if ( $text[$end] -eq ' ' ) {
         break
      }
   }
   return $text.Substring($start, $end - $start)
}

function wrap-text([string] $text) {
   $buf = new-object Text.StringBuilder
   $lnl = $host.UI.RawUI.WindowSize.Width - 2
   $pos = 0
   $linepos = 0
   while ( $pos -lt $text.Length ) {
      $word = (next-word $text $pos)
      if ( $linepos + $word.Length -gt $lnl ) {
         [void] $buf.Append("`n")
         $linepos = 0
      }
      [void] $buf.Append($word + ' ')
      $pos += $word.Length + 1
      $linepos += $word.Length + 1
   }
   write $buf.ToString()
}


if ( $js -ne $null ) {
   $js = $js.trim()
   $p1 = "^.+'(?<text>.+)<br>'\);"
   $p2 = '">(?<author>.+)</a>'

   $authorline = $js.Substring($js.LastIndexOf("`n"))
   $maintext = $js.Substring(0, $js.LastIndexOf("`n"))
   if ( $maintext -match $p1 ) {
      $matches.text.Split("`n") | %{
         wrap-text (strip-html $_)
      }
   }
   if ( $authorline -match $p2 ) {
      write "`t`t-- $($matches.author)"
   }
}

Dev Environment for PowerShell

Link. March 13, 2008. Comments [0]. Posted in: .NET | PowerShell

A few people have asked already about my PowerShell script for configuring a development environment for .NET / Visual Studio / SDK work, so I thought I might as well break it into it's own script.

Here it is:

###############################################################################
# Configures the .NET / Visual Studio / Windows SDK
# Build environment. Loosely based on the SDK batch files.
#
# First it will try to set up the environment for .NET 3.5
# and VS2008. Failing that, falls back to .NET 3.0/VS2005.
###############################################################################

$NETFXDIR = "$env:WINDIR\Microsoft.NET\Framework"
$FX20 = "$NETFXDIR\v2.0.50727"
$FX35 = "$NETFXDIR\v3.5"

function script:append-path {
   $env:PATH += ';' + $args
}
function script:append-lib {
   if ( test-path('Env:\LIB') ) {
      $env:LIB += ';' + $args
   } else {
      $env:LIB = $args
   }
}
function script:append-include {
   if ( test-path('Env:\INCLUDE') ) {
      $env:INCLUDE += ';' + $args
   } else {
      $env:INCLUDE = $args
   }
}
function script:get-vsdir([string] $version) {
   $regpath = "HKLM:SOFTWARE\Microsoft\VisualStudio\$version"
   if ( test-path($regpath) ) {
      $regKey = get-itemproperty $regpath
      return $regkey.InstallDir
   }
   return $null
}
function script:set-vsenv([string] $version) {
   $VSDIR = (get-vsdir $version)
   if ( $VSDIR -ne $null ) {
      append-path $VSDIR
      append-path "$VSDIR..\..\VC\bin"
      append-path "$VSDIR..\Tools"
      
      append-include "$VSDIR..\..\VC\include"
      append-lib "$VSDIR..\..\VC\lib"
      return $true
   }
   return $false
}
function script:get-psdkdir {
   $regpath = "HKLM:SOFTWARE\Microsoft\Microsoft SDKs\Windows\"
   if ( test-path($regpath) ) {
      $regKey = get-itemproperty $regpath
      return $regkey.CurrentInstallFolder
   }
   return $null
}
function script:set-psdkenv {
   $sdkdir = (get-psdkdir)
   if ( ($sdkdir -ne $null) -and (test-path $sdkdir) ) {
      append-path "$sdkdir\bin"
      if ( test-path "$sdkdir\include" ) {
         append-include "$sdkdir\include" 
      }
      if ( test-path "$sdkdir\lib" ) {
         append-lib "$sdkdir\lib"
      }
   }
}

set-psdkenv
# if .NET 3.5 is installed, default to that, otherwise use 2.0
if ( test-path($FX35) ) {
   append-path $FX35
}
append-path $FX20
if ( -not (set-vsenv "9.0") ) {
   [void] (set-vsenv "8.0")
}

Feel free to customize as you see fit :-).

Syndicate

About

Tomas Restrepo is a software developer located in Colombia, South America. His interests include .NET, Connected Systems, PowerShell and lately dynamic programming languages. More...

tomasrestrepo @ twitter My Flickr photostream My saved links on delicious My Technorati Profile

email: tomas@winterdom.com
msn: tomasr@passport.com

View my profile on LinkedIn

MVP logo

Ads


Links

Categories

Statistics

Total Posts: 1022
This Year: 92
This Month: 11
This Week: 0
Comments: 792

Blogroll

Post Archive

Other

Copyright © 2002-2008, Tomas Restrepo.

Powered by: newtelligence dasBlog 2.1.8139.823

Sign In