How to uninstall Powershell v1.0 from Windows Server 2003 R2

April 27th, 2009

If you’re lucky, you can uninstall Powershell simply by finding the Windows update in ARP that has the signature blue icon.  When I was rolling out Powershell CTP3 (now deeply integrated into our build & deploy scripts), this worked on the majority of our machines.  All except the TFS server, as luck would have it.  Figures!

According to KB926140, this will fail if you installed Powershell before you installed SP2 (or upgraded to R2) because the latter update includes the former.  MSI patching is fun like that.  So if you want v2.0 goodness, it’s time to do some surgery.

WARNING: this is completely unsupported by Microsoft.  It happened to work for me.  YMMV.

First you need to dig up a copy of PSCustomSetup.exe.  If you have a machine that already has PS v1.0 on it, you’ll find this tool in %windir%\$NtUninstallKB926139$.  (Note: the KB number may be 928439 on Vista, but I don’t have a Vista machine to say for sure.  Use your “gci -filter” skillz.)  Or you can dig it out of the v1.0 install package.  You might also try to grab a copy of spuninst.inf that’s appropriate for your OS, just to sanity check you aren’t missing anything, though we will be hacking our own…

Basically, we need to manually replay the steps that the uninstaller would have taken.  Ordinarily, the .inf file spells this out in a vaguely human-readable format.  Thanks to the efforts of some folks around the net[1][2][3], I was able to come up with a comprehensive list.  Technically it can vary from OS to OS, or even machine to machine, but you wouldn’t be here unless you had the 2003 R2 issue in particular – XP and Vista aren’t affected since PS is not part of their service packs.  Plus, it’s fairly easy to abstract out the machine-specific paths with an environment variable here & there.  Thus, my instructions:

1) Copy the PSCustomSetup.exe you found to a temporary directory on the affected machine.

2) Copy/paste this first script block into a file named uninstall.cmd and save it into the temporary directory.

pushd %windir%\Microsoft.NET\Framework\v2.0.50727 ngen.exe uninstall "System.Management.Automation,Version=1.0.0.0,Culture=neutral,PublicKeyToken=31bf3856ad364e35,ProcessorArchitecture=msil" /silent /nologo /NoDependencies ngen.exe uninstall "Microsoft.PowerShell.ConsoleHost,Version=1.0.0.0,Culture=neutral,PublicKeyToken=31bf3856ad364e35,ProcessorArchitecture=msil" /silent /nologo /NoDependencies ngen.exe uninstall "Microsoft.PowerShell.Commands.Management,Version=1.0.0.0,Culture=neutral,PublicKeyToken=31bf3856ad364e35,ProcessorArchitecture=msil" /silent /nologo /NoDependencies ngen.exe uninstall "Microsoft.PowerShell.Commands.Utility,Version=1.0.0.0,Culture=neutral,PublicKeyToken=31bf3856ad364e35,ProcessorArchitecture=msil" /silent /nologo /NoDependencies ngen.exe uninstall "Microsoft.PowerShell.Security,Version=1.0.0.0,Culture=neutral,PublicKeyToken=31bf3856ad364e35,ProcessorArchitecture=msil" /silent /nologo /NoDependencies ngen.exe uninstall "System.Management.Automation.resources,Version=1.0.0.0,Culture=en,PublicKeyToken=31bf3856ad364e35,ProcessorArchitecture=msil" /silent /nologo /NoDependencies ngen.exe uninstall "Microsoft.PowerShell.ConsoleHost.resources,Version=1.0.0.0,Culture=en,PublicKeyToken=31bf3856ad364e35,ProcessorArchitecture=msil" /silent /nologo /NoDependencies ngen.exe uninstall "Microsoft.PowerShell.Commands.Management.resources,Version=1.0.0.0,Culture=en,PublicKeyToken=31bf3856ad364e35,ProcessorArchitecture=msil" /silent /nologo /NoDependencies ngen.exe uninstall "Microsoft.PowerShell.Commands.Utility.resources,Version=1.0.0.0,Culture=en,PublicKeyToken=31bf3856ad364e35,ProcessorArchitecture=msil" /silent /nologo /NoDependencies ngen.exe uninstall "Microsoft.PowerShell.Security.resources,Version=1.0.0.0,Culture=en,PublicKeyToken=31bf3856ad364e35,ProcessorArchitecture=msil" /silent /nologo /NoDependencies popd PSCustomSetupUtil.exe /uninstall "System.Management.Automation,Version=1.0.0.0,Culture=neutral,PublicKeyToken=31bf3856ad364e35,ProcessorArchitecture=msil"" PSCustomSetupUtil.exe /uninstall "Microsoft.PowerShell.ConsoleHost,Version=1.0.0.0,Culture=neutral,PublicKeyToken=31bf3856ad364e35,ProcessorArchitecture=msil"" PSCustomSetupUtil.exe /uninstall "Microsoft.PowerShell.Commands.Management,Version=1.0.0.0,Culture=neutral,PublicKeyToken=31bf3856ad364e35,ProcessorArchitecture=msil"" PSCustomSetupUtil.exe /uninstall "Microsoft.PowerShell.Commands.Utility,Version=1.0.0.0,Culture=neutral,PublicKeyToken=31bf3856ad364e35,ProcessorArchitecture=msil"" PSCustomSetupUtil.exe /uninstall "Microsoft.PowerShell.Security,Version=1.0.0.0,Culture=neutral,PublicKeyToken=31bf3856ad364e35,ProcessorArchitecture=msil"" PSCustomSetupUtil.exe /uninstall "System.Management.Automation.resources,Version=1.0.0.0,Culture=en,PublicKeyToken=31bf3856ad364e35,ProcessorArchitecture=msil"" PSCustomSetupUtil.exe /uninstall "Microsoft.PowerShell.ConsoleHost.resources,Version=1.0.0.0,Culture=en,PublicKeyToken=31bf3856ad364e35,ProcessorArchitecture=msil"" PSCustomSetupUtil.exe /uninstall "Microsoft.PowerShell.Commands.Management.resources,Version=1.0.0.0,Culture=en,PublicKeyToken=31bf3856ad364e35,ProcessorArchitecture=msil"" PSCustomSetupUtil.exe /uninstall "Microsoft.PowerShell.Commands.Utility.resources,Version=1.0.0.0,Culture=en,PublicKeyToken=31bf3856ad364e35,ProcessorArchitecture=msil"" PSCustomSetupUtil.exe /uninstall "Microsoft.PowerShell.Security.resources,Version=1.0.0.0,Culture=en,PublicKeyToken=31bf3856ad364e35,ProcessorArchitecture=msil"" PSCustomSetupUtil.exe /removeenvvariable PATH "C:\WINDOWS\system32\WindowsPowerShell\v1.0" PSCustomSetupUtil.exe /removeenvvariable PATHEXT ".PSC1" PSCustomSetupUtil.exe /wmsettingchange rd /s /q %windir%\$NtUninstallKB926139$ rd /s /q %windir%\system32\windowspowershell pushd %windir%\assembly\gac_msil rd /s /q System.Management.Automation rd /s /q Microsoft.PowerShell.ConsoleHost rd /s /q Microsoft.PowerShell.Commands.Management rd /s /q Microsoft.PowerShell.Commands.Utility rd /s /q Microsoft.PowerShell.Security rd /s /q System.Management.Automation.resources rd /s /q Microsoft.PowerShell.ConsoleHost.resources rd /s /q Microsoft.PowerShell.Commands.Management.resources rd /s /q Microsoft.PowerShell.Commands.Utility.resources rd /s /q Microsoft.PowerShell.Security.resources popd

3) Open a CMD window, navigate to the temporary directory, and run the script.  Watch for errors.  (Post to the comments…)  Make sure it’s a 64-bit CMD window if you’re on x64 Windows.

4) Copy this block of text to a file named uninstall.reg and save it to the temporary directory.  Run it and accept the “yes, I really want to do this” prompt.

Windows Registry Editor Version 5.00 [-HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\PowerShell] [-HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Cryptography\OID\EncodingType 0\CryptSIPDllCreateIndirectData\{603BCC1F-4B59-4E08-B724-D2C6297EF351}] [-HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Cryptography\OID\EncodingType 0\CryptSIPDllGetSignedDataMsg\{603BCC1F-4B59-4E08-B724-D2C6297EF351}] [-HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Cryptography\OID\EncodingType 0\CryptSIPDllIsMyFileType2\{603BCC1F-4B59-4E08-B724-D2C6297EF351}] [-HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Cryptography\OID\EncodingType 0\CryptSIPDllPutSignedDataMsg\{603BCC1F-4B59-4E08-B724-D2C6297EF351}] [-HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Cryptography\OID\EncodingType 0\CryptSIPDllRemoveSignedDataMsg\{603BCC1F-4B59-4E08-B724-D2C6297EF351}] [-HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Cryptography\OID\EncodingType 0\CryptSIPDllVerifyIndirectData\{603BCC1F-4B59-4E08-B724-D2C6297EF351}] [-HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\HotFix\KB926139] [-HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Updates\Windows Server 2003\SP2\KB926139] [-HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Updates\Windows PowerShell 1.0] [-HKEY_CLASSES_ROOT\.ps1] [-HKEY_CLASSES_ROOT\.ps1xml] [-HKEY_CLASSES_ROOT\.psc1] [-HKEY_CLASSES_ROOT\Microsoft.PowerShellConsole.1] [-HKEY_CLASSES_ROOT\Microsoft.PowerShellScript.1] [-HKEY_CLASSES_ROOT\Microsoft.PowerShellXmlData.1]

5) Install Powershell v2.0

6) Clean up the temp dir.

Success?  Hope so – if not, it’s probably time to nuke that install!  (or at minimum, restore from backup)  Good luck everyone. 

Note: these instructions do not remove shortcuts from the Start Menu or anywhere else.  You’ll need to do that yourself.

[1] http://groups.google.com/group/microsoft.public.windows.powershell/browse_thread/thread/51a54e7de0ed5d1f/eaba9eb2c0d772ec

[2] http://blogs.msdn.com/powershell/archive/2007/01/09/behind-powershell-installer-for-windows-xp-windows-server-2003.aspx

[3] http://marcoshaw.blogspot.com/2008/06/powershell-v2-ctpctp2-error-error.html

The VS2008 remote debug transport (MSVSMON.EXE) does not support Silverlight?

April 20th, 2009

So, a Silverlight app we recently deployed into production works great across a wide variety of machines, browsers, and even OS platforms…except for one poor customer.  For him the app crashes soon after loading, with a strange exception deep in the SL runtime.  Sounds like a job for the remote debugger, right?

BzztI think we don't support remote Windows debugging for Silverlight.  Can anyone confirm or deny?

It sure doesn’t work for me.  I can choose a browser from the list of processes and “Silverlight code” from the list of debuggers, but I always get “Unable to attach to the process.”

Get-TfsItemProperty is the bridge between server and local objects

March 27th, 2009

Most of the real-world examples of the TFS powershell tool revolve around querying (and possibly updating) the state of the server. 

And rightfully so: most of the operations you want to do locally are already possible without any special tools.  The “Powershell snap-in system” we know and love today was created to help IT admins manage their servers – even today, only a brave few developers have completely replaced cmd.exe at their daily workstation.

Nevertheless, we shouldn’t be satisfied to have our TFS cmdlets live in their own little world.  Luckily, all the info we could ever want about a TFS version controlled item – including its local state – comes packaged in the ExtendedItem object.  Even more fortuitously, if you revisit the exploration of QualifiedItemSpec with an eye for detail, you’ll notice that one overload of ToQualifiedItem() is not like the others.*  In the constructor where ExtendedItem decays into QualifiedItem, the properties saved into the resulting tuple include LocalItem and VersionLocal instead of more familiar ones like ServerItem.  This is not a coincidence :) 

Let’s sum up what we know so far.  Native TFS objects are funneled down the pipeline by stripping them to bare essentials about the items they represent (QIs); these serve as a common interface between cmdlets.  When an ExtendedItem is stripped down, the next cmdlet will use the local filename & local version info as its parameters.  Get-TfsItemProperty is the cmdlet that returns ExtendedItems.  Capiche?

Time to put our newfound knowledge into action.  If you’ve also been following the last several posts on file manipulation in powershell, these examples will hopefully inspire an “aha!” moment.

001
002
003
004
005
# find all C# makefiles with HintPath elements, check them out, and open them in my text editor
tfprop $/project/*csproj -r | get-fullpath | ss hintpath | tfedit -passthru | ue

# find my shelvesets related to powershell, unshelve them, and open every file in the ISE
get-tfsshelveset | ? { $_.name -like "*powershell*" } | tfunshelve | tfprop | ise

[Note that I have get-tfsitemproperty aliased to ‘tfprop’ rather than ‘tfproperties’ as it appears in a default install of the Power Tools.]

Are we having fun yet?!

So far we’ve only really exercised the filename part of the QI.  Equally interesting things happen when you take advantage of the special ability that Get-TfsItemProperty offers to manipulate the local version.

001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
# Force unshelve to pend changes against your current workspace ("have") version. Ordinarily,
# unshelve rolls the affected files back to the version you had at the time you shelved the
# changes.
# Pro: this ensures your workspace stays in a consistent, buildable state
# Con: you may need to resolve content and/or namespace conflicts
function unpack
{
    [CmdletBinding()]
    param (
        [parameter(Mandatory=$True, Position=0, ValueFromPipeline=$True)]
        [Microsoft.TeamFoundation.PowerTools.Powershell.ShelvesetSpec]$ss
    )
   
    process
    {
        $currentWorkspaceVersions = tfstatus -shelveset $ss | tfprop
        restore-tfsshelveset $ss | out-null
        $currentWorkspaceVersions | tfget
        tf resolve /prompt
    }
}

[Microsofties: think bbpack / jjpack]

Opens up a whole new world of TFVC scripting mania, wouldn’t ya say?

*I’ll admit the one for GettingEventArgs is a little funky as well.  Here, the choice of properties to expose came down to UX / gut feel.  TargetLocalItem is usually what you want to see; it tends to be the “final result” of mainline operations, surviving things like Rename and Undo fairly well.  But it’s not always the best choice.  Obviously when it’s null we need a backup; good old ServerItem will do.  It’s [almost always] guaranteed to be present, but it’s  not always perfect either – now you have the occasional strange appearance of server paths in your list of local files being touched by the Update.  Finding the right info gets really tricky really fast: was TargetLocalItem null because of a pending Delete, because it’s cloaked or otherwise excluded from our workspace, or because we’re undoing a pending Branch or Merge?  That’s just one example.  The logic behind which name tf.exe displays in which scenario is actually one of the more complex pieces of the app.  With powershell we can accept “good enough” since the object model lets you manipulate things to your heart’s content.

Powershell tidbit: hit F6 in the ISE to execute the current selection

March 27th, 2009

For anyone who hasn’t discovered this feature yet, make sure to give it a whirl.  It’s by far my favorite “little” thing about the new environment.  /Berg tweet out

Powershell tidbit: hacky way to find the 32-bit Program Files directory

March 27th, 2009

In the process of tidying up another function for posting, I found that there is no clean way to get a Powershell variable from the system that:

  1. Points to “C:\program files (x86)” regardless whether you’re in a 32-bit or 64-bit process.
  2. Exists on legacy 32-bit operating systems.

Sounds like a common thing you’d need: to find the location where 32-bit programs are installed regardless of any other factors.  Powershell’s $env:ProgramFiles(x86) variable comes close, but even if it were present on 32-bit OSes -- honestly not sure about that -- the parentheses in the variable name present a nasty parse problem.  No amount of quoting or backtick`ing or $()’ing got me the result I was looking for.  If you have more patience than me, have at it.

And oh yeah, did I mention I was shopping for a better null coalescing operator than the one that comes with PSCX?  Note to PSCX folks: I hate wrapping stuff in scriptblocks when there’s no real need for them.  Thanks, Richard’s pinky finger.

So anyway, I resorted to putting this abomination in my $profile:

001
002
003
004
function ?? {
    (@($args | ?{$_}) + $null)[0]
}
set-variable programFilesX86 (?? (cat 'Env:\ProgramFiles(x86)' -ea SilentlyContinue) $Env:ProgramFiles) -scope global

Yes, it’s ugly and nonstandard (in the sense that I can’t rely on this being present in other people’s script environments)…suggestions welcome...

Invoke-UltraEdit.ps1: integrate your favorite text editor with the Powershell pipeline

March 27th, 2009

Less talk, more code.  As long as that code has ample inline comments, of course!

001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
<#
.Synopsis
Opens the desired file(s) in UltraEdit.
    
.Description
    The file(s) you specify on the command line or the pipeline will be opened in UltraEdit. We'll launch a new
    copy of the app, but only if necessary. The pipeline supports many varieties of objects via my Get-FullPath
    script.
    
    Put this in your $profile:
    PS> . "c:\path\to\invoke-ultraedit.ps1"
    PS> set-alias ue invoke-ultraedit
    
.Parameter File
    Path to the file being opened. Any object that contains such a path as a property named FullName, Path, etc.
    will do.
        
.Example
    PS> # open foo.txt in UltraEdit
    PS> ue foo.txt
    
.Example
    PS> # open any project files that contain a HintPath element
    PS> dir -r -fil *csproj | ss hintpath | ue
        
.ReturnValue

.Link
    http://richardberg.net/blog/?p=60
    http://richardberg.net/blog/?p=57
    
.Notes
NAME: Invoke-UltraEdit.ps1
AUTHOR: COATUECAP\rberg
LASTEDIT: 03/27/2009 17:24:02

#Requires -Version 2.0
#>
function Invoke-UltraEdit
{
    [CmdletBinding()]
    Param(
        [Parameter(Position=0, Mandatory=$true, ValueFromPipeline=$true)] 
        [psobject] $file
    )
 
    Begin
    {
        # this path isn't stored anywhere in the registry - you may need to adjust it
        $ue = join-path $programFilesX86 "IDM Computer Solutions\UltraEdit\Uedit32.exe"
         
        # If UltraEdit isn't running yet, launching it will hang the console until it exits
        # Same deal if there's an elevated UltraEdit process running but our shell is not elevated
        Function HaveToStartNewEditor
        {
           # If there is at least one process whose handle we have permission to grab, we're ok.
            $haveToStart = $true
            get-process (dir $ue).basename -ea SilentlyContinue | 
           % { 
                   if ($_.Handle -ne $null) 
                       { $haveToStart = $false }
         }
   
            return $haveToStart
        }
    }
  
    Process
    {
        while (HaveToStartNewEditor)
        { 
            invoke-item $ue
            sleep 2 
        }   

        # launch with option to use existing window
        & $ue /foi ($file | get-fullpath)
    }
}

As you can probably tell, you’ll need some other code for this to compilebe interpreted successfully.

Easily manage files in Powershell ISE CTP3

March 27th, 2009

Since Win7 debuted, I’ve started forcing myself to use the ISE more often.  It doesn’t have a ton of features – even the much awaited debugger lags some v1.0-era 3rd party IDEs – but it does show great potential.  In particular, who doesn’t love an environment that’s scriptable using its native language? :) 

[and oh yeah, we finally get to leave the world of CSRSS.exe behind.  Ctrl+V!  sweet blessed Ctrl+V!]

Naturally, I started the customization process by scouring the web.  Keith’s "Yank Line” function quickly joined my Custom menu.  (though I map it to Ctrl+K, a vestige of muscle memory from Pico.  thanks for the memories, crappy Solaris boxes at Duke!)  Soon after came a function templater from Andy Schneider, handy especially since the new v2 syntax is pretty verbose.  And of course Lee’s syntax highlighter, without which the pretty code you’re seeing would not be possible – though in full disclosure I’ve heavily modified it.  (first to fit into a Module, then to remove the Apartment Threading overhead that’s thankfully not relevant in the ISE, and finally to take the currently selected text instead of the whole document)

Any other recommendations for $profile inclusion?  Or know how to override a keyboard shortcut, for that matter?

Back to the point of this post.  My work style tends to involve dozens of tabs, files scattered all over the place, saved & not, until I clean things up hours later.  So naturally the built-in file management features of ISE have got to go!  With the help of the $psise object model, the get-fullpath script I introduced last time, and liberal additions to the ISE profile (Microsoft.PowerShellISE_profile.ps1), it’s not hard to greatly beef things up:

  • quick Open from the interactive prompt, including rich pipeline support seen in previous posts
  • ditto for Close
  • Close All But This
  • Undo Close with a full FIFO stack of everything you’ve ever closed using my functions
  • Save All
  • Copy Full Path to clipboard
001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
080
081
082
083
084
085
086
087
088
089
090
091
092
093
094
095
096
097
098
099
100
101
102
103
104
105
106
107
108
109
110
111
112
113
# quick open from the command prompt. Pipeline enabled :)
function ise 
{
    [CmdletBinding()]
    param (
        [parameter(Mandatory=$True, Position=0, ValueFromPipeline=$True)]
        [psobject]$file,
        [Parameter()]
        [switch] $PassThru = $false 
    )   
    process 
    {
        if ($PassThru)
            { $file }       
        $psise.CurrentOpenedRunspace.OpenedFiles.Add(($file | get-fullpath))
    }
}

# ditto for close, with my desired keyboard shortcuts
function close
{
    [CmdletBinding()]
    param (
        [parameter(Position=0, ValueFromPipeline=$True)
        [psobject]$file = ""
    )
   
    begin
    {
        # getting desired behavior of "no input = close current file" is harder than it looks...
        $closedSomething = $false
       
        # set up global undo buffer
        if ($RecentlyClosedTabs -eq $null)
            { set-variable -name RecentlyClosedTabs -value (new-object system.collections.stack) -scope Global }
    }
   
    process
    {
        if ($file -ne "")  # thought I could use the 'continue' statement instead of a giant If block, but it exits the whole function
        {       
            $fullPath = $file | get-fullpath
            if (@($fullPath).count -gt 0)  # can't simply check against null for some weird reason
            {
                # because there is no constructor for System.Management.Automation.Host.OpenedFile, I have to do stupid iteration
                # that makes this routine n^2
                $fileToClose = $psise.CurrentOpenedRunspace.OpenedFiles | 
                    ? { $_.fullpath -eq $fullpath }                   
                $psise.CurrentOpenedRunspace.OpenedFiles.Remove($fileToClose) | out-null
                $closedSomething = $true
                $RecentlyClosedTabs.Push($fullPath)
            }
        }
    }
   
    end
    {
        if (! $closedSomething)
        {
            $curFile = $psise.CurrentOpenedFile
            $psise.CurrentOpenedRunspace.OpenedFiles.Remove($curFile) | out-null
            $RecentlyClosedTabs.Push($curFile) 
        }
       
        # after we've closed a file, chances are we don't want the cursor stuck in another random file
        $psise.CurrentOpenedRunspace.CommandPane.Focus()
    }
}
# $psISE.CustomMenu.Submenus.Add('_Close File', {close}, 'Ctrl+W') # can't overwrite keyboard shortcuts :(

function CloseAllButThis
{
    # need to clone the array of opened files since we'll be modifying it as we enumerate
    $temp = [array]::createinstance([System.Management.Automation.Host.OpenedFile], $psise.CurrentOpenedRunspace.OpenedFiles.Count)
    $psise.CurrentOpenedRunspace.OpenedFiles.CopyTo($temp, 0)
   
    # edge case: if there's only 1 file open, do nothing
    if ($temp.count -eq 1)
        { break }
 
    # close the files
    $temp | ? { $_ -ne $psise.CurrentOpenedFile } | close
}
$psISE.CustomMenu.Submenus.Add('Close All But _This', {CloseAllButThis}, 'Ctrl+Shift+W')

# this will spew errors when there are Untitled files, but I kinda like that behavior
function SaveAll
{   
    $psise.CurrentOpenedRunspace.OpenedFiles | % { $_.Save() }
}
$psISE.CustomMenu.Submenus.Add('_Save All', {CloseAllButThis}, 'Ctrl+Alt+S')  # really want Ctrl+Shift+S :(

# undo close tab(s)
function UndoClose ([int] $files = 1)
{
    if ($RecentlyClosedTabs -eq $null -or $RecentlyClosedTabs.Count -le 0 -or $files -le 0)
        { break }
   
    # if user tries to pop more items than we have on the stack, just let the error bubble up
    while($files -gt 0)
    {
        $RecentlyClosedTabs.Pop() | ise
        --$files
    }
}
$psISE.CustomMenu.Submenus.Add('_Undo Close', {UndoClose}, 'Ctrl+Alt+U')

# copy full path of the open file to the clipboard
function Get-OpenedPath
{
    $psise.CurrentOpenedFile.FullPath | out-clipboard
}
$psISE.CustomMenu.Submenus.Add('Copy Full _Path', {Get-OpenedPath}, 'Ctrl+Alt+C')

As you can see, the Close function ended up much uglier than expected.  Powershell limitations/bugs or programmer ignorance?  Suggestions welcome.  Powershell verb-noun Nazis need not apply.  Functions which live only in the $profile for quick consumption at the interactive prompt and/or ISE shortcut key are not obligated to follow the rules for discoverability, IMO.

Fixing consistency issues with Powershell’s filename properties

March 27th, 2009

When I decided to bitch about select-object, I used “dir” in my examples for a reason: passing filesystem objects around the pipeline is simultaneously one of the most common yet one of the most annoying things you can do in Powershell.  Let’s review some built-in cmdlets from the POV of getting the full path to a given file or folder:

CmdletInput property name accepted on pipelineOutput property name on object sent to pipeline
Get-Item, Get-ChildItem, Set-ItemPathFullName
Join-Path, Split-Path, Test-PathPathn/a
Resolve-Path, Select-StringPathPath*
New-ItemNameFullName

* note that the built-in documentation for Resolve-Path is wrong!  as of CTP3, it’s claiming the output is a string

Plus, you may be working with 3rd party tools that obey none of the above conventions. 

For example, with the TFS Power Tools, Hyung had the foresight to add string[] to the list of types we can convert to QualifiedItems behind the scenes using extra constructors, but we still don’t support the ValueFromPipelineByPropertyName idiom that’s needed to make non-trivial filesystem objects flow.  Furthermore, the objects we output are native TFS types, which were never designed with Powershell in mind.  Most of the time there’s little overlap between these objects and non-TFS cmdlets, but a big exception is the LocalItem property of the ExtendedItem object returned by “tfprop.”  In hindsight, we should have added a NoteProperty that aliased it to Path.  (…or FullName, or Name, depending on whether the PS guys make up their mind :) )

Then there are tools that have nothing at all to do with Powershell.  Some sort of wrapper script is going to be required regardless, but without consistency in the kind of input your wrapper can expect, it can get needlessly complex.

My solution to all of the above is outlined below.  Time to let the code do the talking:

001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
function get-fullpath
{
    [CmdletBinding()]
    Param(
        [Parameter(ParameterSetName="ByPropertyName", Position=0, Mandatory=$true, ValueFromPipelineByPropertyName=$true, ValueFromPipeline=$true)]       
        [Alias("FullName")]
        [Alias("FullPath")]
        # [Alias("Name")] - too many false positives
        [Alias("Path")]
        [Alias("LocalItem")]
        [string] $file
    )
   
    Process
    {       
        # eliminate nonexistent files, obviously, but also random objects that were coerced from the pipeline by calling their ToString() method
        if (!(test-path $file))
            { break }
       
        # expand relative paths (in case the immediate window's workingdir != the workingdir of powershell_ise.exe)
        resolve-path $file | 
            # and also make sure UNC paths are in normal format, not Powershell format
            convert-path 
    }
}

Who knew that ValueFromPipelineByPropertyName worked on aliases?  I didn’t.  I thought it would be nice, but since I didn’t see it documented anywhere, my initial sketch for this function used a ton of ugly ParameterSets as a workaround.  Somewhere in there I decided to try Aliases; lo and behold, it works!  Yay.

This function is useful on its own – if nothing else I use it as a wholesale replacement for resolve-path, whose behavior on UNC paths is bizarre in my opinion – but its real value is as a helper.  For example, a couple snippets from my $profile:

001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
function tfedit 
{
[CmdletBinding()]
    Param(
        [Parameter(Position=0, Mandatory=$true, ValueFromPipeline=$true)]
        [psobject] $file,
        [Parameter()]
        [switch] $PassThru = $false 
    )
   
    Process 
    {
        if ($PassThru)
            { $file }
       tfpend -edit ($file | get-fullpath)
    }
}

 

001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
function ise 
{
    [CmdletBinding()]
    param (
        [parameter(Mandatory=$True, Position=0, ValueFromPipeline=$True)]
        [psobject]$file,
        [Parameter()]
        [switch] $PassThru = $false 
    )   
    process 
    {
        if ($PassThru)
            { $file }       
        $psise.CurrentOpenedRunspace.OpenedFiles.Add(($file | get-fullpath))
    }
}

I think the PS parser should be smart enough that double-parens are not required in the second example, but oh well.  Pretty simple overall, and super convenient.  Enjoy!

Select-Object is annoying

March 26th, 2009

Oh how I long for a real Select command.  I’m not a SQL-head by any stretch, but I recognize the need for basic set operations like projection.  The language geeks in DevDiv surely do too: witness LINQ, and to some degree, Powershell itself.  However, I argue PS is falling short on this front.

dir | select fullname, lastwritetime

FullName                   LastWriteTime       
--------                   -------------       
C:\Users\rberg\Contacts    2/19/2009 12:34:40 PM
C:\Users\rberg\Desktop     3/24/2009 12:49:31 PM
C:\Users\rberg\Documents   3/23/2009 4:34:23 PM
C:\Users\rberg\Downloads   3/25/2009 6:38:20 PM
C:\Users\rberg\Favorites   2/19/2009 12:34:43 PM
C:\Users\rberg\Links       3/10/2009 3:36:02 PM
C:\Users\rberg\Music       2/19/2009 1:26:36 PM
C:\Users\rberg\Pictures    2/19/2009 1:26:38 PM
C:\Users\rberg\Saved Games 2/19/2009 12:34:40 PM
C:\Users\rberg\Searches    2/19/2009 12:34:49 PM
C:\Users\rberg\Videos      2/19/2009 1:26:39 PM
C:\Users\rberg\.kdiff3rc   3/25/2009 5:23:33 PM
C:\Users\rberg\_lesshst    3/4/2009 3:01:33 PM
 

It looks like it’s doing something useful, right?  But the more I use Powershell, the more it seems like it’s violating its own guidelines like “input & output should be useful objects, not text-based hacks” and “design for the middle of the pipeline.”  Let’s examine what Select-Object really returns. 

PS >dir .\foo.txt | select name | gm

   TypeName: Selected.System.IO.FileInfo

Name        MemberType   Definition                      
----        ----------   ----------                      
Equals      Method       System.Boolean Equals(Object obj)
GetHashCode Method       System.Int32 GetHashCode()      
GetType     Method       System.Type GetType()           
ToString    Method       System.String ToString()        
Name        NoteProperty System.String Name=foo.txt
 

That’s not a Name, that’s something else entirely.  Besides, I get the feeling it’s not being completely honest with me…doesn’t a FileInfo object have more methods?…

PS >(dir .\foo.txt | select name).gettype()

IsPublic IsSerial Name               BaseType
-------- -------- ----               --------
True     False    PSCustomObject     System.Object 

Caught ya!  Liar! 

I wouldn’t mind a little custom magic going on under the hood if it meant more power in the user’s hands.  But in reality, these PSCustomObjects being tossed around seem to be of very little utility.  In almost every case, simply returning the native type (e.g. a System.String in the case of “select name”) would play nicer with the rest of the pipeline.  I understand there’s a tough barrier when you start selecting multiple objects, or when you want lazy evaluation -- but sorry guys, the current solution ain’t it.

To be clear, I want to be able to do stuff like:

$a = dir foo.txt | select lastwritetime
# oops, we don’t have a real DateTime
$b = $a.adddays(1)  

# oops, neither a string nor a ‘name’ property
dir *.exe | select basename | get-process 

# not even the NoteProperties work in a useful manner
dir | select psparentpath | convert-path

# wouldn’t this be cool?  PS already has quasi tuple syntax, after all
$x, $y = dir | select attributes, length

# or this?  assume do-something is a cmdlet that takes 3 strongly typed parameters by position
# and/or by property name
dir | select fullname –as path, versioninfo, isreadonly –as force | do-something

Am I crazy to want these to work?  Anyone know of ongoing work in this area before I waste some hobby time? :)

Brilliant Powershell posts

March 26th, 2009

Working at Microsoft, it’s almost impossible to not stay plugged into the technology zeitgeist.  Until you get your email filters set up properly – inevitably running into the Exchange limits – your inbox is literally a news ticker.  Needless to say, establishing that kind of community doesn’t happen automatically in the “real world.”  So in the last week or two I’ve been fleshing out my RSS feed subscriptions.

In the process, I’ve come across some people doing seriously amazing things with Powershell.  Some of these efforts I hope I can find time to contribute and extend; others I’ll just sit back in awe.  (I’m not saying which is which…you too will have to subscribe!)

 

Happy scripting!