Tag Archives: PowerTips

PowerShell PowerUser Tips: The Hash TabExpansion

One of my favorite features in the built-in TabExpansion in PowerShell is one that many (if not most) users are unaware of: the “#” hash mark.

In order to test this tip, you’re going to need a command console that you’ve used and typed several commands into, so that Get-History will return more than a few different commands. Now, actually run the Get-History command so you can see the list of your last few commands.

The basic tip is pretty simple: if you type “#” and then hit the Tab key, PowerShell completes the full previous command-line.

You can also hit tab again to complete the next oldest command-line, and so on, right back to the beginning (it actually wraps around). You can even hit Shift-Tab to reverse direction if you go past the command-line you wanted. Additionally, this works on your history, so it even completes multi-line items. The one weird thing is that if you tab-complete past an item with multiple lines, the TabExpansion function doesn’t realize the cursor’s not on the prompt line anymore, so it doesn’t quite redraw right, but it’s mostly ok: the commands still work.

Of course, if that’s all there was to this, I’d just have tweeted and gone back to preparing for my presentation at the Windows Server 2012 Launch Event in Rochester

The really cool thing is that you can filter the feature. That is: if you type # and then the first few characters of some command-line in your history, when you hit tab you will get the most recent command-line that starts with those characters, and as before, you can hit tab repeatedly to cycle through all the commands in your history that match.

There’s one more part to the hash-tab feature: numbers. If you know the history id of the command you want to type, you can type, for instance, #20{Tab} to complete the 20th command from your PowerShell session. It’s basically the same as using “r” shortcut for invoke-history, except you hit tab after the number instead of space before, and you get to see the command (and edit it) before you press Enter.

So to sum up:

  • hash-tab – completes command-lines from your history
  • hash-txt-tab – filters your history like get-history | where { $_.Commandline -Like txt* }
  • hash-id-tab – completes the command from history with the matching id

PowerBoots: The tutorial walkthrough

[new] Updated to PowerBoots 0.1

An introduction to PowerBoots

Please excuse me if I start by just copying the basic ideas of the Shoes Tutorial, but I figured that since PowerBoots is inspired by Shoes, that was as good a place as any to start. PowerBoots (or just “Boots”) is a PowerShell 2.0 module with functions for writing Windows Presentation Framework (WPF) applications in the PowerShell scripting language. You should get the latest version of PowerBoots before continuing, and install it by putting the “PowerBoots” folder in one of your “Modules” folders (list them by typing $Env:PSMODULEPATH in PowerShell v2).

Don’t forget to start PowerShell.exe with the STA parameter (This is no longer required in PowerBoots 0.1).

Did I hear someone ask what is WPF? It was introduced as part of .Net 3.0 (and vastly improved in .Net 3.5), so you can expect to find it preinstalled on computers from Vista on, and of course you can download and install it on XP if it’s not already installed. The only thing you really need to know about WPF for the purposes of this tutorial is that it is the new GUI toolkit for .Net, and that it is container based — you put elements into other elements to control the layout, rather like HTML and Java Swing… you can pick up the rest as we go along.

A simple Boots program


New-BootsWindow -SizeToContent WidthAndHeight -Content {
   Button -Content "Push Me"
}
 

I’ve recently changed this first example to pass a script block to the content instead of using parentheses, because although PowerBoots supports running in an MTA PowerShell host, the only UI-creation command that is MTA-safe is the New-BootsWindow command. All of the “new” functions must then be inside of a scriptblock which is passed to the New-BootsWindow cmdlet and executed on that STA thread. You can use parenthesis ( and ) as a container instead, but that requires the host to be in STA threading mode (run: PowerShell -STA) so the controls can be created:


New-BootsWindow -SizeToContent WidthAndHeight -Content (
   Button -Content "Push Me"
)
 

This first example is a bit uglier than the Shoes syntax, so lets see if we can’t clean it up some. The -Content parameter is positional, so the first non-named argument you pass will be used for that. The same is true for the -Children parameter of panels, and in fact, each of the other similar parameters: Items, Blocks, and Inlines.

We have used a function New-BootsWindow which has an alias Boots. Boots takes all the same parameters as the Window function mentioned previously, but it uses slightly more useful defaults, and has a few other major benefits as well, the first of which is that it automatically “shows” the window, and the second is that it supports an -Async parameter which allows the window to come out in a new thread so that you can continue using PowerShell while the window remains alive and responsive. There is one catch: New-BootsWindow cannot take it’s content on the pipeline (the old function, now renamed “Out-BootsWindow” can take pipeline content, but is a script function, and requires -STA mode) — you have to specify it as a ScriptBlock. So now that we know this, we can rewrite our first example like this:


Boots { Button "Push Me" }
 

Just for the record, the simplest Boots program would just be a simple popup dialog to put some text in a Window, like: Boots { $msg }

We can put controls in a stack


Boots {
   StackPanel {
      Button "A bed of clams"
      Button "A coalition of cheetas"
      Button "A gulp of swallows"
   }
}
 

StackPanels are awesome. So are WrapPanels. Try that code with a WrapPanel instead of a StackPanel and see what the difference is. This brings up another point: those positional parameters we mentioned earlier: Content, Children, Items, Blocks, and Inlines, are also set to accept the value from the pipeline. Not only that, but they are intelligent about whether or not the content model accepts multiple items! So we can actually rewrite that script like this, and get the same results:


Boots { "A bed of clams", "A coalition of cheetas", "A gulp of swallows" | Button | StackPanel }
 

Now we’re really onto something! Continue reading

PowerShell PowerUser Tips: List the Cmdlets in an Assembly

Tiny script… let me know if you know a better way.


function Get-Cmdlets {
param([System.Reflection.Assembly]$assembly)

$assembly.GetTypes() | Where-Object {
     $_.GetCustomAttributes([System.Management.Automation.CmdletAttribute],$false)
} | ForEach-Object {
     $type = $_
     $_.GetCustomAttributes([System.Management.Automation.CmdletAttribute],$false)  
} | Select VerbName, NounName, @{n="Type";e={$type}} |
}

## Example usage.
## You can use the [System.Reflection.Assembly]::Load... methods to get an assembly
## But for an example, use the "CallingAssembly" (System.Management.Automation )
Get-Cmdlets ([System.Reflection.Assembly]::GetCallingAssembly()) |
     Sort VerbName, NounName | Format-Table -auto
 

PowerShell Power User Tips: A Better Prompt

For this edition of my Power User tips for PowerShell, I’m going to share my (heavily annotated) prompt function. Feel free to to copy useful pieces or just place the whole thing in your profile script :) I’m not going to say anything more, I’ll let the comments speak for themselves.

Edit: Someone just pointed out that I forgot the bit of my prompt that sets my current path into the window title, and I realized I also forgot the bit that puts (Admin) in the title if you’re running “elevated” on Vista.

[new] Edit: Ok, how many people noticed that I incorrectly used the Environement.CurrentDirectory when I set the WindowTitle (meaning it would only work right in FileSystem drives)? Fixed now.


# Set-Prompt.ps1 (Dot-Source from your profile)
###################################################
# This should go OUTSIDE the prompt function, it doesn't need re-evaluation
# We're going to calculate a prefix for the window title
# Our basic title is "PoSh - C:\Your\Path\Here" showing the current path
if(!$global:WindowTitlePrefix) {
   # But if you're running "elevated" on vista, we want to show that ...
   if( ([System.Environment]::OSVersion.Version.Major -gt 5) -and ( # Vista and ...
         new-object Security.Principal.WindowsPrincipal (
            [Security.Principal.WindowsIdentity]::GetCurrent()) # current user is admin
            ).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) )
   {
      $global:WindowTitlePrefix = "PoSh (ADMIN)"
   } else {
      $global:WindowTitlePrefix = "PoSh"
   }
}

function prompt {
   # FIRST, make a note if there was an error in the previous command
   $err = !$?

   # Make sure Windows and .Net know where we are (they can only handle the FileSystem)
   [Environment]::CurrentDirectory = (Get-Location -PSProvider FileSystem).ProviderPath
   # Also, put the path in the title ... (don't restrict this to the FileSystem
   $Host.UI.RawUI.WindowTitle = "{0} - {1} ({2})" -f $global:WindowTitlePrefix,$pwd.Path,$pwd.Provider.Name
   
   # Determine what nesting level we are at (if any)
   $Nesting = "$([char]0xB7)" * $NestedPromptLevel

   # Generate PUSHD(push-location) Stack level string
   $Stack = "+" * (Get-Location -Stack).count
   
   # my New-Script and Get-PerformanceHistory functions use history IDs
   # So, put the ID of the command in, so we can get/invoke-history easier
   # eg: "r 4" will re-run the command that has [4]: in the prompt
   $nextCommandId = (Get-History -count 1).Id + 1
   # Output prompt string
   # If there's an error, set the prompt foreground to "Red", otherwise, "Yellow"
   if($err) { $fg = "Red" } else { $fg = "Yellow" }
   # Notice: no angle brackets, makes it easy to paste my buffer to the web
   Write-Host "[${Nesting}${nextCommandId}${Stack}]:" -NoNewLine -Fore $fg
   
   return " "
}

Ideas for Writing Composable PowerShell scripts

I was just having some fun with some recent blog posts…

WPF & PowerShell – Part 5 has a script for “Get-Listbox” and for “Show-Control” and Halr9000 wrote a script he called Get-PSBlogroll

I had modified the example from the WPF post to create a listbox which will “start” whatever you double click …


Get-Listbox Title | Show-Control @{
   "MouseDoubleClick" = { [Diagnostics.Process]::Start( $window.Content.SelectedItem ) }
}

And I tried something like this to let me launch links from hal9000’s blogroll:


Get-PsBlogRoll | % { $_.HtmlUrl } | Get-Listbox | Show-Control @{
   "MouseDoubleClick" = { [Diagnostics.Process]::Start( $window.Content.SelectedItem ) }
}

Of course, what I’d like is to see the titles, instead of the URL, but because of how Get-ListBox was written, it only accepts strings, and therefore only returns strings. I tried adding the URL as a NoteProperty on the title string, but there’s a bug in the script cmdlet implementation that strips ETS properties. So if you specify your parameter type as a [string], you loose the NoteProperty.

Idea 1: Use [PsObject] for your pipeline parameter

Of course, if they fix the bug this tip will go away, but in the meantime, you’re better off not casting the input from the PowerShell native [PsObject] type, because you loose any extended type system attributes (and if you cast XML to a string, it comes out yucky).

Idea 2: Don’t output xml nodes

Even after fixing the Get-Listbox script so it outputs what it gets in without casting it, I still have to craft PsObjects with the output from Halr9000’s script, because otherwise Get-ListBox just outputs the .ToString() representation of the xml node, which is invariably useless — I could data-bind to the xml attributes, but it requires a separate syntax in the XAML — which means you’d have to have separate code for xml vs objects. So basically, you’re better off with a | Select * at the end of your Get-PsBlogRoll.

Idea 3: Display scripts should take a property (or a property list)...

What I eventually ended up doing is rewriting Get-Listbox to take a “Properties” parameter so that I could specify which of the attributes of an object I wanted to use … that way, I can pass in the almost unmodified output from Get-PSBlogRoll into the new Get-ListBox, and run with it. In the end, it looks something like this:


cmdlet Get-Listbox {
   param(  
    [Parameter(ValueFromPipeline=$true, Mandatory=$true)]$Input,
    [Parameter(Position=0,Mandatory=$false)][string[]]$Properties
    )
   Begin{
      $listItems = @()      
   }
   Process
   {
      if( $input -is [type] -and $input.IsEnum )
      {
         $listItems+= [Enum]::GetValues($input)
      }
      elseif( $input.GetType().IsEnum )
      {
         $listItems+= [Enum]::GetValues($input.GetType())          
      }
      else
      {
         $listItems += $input
      }              
   }
   
   End
   {
      $listbox = New-Object System.Windows.Controls.Listbox
      $listBox.ItemsSource = $listItems # | Select -unique

      # An optional DataTemplate, if they specified which properties they want to see
      if($Properties) {
         $xml = "<DataTemplate xmlns='http://schemas.microsoft.com/winfx/2006/xaml/presentation'><StackPanel>"
         foreach($property in $Properties) {
            $xml += "<TextBlock Text='{Binding Path=$($property)}'/>"
         }
         [XML]$xml = $xml + "</StackPanel></DataTemplate>"
         
         $listBox.ItemTemplate = [Windows.Markup.XamlReader]::Load(
            (New-Object System.Xml.XmlNodeReader $xml))
      }
     
      $listBox
    }
}

function Get-PsBlogRoll {
   $webClient = new-object System.Net.WebClient
   [xml]$Opml = $webClient.DownloadString("http://bloglines.com/export?id=halr9000")
   $PsBlogroll = $Opml.opml.body.outline | Where-Object { $_.text -eq '@Powershell' }
   ## The Select statement outputs a custom PSObject instead of the original (XmlNode) object
   $PsBlogroll.Outline | Select-Object title, text, htmlUrl, xmlUrl
}

Get-PSBlogRoll | Get-Listbox title | Show-Control @{
   "MouseDoubleClick" = {
      # But now I can use the "HtmlUrl" property to launch it in my browser
      [Diagnostics.Process]::Start( $window.Content.SelectedItem.HtmlUrl )
   }
}

Of course, because of how I wrote the DataTemplate for the properties (using a StackPanel and a loop to output multiple TextBlock objects) you could choose to show the Url as well, and all you’d have to do is change the call to Get-Listbox like this: Get-Listbox title,htmlUrl … which is pretty cool. Does anyone else have any ideas for making these even easier to compose?

A really nice trick would be if the wpf cmdlet could use Write-Output in the click handler and actually yield to allow the rest of the pipeline to process that item, but so far, I haven’t been able to find a way to do that (short of closing the WPF window, of course).

PowerShell Power User Tips: Bash-style “alias” command.

I keep hearing from new users who are used to bash-style aliases, how frustrating it is not to be able to create aliases with parameters, the way you can in bash …

I’m going to show you how to make the “alias” command work roughly the way it does in bash, but first, let me clear this up:

Bash or Csh PowerShell
script script
alias, shell functions function
... alias

In Bash, the recommendation is that “for almost every purpose, shell functions are preferred over aliases” ... and that recommendation is even stronger for PowerShell. The PowerShell “alias” is a true alias — it’s just another name for a command, script, function, etc. and it doesn’t support passing parameters or making mini-scripts at all.

Usually, an alias serves to give you a name you can remember, but sometimes it’s just to shorten the name, . Another use for them is to let you specify the default cmdlet — if you have two cmdlets (or script functions) with the same name, you can use an alias to override which one is executed by default.

The PowerShell function can do everything a bash alias can do (and everything a function or script can do, but this isn’t a tip about that, it’s a tip about making an alias [new] and unalias command that works the way you expect it to). So …


## Aliases.ps1 -- to be dot-sourced from your profile
if($Host.Version.Major -ge 2) {
   function script:Resolve-Aliases
   {
      param($line)

      [System.Management.Automation.PSParser]::Tokenize($line,[ref]$null) | % {
         if($_.Type -eq "Command") {
            $cmd = @(which $_.Content)[0]
            if($cmd.CommandType -eq "Alias") {
               $line = $line.Remove( $_.StartColumn -1, $_.Length ).Insert( $_.StartColumn -1, $cmd.Definition )
            }
         }
      }
      $line
   }
}

function alias {
   # pull together all the args and then split on =
   $alias,$cmd = [string]::join(" ",$args).split("=",2) | % { $_.trim()}

   if($Host.Version.Major -ge 2) {
      $cmd = Resolve-Aliases $cmd
   }
   New-Item -Path function: -Name "Global:Alias$Alias" -Options "AllScope" -Value @"
Invoke-Expression '$cmd `$args'
###ALIAS###
"
@

   Set-Alias -Name $Alias -Value "Alias$Alias" -Description "A UNIX-style alias using functions" -Option "AllScope" -scope Global -passThru
}

function unalias([string]$Alias,[switch]$Force){
   if( (Get-Alias $Alias).Description -eq "A UNIX-style alias using functions" ) {
      Remove-Item "function:Alias$Alias" -Force:$Force
      Remove-Item "alias:$alias" -Force:$Force
      if($?) {
         "Removed alias '$Alias' and accompanying function"
      }
   } else {
      Remove-Item "alias:$alias" -Force:$Force
      if($?) {
         "Removed alias '$Alias'"
      }
   }
}

You can save that as alias.ps1 and create your aliases the way you used to in bash alias ls='ls -recurse' and still be able to invoke them and, you can pass them extra parameters somewhat like in csh when you invoke them. :-) I’ve added some extra text in the function name and content and in the alias description, so it’s actually pretty easy to find all the aliases and functions this script creates and dump them to a file so you can reload them later if you want … but I haven’t actually written a function to do that yet myself.

One important note: You must not use recursive aliases in the bindings on v1 — that is, alias ls='ls -recurse' will loop until it hits PowerShell’s recursive limit (only 100) and exit if you try to use it on v1 — because the script won’t be able to resolve the alias “ls” to Get-ChildItem … you’re probably better off not relying on that feature anyway.

PowerShell Power User Tips: Current Directory

This is the second in an occasional series of tips for PowerShell users: short posts which don’t intend to give guidance, but merely a tip on a feature you may not be aware of, or maybe even answers to some of the recurring questions that come up in #PowerShell.

Fixing the “Current Directory” problem

The core of this tip is very simple: Windows tracks your application’s “current directory” ... and you can get and set this location using static methods of the System.IO.Directory class: SetCurrentDirectory and GetCurrentDirectory.

The reason this is showing up as a Power User Tip is that PowerShell doesn’t set this environment setting when you navigate — it uses it’s internal “PSProvider” architecture, and doesn’t differentiate between whether you’re in a FileSystem location or a registry location, or even a third-party provider. So, it never actually changes the current directory, and any console command or .net method you call which uses the current directory will most likely be in the wrong place — like, for instance:


$sw = New-Object System.IO.StreamWriter("NeatFile.txt")
$sw.writeline("I could write a lot of neat stuff here!")
$sw.close()

The problem is that now you don’t know where “NeatFile.txt” is — the “current directory” depends on how you launched PowerShell — most frequently it’s your $Home directory (equivalent to Env:HOMEDRIVE + Env:HOMEPATH — usually something like C:\Documents and Settings\YourName), but it could be your SystemRoot (C:\Windows) or the current directory of the app that launched PowerShell (eg: C:\Windows\System32 when you run it via “runas”). You can figure it out by running: [IO.Directory]::GetCurrentDirectory().

But here’s something more interesting: you can “fix” the problem by using [IO.Directory]::SetCurrentDirectory. There are a couple of catches, however: You can’t just use $pwd or Get-Location because you might be in the registry or some other location that’s not a Directory. And you can’t just use Get-Location -PSProvider FileSystem because even though it returns the current FileSystem provider path, the FileSystem provider supports “fake” PSDrives (eg: you could create a Scripts: drive like new-psdrive scripts filesystem "$Home\Scripts") and these aren’t actually supported by the .Net FileSystem. Luckily, PowerShell includes a Convert-Path cmdlet which was created for this very purpose: converting a path from a Windows PowerShell path to a native path supported by the underlying provider.

Without further ado, here’s a one-liner you can add to your prompt function:


[IO.Directory]::SetCurrentDirectory((Convert-Path (Get-Location -PSProvider FileSystem)))

[new] Edit: You can do the same thing using the System.Environment class, and it turns out that the ProviderPath is a property of the PathInfo object, perhaps you’ll find this syntax simpler:


[Environment]::CurrentDirectory=(Get-Location -PSProvider FileSystem).ProviderPath