# Wednesday, November 19, 2008

It seems lots of people are catching the PowerShell bug these days. People who previously considered themselves as administrators have learned enough of the magic from using PowerShell from day to day over the last few months that they feel comfortable enough to tinker around with C#.

This is an example Cmdlet skeleton that is designed to work with Files and Directories. It looks after resolving wildcards, supports –WhatIf and –Confirm, accepting files and/or directories from the pipeline or accepting strings of paths from the pipeine. It also ensures that the only types of paths it can accept lie on the File System. Here’s an example of the invocation styles it supports:

cmdlet

Here’s the source code for this. It doesn’t really do a lot. It spits out two different types of custom objects: one for a file, and another for a directory. Feel free to do what you like with it; it’s yours. Have fun.

using System;
using System.Collections.Generic;
using System.IO;
using System.Management.Automation;
using Microsoft.PowerShell.Commands;
namespace PSQuickStart
{
    [Cmdlet(VerbsCommon.Get, Noun,
        DefaultParameterSetName = ParmamSetPath,
        SupportsShouldProcess = true)
    ]
    public class GetFileMetadataCommand : PSCmdlet
    {
        private const string Noun = "FileMetadata";
        private const string ParamSetLiteral = "Literal";
        private const string ParmamSetPath = "Path";
        private string[] _paths;
        private bool _shouldExpandWildcards;
        [Parameter(
            Position = 0,
            Mandatory = true,
            ValueFromPipeline = false,
            ValueFromPipelineByPropertyName = true,
            ParameterSetName = ParamSetLiteral)
        ]
        [Alias("PSPath")]
        [ValidateNotNullOrEmpty]
        public string[] LiteralPath
        {
            get { return _paths; }
            set { _paths = value; }
        }
        [Parameter(
            Position = 0,
            Mandatory = true,
            ValueFromPipeline = true,
            ValueFromPipelineByPropertyName = true,
            ParameterSetName = ParmamSetPath)
        ]
        [ValidateNotNullOrEmpty]
        public string[] Path
        {
            get { return _paths; }
            set
            {
                _shouldExpandWildcards = true;
                _paths = value;
            }
        }
        protected override void ProcessRecord()
        {
            foreach (string path in _paths)
            {
                // This will hold information about the provider containing
                // the items that this path string might resolve to.                
                ProviderInfo provider;
                // This will be used by the method that processes literal paths
                PSDriveInfo drive;
                // this contains the paths to process for this iteration of the
                // loop to resolve and optionally expand wildcards.
                List<string> filePaths = new List<string>();
                if (_shouldExpandWildcards)
                {
                    // Turn *.txt into foo.txt,foo2.txt etc.
                    // if path is just "foo.txt," it will return unchanged.
                    filePaths.AddRange(this.GetResolvedProviderPathFromPSPath(path, out provider));
                }
                else
                {
                    // no wildcards, so don't try to expand any * or ? symbols.                    
                    filePaths.Add(this.SessionState.Path.GetUnresolvedProviderPathFromPSPath(
                        path, out provider, out drive));
                }
                // ensure that this path (or set of paths after wildcard expansion)
                // is on the filesystem. A wildcard can never expand to span multiple
                // providers.
                if (IsFileSystemPath(provider, path) == false)
                {
                    // no, so skip to next path in _paths.
                    continue;
                }
                // at this point, we have a list of paths on the filesystem.
                foreach (string filePath in filePaths)
                {
                    PSObject custom;
                    // If -whatif was supplied, do not perform the actions
                    // inside this "if" statement; only show the message.
                    //
                    // This block also supports the -confirm switch, where
                    // you will be asked if you want to perform the action
                    // "get metadata" on target: foo.txt
                    if (ShouldProcess(filePath, "Get Metadata"))
                    {
                        if (Directory.Exists(filePath))
                        {
                            custom = GetDirectoryCustomObject(new DirectoryInfo(filePath));
                        }
                        else
                        {
                            custom = GetFileCustomObject(new FileInfo(filePath));
                        }
                        WriteObject(custom);
                    }
                }
            }
        }
        private PSObject GetFileCustomObject(FileInfo file)
        {
            // this message will be shown if the -verbose switch is given
            WriteVerbose("GetFileCustomObject " + file);
            // create a custom object with a few properties
            PSObject custom = new PSObject();
            custom.Properties.Add(new PSNoteProperty("Size", file.Length));
            custom.Properties.Add(new PSNoteProperty("Name", file.Name));
            custom.Properties.Add(new PSNoteProperty("Extension", file.Extension));
            return custom;
        }
        private PSObject GetDirectoryCustomObject(DirectoryInfo dir)
        {
            // this message will be shown if the -verbose switch is given
            WriteVerbose("GetDirectoryCustomObject " + dir);
            // create a custom object with a few properties
            PSObject custom = new PSObject();
            int files = dir.GetFiles().Length;
            int subdirs = dir.GetDirectories().Length;
            custom.Properties.Add(new PSNoteProperty("Files", files));
            custom.Properties.Add(new PSNoteProperty("Subdirectories", subdirs));
            custom.Properties.Add(new PSNoteProperty("Name", dir.Name));
            return custom;
        }
        private bool IsFileSystemPath(ProviderInfo provider, string path)
        {
            bool isFileSystem = true;
            // check that this provider is the filesystem
            if (provider.ImplementingType != typeof(FileSystemProvider))
            {
                // create a .NET exception wrapping our error text
                ArgumentException ex = new ArgumentException(path +
                    " does not resolve to a path on the FileSystem provider.");
                // wrap this in a powershell errorrecord
                ErrorRecord error = new ErrorRecord(ex, "InvalidProvider",
                    ErrorCategory.InvalidArgument, path);
                // write a non-terminating error to pipeline
                this.WriteError(error);
                // tell our caller that the item was not on the filesystem
                isFileSystem = false;
            }
            return isFileSystem;
        }
    }
}
posted on Wednesday, November 19, 2008 5:58:18 PM (Eastern Standard Time, UTC-05:00)  #    Comments [0] Trackback
# Thursday, November 13, 2008

Here’s just a quick rundown of the changes in PowerShell 2.0 in comparison to the last public release, CTP2. This may be helpful to those of you who don’t have access to the Windows 7 PDC bits. Remember, things are still subject to change - it’s not in beta yet.

Unchanged cmdlets are not listed here.

Added (or renamed*) Cmdlets

Enter-PSSession
Remove-PSSession
Get-PSSession
Exit-PSSession
Get-PSSessionConfiguration
Unregister-PSSessionConfiguration
Register-PSSessionConfiguration
Debug-Process
Test-ModuleManifest
New-ModuleManifest
New-WSManSessionOption
New-PSSession
Import-PSSession
Export-PSSession
Set-PSSessionConfiguration
Get-WSManInstance
Get-WSManCredSSP
Enable-WSManCredSSP
Set-WSManInstance
Set-WSManQuickConfig
New-WSManInstance
Remove-WSManInstance
Ping-WSMan
New-WebServiceProxy
Get-PSTransaction
Connect-WSMan
Disable-WSManCredSSP
Invoke-WSManAction
Disconnect-WSMan
Get-ComputerRestorePoint
Disable-ComputerRestore
Enable-ComputerRestore
Get-Counter
Clear-EventLog
Export-Counter
Import-Counter
Checkpoint-Computer
Restart-Computer
Stop-Computer
Ping-Computer
Resume-BitsTransfer
Set-BitsTransfer
Suspend-BitsTransfer
Add-BitsFile
Clear-BitsTransfer
Remove-EventLog
Restore-Computer
New-Module
Get-HotFix
Complete-BitsTransfer
Write-EventLog
Show-EventLog
Limit-EventLog
Get-BitsTransfer
New-BitsTransfer
New-EventLog

Changed Cmdlets (parameter or parameterset differences)

Invoke-Command
Receive-PSJob
Set-WmiInstance
Stop-PSJob
Wait-PSJob
Remove-PSJob
Export-ModuleMember
Start-PSJob
Get-PSJob
Add-Module
Set-Service

   

Removed (or renamed*) Cmdlets

Pop-Runspace
New-Runspace
Push-Runspace
Get-Runspace
Remove-Runspace

   

* e.g. The *-Runspace Cmdlets have been renamed to *-PSSession

posted on Thursday, November 13, 2008 11:15:00 PM (Eastern Standard Time, UTC-05:00)  #    Comments [0] Trackback
# Monday, November 10, 2008

Fellow MVP Hal Rottenberg asked me to highlight this short and painless survey we’re trying to get out to y’all.

Think of the following in terms of impact on the greatest number of people at a tech convention such as Microsoft TechEd or MMS. Your opinions will help shape our plans for future shows.

Thanks!

Hal Rottenberg
Community Director, PowershellCommunity.org

It shouldn’t take you too long ;-)

posted on Monday, November 10, 2008 10:29:41 AM (Eastern Standard Time, UTC-05:00)  #    Comments [0] Trackback
# Thursday, November 06, 2008

So it looks like the High King of PowerShell has spoken over at TechED Europe today and answered the question that sits on every child’s lips in the back of a moving car: “Are we there yet?”

“Nope. Not Yet.”

“So when, dad?”

Dmitry Sotnikov (who’s  not my dad, btw) of PowerGUI fame broke the news to the web at large, and I’m just shamelessly grabbing the HTML directly from his post:

  • December 2008 - CTP 3 (Community Technology Preview) or Beta 1 if it meets the internal criteria and all names/features are finalized.
  • RTM - end of 2009/early 2010 as part of Windows 7 and Windows Server 2008 R2.
  • RTM for XP, 2003, Vista, and 2008 - as a downloadable package (with new WinRM bundled in there) only a few months later.

Yay!

posted on Thursday, November 06, 2008 1:23:50 PM (Eastern Standard Time, UTC-05:00)  #    Comments [0] Trackback
# Tuesday, November 04, 2008

I’d say a lot of folks will be chomping at the bit for this. It’s the Tk to PowerShell’s Tcl and is really the final piece of the puzzle where the shell’s position as the de-facto Windows Shell stands anyway. Well, maybe Remoting support for XP and Windows Server 2003 is ahead there just a bit, but a forms designer must be a close second.

Check it out: http://blog.sapien.com/index.php/2008/11/03/free-primalforms-tool-for-powershell-released/

Congratulations guys on shipping such a fine tool, for FREE. All it costs you is an email address. Yah, sell your soul, it’ll be worth it.

posted on Tuesday, November 04, 2008 12:32:56 PM (Eastern Standard Time, UTC-05:00)  #    Comments [1] Trackback
# Thursday, October 30, 2008

Fellow PowerShell MVP and developer all-round expert administrator Brandon Shell asked me how he could navigate to a Cmdlet’s implementation in Lutz Roeder’s Reflector -- which has now been acquired by Red-Gate Software btw. At first I was going to explain to him how snap-ins work and how to figure out where things are, and then a really simple trick hit me that uses some fairly secret command-line parameter that Reflector can accept, namely the /select parameter:

  1. function Reflect-Cmdlet {  
  2.     param([Management.Automation.CommandInfo]$command)  
  3.     if ($input) {  
  4.         trap { $_; break }  
  5.         $command = $input | select -first 1  
  6.     }      
  7.          
  8.     # resolve to command if this is an alias  
  9.     while ($command.CommandType -eq "Alias") {  
  10.         $command = Get-Command ($command.definition)  
  11.     }  
  12.       
  13.     $name = $command.ImplementingType      
  14.     $DLL = $command.DLL  
  15.       
  16.     if (-not (gcm reflector.exe -ea silentlycontinue)) {  
  17.         throw "I can't find Reflector.exe in your path." 
  18.     }  
  19.        
  20.     reflector /select:$name $DLL 

Just pipe the output of get-command to it, like: gcm dir | reflect-cmdlet and Reflector will open up with the class selected (it takes a few seconds).

Update: Doug pointed out in a comment that the gcm reflector.exe line could benefit from an erroraction to keep it silent on failure, so only the throw message shows. Thanks Doug!

posted on Thursday, October 30, 2008 2:44:40 PM (Eastern Daylight Time, UTC-04:00)  #    Comments [3] Trackback
# Wednesday, October 15, 2008

The verbs list for naming your Cmdlets is pretty strict. You should definitely try to pick one of the known verbs. You can see them here:

function Get-Verb {
    [psobject].assembly.getexportedtypes() | `
        ? {$_.name -like "Verbs*"} | gm -static -membertype property | `
        sort Name | select Name
}

ps> Get-Verb

The noun end of the spectrum is pretty open-ended. Single nouns are better, and try to use the singular if you can. Cmdlets typically return one or more objects. so the singular form is the convention - better than trying to name your Cmdlet "Get-Noun(s)" which is just plain silly.

To prefix or not to prefix: this is a sticky one. The Religious Right, aka the Microsoft Powershell team, dictate that you should not prefix. The reality is more complicated and is under constant debate. If you pick a verb-noun pair that already exists and is currently loaded into powershell, you must disambiguate the command with the snapin name (snapinname\verb-noun) or it won't run. The snapin name is typically long and ugly. A noun prefix is typically short and sweet. The problem with the latter is that your commands are now harder to discover for someone who doesn't know they are there (and as such doesn't know your magic prefix). Ultimately though, people load your snapin explicitly and should know the prefix, so I tend to lean towards this although I feel a strong connection with the "One Noun Way."

(taken from one of my newgroup posts)

posted on Wednesday, October 15, 2008 12:20:19 PM (Eastern Daylight Time, UTC-04:00)  #    Comments [0] Trackback
# Monday, September 29, 2008

Specifically, get/set-content works with providers that facilitate content reading and writing. Out-File works only with the FileSystem provider and is for all intents and purposes an alias to the > redirection operator.

An interesting thing to note about get-content and set-content is that you use them indirectly every day. Both the variable and function providers support get- and set-content, and when you use the dollar ($) syntax to read or write to a variable, you are implictly calling get-content and set-content against a path in the variable provider. E.g.

ps> $foo = 5

...is the same as:

ps> ${variable:foo} = 5

e.g. set the content of item foo in the psdrive called "Variable" to 5. This is why you can also write to a file like:

ps> ${c:\temp\foo.txt} = "some content"

...and read it back with:

ps> ${c:\temp\foo.txt}
some content

Ultimately, when using the $ syntax, the "variable" drive is the default drive to read and write content from. Elegant, eh?

posted on Monday, September 29, 2008 3:25:09 PM (Eastern Daylight Time, UTC-04:00)  #    Comments [0] Trackback
# Friday, August 15, 2008

The new eventing infrastructure in PowerShell 2.0 is pretty delicious. You couldn’t do the following in 1.0 without a 3rd party snap-in (like my PSEventing snapin), but now it’s all there at the touch of your fingers. Well, it demands a bit of a sniff around WMI too, but hey, it works well.  With this module, anytime you add or remove a removable device like an external harddrive or USB key, or map a new network drive in explorer, PowerShell will now automatically add or remove a corresponding PSDrive for you.

  1. # AutoMount.psm1 v1.0  
  2. # Oisin "x0n" Grehan (MVP)  
  3.  
  4. $query = new-object System.Management.WqlEventQuery  
  5. $query.EventClassName = "__InstanceOperationEvent" 
  6.  
  7. # default to every 2 seconds  
  8. $query.WithinInterval = new-object System.TimeSpan 0,0,2  
  9.  
  10. # this WMI is only available with Windows 2003 and Vista (not XP it appears).  
  11. # this could be rewritten to use different WMI queries to support 2000/NT/XP also.  
  12. $query.QueryString = "Select * from Win32_VolumeChangeEvent" 
  13.  
  14. # attach a watcher  
  15. $watcher = new-object System.Management.ManagementEventWatcher $query 
  16.  
  17. # here we use -SupportEvent instead of -SourceIdentifier  
  18. # this prevents this event from being generally visible  
  19. # also note the use of the call operator to invoke a   
  20. # function in the scope of the module since this action  
  21. # occurs outside of module scope.  
  22. Register-ObjectEvent $watcher -EventName "EventArrived" `  
  23.     -SupportEvent "WMI.VolumeChange" -Action {  
  24.         & (get-module automount) VolumeChangeCallback @args 
  25.     }  
  26.  
  27. # New PSEvents:  
  28. #  
  29. #     PowerShell.DeviceConfigurationChanged  
  30. #     PowerShell.DeviceArrived  
  31. #     PowerShell.DeviceRemoved  
  32. #     PowerShell.DeviceDocking  
  33.  
  34. # win32_volumechangeevent event types  
  35. $eventTypes = @{  
  36.     1 = "ConfigurationChanged";  
  37.     2 = "Arrived";  
  38.     3 = "Removed";  
  39.     4 = "Docking";  
  40. }  
  41.  
  42. # private module level callback function  
  43. function VolumeChangeCallback ($sender, $eventargs) {  
  44.     trap { write-warning $_ }  
  45.  
  46.     $driveName = $eventArgs.NewEvent.DriveName.TrimEnd(":")  
  47.     $eventType = [int]$eventArgs.NewEvent.EventType # was uint16  
  48.  
  49.     $forwardedEvent = "Device$($eventTypes[$eventType])" 
  50.       
  51.     # forward a new simpler event specific to device event type  
  52.     [void]( New-PSEvent "PowerShell.$forwardedEvent" -Sender $driveName `  
  53.         -EventArguments $eventargs )  
  54. }  
  55.  
  56. # hook up our psdrive mount / unmount events  
  57. # and start the WMI watcher  
  58. function Enable-AutoMount {  
  59.  
  60.     Register-PSEvent -SourceIdentifier "PowerShell.DeviceArrived" `  
  61.         -Action {              
  62.             new-psdrive -name $args[0] -psprovider `  
  63.                 filesystem -root "$args[0]:";  
  64.          }  
  65.  
  66.     Register-PSEvent -SourceIdentifier "PowerShell.DeviceRemoved" `  
  67.         -Action {  
  68.             remove-psdrive -name $args[0] -ea 0; # may not exist  
  69.         }  
  70.       
  71.     $watcher.Start()  
  72. }  
  73.  
  74. # tear down our psdrive mount / unmount events  
  75. # and stop the WMI watcher  
  76. function Disable-AutoMount {  
  77.  
  78.     Unregister-PSEvent -SourceIdentifier "PowerShell.DeviceArrived" 
  79.     Unregister-PSEvent -SourceIdentifier "PowerShell.DeviceRemoved" 
  80.       
  81.     $watcher.Stop()  
  82. }  
  83.  
  84. # export functions to control automount  
  85. Export-ModuleMember Enable-AutoMount, Disable-AutoMount  
  86.  
  87. # start watching and (un)mounting  
  88. Enable-AutoMount 

This only works PowerShell v2.0 CTP2, and you’ll need to save it as AutoMount.psm1 in a directory under your documents folder like so (vista example):

%userprofile%\documents\windowspowershell\packages\automount\automount.psm1

You can then load it with the command:

ps> add-module automount

I have this in my profile.  You can temporarily disable automount with the function Disable-AutoMount and reenable it at anytime with Enable-AutoMount. The module also exposes four new events for you to consume yourself. You could, for example, hook your own script to run anytime a device is added and/or removed. This is what I do myself in the module. I hook a WMI event once then forward 1 of 4 possible new events depending on the type of WMI event that was raised.

NOTE: this particular flavour of WMI query only works in Vista and Windows 2003 it appears. I’m looking into getting it working with 2000/XP also.

Have fun!

posted on Friday, August 15, 2008 10:14:47 PM (Eastern Daylight Time, UTC-04:00)  #    Comments [0] Trackback
# Sunday, August 10, 2008

With PowerShell’s new –STA startup switch, interacting with the Windows Forms object model was never so easy:

PS> $text = & {powershell –sta {add-type –a system.windows.forms; [windows.forms.clipboard]::GetText()}}

Easy, eh?

posted on Sunday, August 10, 2008 11:36:37 AM (Eastern Daylight Time, UTC-04:00)  #    Comments [0] Trackback