Tag Archives: Modules

Creating PowerShell Modules, the easy way.

Today I’m going to show you a new way to build a PowerShell module: I think it’s the fastest, easiest way to turn a few PowerShell script files into a reusable, shareable module.

Here’s the “too long; didn’t read” version: I’ve written a function that will allow you to specify a list of scripts and collect them into a module. Get the script from the end of this post, and you can run this command to build a module named “MyUtilities” within the current directory, moving the ps1 scripts into the new sub-directory:

New-ScriptModule .\MyUtilities *.ps1 -move

Or you can run this command to create the “MyUtilities” module in your regular modules folder from all the ps1 or psm1 scripts in this directory or any sub-directories, overwriting any existing module:

New-ScriptModule MyUtilities *.ps1, *.psm1 -recurse -force

If you want greater precision, you can pipe the files in. This command will let you collect only the ps1 and psm1 files in the current directory, but recursively collect ALL files from sub-directories (like language data files, or nested modules), and generate the module containing them.

dir *.ps1, *.psm1, *\* | dir -s | New-ScriptModule MyUtility

Once you’ve created a script module using New-ScriptModule, you can run this command to update it with the files that are currently in it’s folder tree, incrementing the version number:

New-ScriptModule MyUtilities -Upgrade

Note: none of these commands move the scripts, they just copy them. However, you can add the -Move switch to move them instead.

It’s important to note that the scripts you put into a module need to define functions (they can’t be pure scripts that you run by file name, or else they’ll execute when you import the module).

What follows is an explanation of what this function does (and steps for doing it by hand). The first step, of course, is to create a “MyUtilities” folder in your PSModulePath, and copy all of the script files into it. Then we can get started with a simple Root Module.

The Root Module (ModuleToProcess)

The root module is the main script or assembly that’s going to be executed/loaded when the module is imported. In our example case, it’s the “MyUtilities.psm1” file (the name matches the folder and ends in psm1). Basically, copy the line below into a new file and save it as “MyUtilities.psm1”:

Get-ChildItem $PSScriptRoot -Recurse -Filter *.ps1 | Import-Module

That simple version just imports every ps1 file in the module folder into the module itself (it’s just like dot-sourcing them, but cleaner to write). In the end, I decided that I wanted to have the list of scripts that would be imported hard-coded in the root module (which can be signed), to make the whole thing harder to tamper with, so in the script below you’ll see I’m building a slightly more complicated root module.

The Module Manifest

I want you to end up with a good reusable and redistributable module, so you should definitely create a module manifest. You don’t really have to set any properties in the manifest except the RootModule and the version (but that defaults to 1.0, so you don’t have to worry about it at first), but I recommend you also pass at least:

  • The Author parameter (it defaults to your $Env:UserName which is usually not a full name).
  • The CompanyName (otherwise it comes out “Unknown”, you could at least set it to your blog url).
  • The FileList (which will matter for packaging someday).
  • The Description.

Here’s an example of how to use New-ModuleManifest to create one:

$Module = "~\Documents\WindowsPowerShell\Modules\MyUtilities"

$Files = Get-ChildItem $Module\*.* -recurse | Resolve-Path -Relative

New-ModuleManifest -Path "$Module\MyUtilities.psd1" -RootModule "MyUtilities.psm1" -FileList $Files -Author "Joel Bennett" -Functions "*-*" -Aliases "*" -Cmdlets $null -Variables "MyUtilities*" -Types "*Types.ps1xml" -Format "*Formats.ps1xml"

That’s it. We’re done. You can try now try: Import-Module MyUtilities and Get-Command -Module MyUtilities to see the results!

Some additional information, and the script.

I want to add some detail here about the conventions I follow which are enforced or encouraged by this function and the modules and manifests that it produces, but as I’ve gone on long enough, I will save that for another post.

I do want to state one caveat: although the function below produces much more complete modules than the few lines above, it’s still basically a beta — it’s only been tested by me (and a couple of people from IRC, thanks guys), so it’s possible that everything is not perfect. That said:

  • It supports piping files in to generate the module from, as well as specifying a pattern to match files (and even the -recurse switch).
  • It handles nested psm1 modules as well as ps1 scripts, and treats *.Types.ps1xml and *.Formats.ps1xml files appropriately.
  • It sets reasonable defaults for the export filters of functions, aliases and variables.
  • It supports upgrading modules, updating the file list and incrementing the version number, but won’t overwrite your customizations.

The current version generates a default module which lists specifically the files to import, and then pushes location to the module folder before importing the scripts, one at a time, writing progress messages and error messages as appropriate.

This function is part of the larger PoshCode Packaging module that I am working on. Hopefully you’ll find it an easy way to put a bunch of scripts into a module. It should work in PowerShell 2-4 (although I haven’t been able to test on 2 yet), but let me know if you have any problems (get it from poshcode).

Get-Command in PowerShell 3 (NOTE: CTP2 Bug causes module loading)

I don’t normally blog about the bugs I find in beta software, but I posted this bug to PowerShell’s Connect and I feel like it got ignored and not voted, so I’m going to try to explain myself better here … The bug is on Connect, but let me talk to you first about how Get-Command is supposed to work.

In PowerShell, Get-Command is a command that serves two purposes: first it lets you search for commands using verb, noun, wildcards, module names etc. and then it also returns metadata about commands. In PowerShell 2, it could only search commands that were in modules (or snapins) you had already imported, or executables & scripts that were in your PATH.

So here’s the deal: Get-Command has always behaved differently when it thinks you’re searching. The only way it can tell that you’re searching is that you don’t provide a full command name. So, if you use a wildcard (e.g.: Get-Command Get-Acl* or even Get-Command Get-Ac[l]), or search using a Noun or Verb (e.g.: Get-Command -Verb Get or Get-Command -Noun Acl or even Get-Command -Verb Get -Noun Acl), then PowerShell assumes you’re searching (and won’t throw an error when no command is found).

In PowerShell 3, because modules can be loaded automatically when you try to run a command from them, Get-Command had to be modified to be able to return commands that aren’t already loaded. The problem the PowerShell team faced is that in order to get the metadata about a command, they needed to actually import the module. What they came up with is that if you’re searching … then Get-Command will not load modules which aren’t already loaded. If you specify a full command name with no wildcards, then PowerShell will load any module(s) where it finds a matching command in order to get the metadata (parameter sets, assembly info, help, etc). And of course, if you specify a full command that doesn’t exist, you’ll get an error!

Perhaps a few examples will help:

Launch PowerShell 3 using:

powershell -noprofile -noexit -command "function prompt {'[$($myinvocation.historyID)]: '}"
 

And then try this, noticing how much more information you get when you specify a specific full name:


[1]: Get-Module
[2]: Import-Module Microsoft.PowerShell.Utility
[3]: Get-Command -Verb Get -Noun Acl | Format-List

Name             : Get-Acl
Capability       : Cmdlet
Definition       : Get-Acl
Path             :
AssemblyInfo     :
DLL              :
HelpFile         :
ParameterSets    : {}
ImplementingType :
Verb             : Get
Noun             : Acl


[4]: Get-Module

ModuleType Name                                ExportedCommands
---------- ----                                ----------------
Manifest   Microsoft.PowerShell.Utility        {Add-Member, ...}

[5]: Get-Command Get-Acl | Format-List

Name             : Get-Acl
Capability       : Cmdlet
Definition       : Get-Acl [[-Path] <string[]>] [-Audit]
                   [-AllCentralAccessPolicies] [-Filter <string>]
                   [-Include <string[]>] [-Exclude <string[]>]
                   [-UseTransaction] [<CommonParameters>]

                   Get-Acl -InputObject <psobject> [-Audit]
                   [-AllCentralAccessPolicies] [-Filter <string>]
                   [-Include <string[]>] [-Exclude <string[]>]
                   [-UseTransaction] [<CommonParameters>]

                   Get-Acl [[-LiteralPath] <string[]>] [-Audit]
                   [-AllCentralAccessPolicies] [-Filter <string>]
                   [-Include <string[]>] [-Exclude <string[]>]
                   [-UseTransaction] [<CommonParameters>]
Path             :
AssemblyInfo     :
DLL              : C:\Windows\Microsoft.Net\assembly\GAC_MSIL\
                   Microsoft.PowerShell.Security\
                   v4.0_3.0.0.0__31bf3856ad364e35\
                   Microsoft.PowerShell.Security.dll
HelpFile         : Microsoft.PowerShell.Security.dll-Help.xml
ParameterSets    : {[[-Path] <string[]>] [-Audit]
                   [-AllCentralAccessPolicies] [-Filter <string>]
                   [-Include <string[]>] [-Exclude <string[]>]
                   [-UseTransaction] [<CommonParameters>],
                   -InputObject <psobject> [-Audit]
                   [-AllCentralAccessPolicies] [-Filter <string>]
                   [-Include <string[]>] [-Exclude <string[]>]
                   [-UseTransaction] [<CommonParameters>],
                   [[-LiteralPath] <string[]>] [-Audit]
                   [-AllCentralAccessPolicies] [-Filter <string>]
                   [-Include <string[]>] [-Exclude <string[]>]
                   [-UseTransaction] [<CommonParameters>]}
ImplementingType : Microsoft.PowerShell.Commands.GetAclCommand
Verb             : Get
Noun             : Acl


[6]: Get-Module

ModuleType Name                                ExportedCommands
---------- ----                                ----------------
Manifest   Microsoft.PowerShell.Security       {ConvertFrom-Sec...}
Manifest   Microsoft.PowerShell.Utility        {Add-Member, ...}
 

But there are several problems:

Get-Command has another parameter: -Module, which allows you to specify which modules should be searched, and in PowerShell 3, it changes the behavior in weird (buggy) ways:

  1. If you specify a single module, then that module is imported (to search it more thoroughly?), even if you specify a specific command that’s not in that module.
  2. If you specify a single module that does not have a command that matches, then Microsoft.PowerShell.Management is loaded also. I don’t know why yet.
  3. If you specify more than one module, and you’re searching, and none of them have a command that matches … it’s just as though you hadn’t specified modules, and nothing unexpected happens.
  4. If you specify more than one module, and a specific command, then it gets really wierd:
    • If the command is in one (or more) of the specified modules, the first module (in PATH order, not the order you specified) which you listed that has the command is imported.
    • If it’s a valid command in a different module, the first module with the command is loaded … and so is Microsoft.PowerShell.Management. I don’t know why! Oh, and you still get the error because it can’t find the command where you told it to look.

I filed a bug on Connect to cover that last scenario where the module containing the command is loaded even though you gave Get-Command a list of modules to look in, here’s another example, and notice that even though all I do here is run the same command over and over (I added some Get-Module to show you WHY you get these results, but it’s the same without them), but I get different results:


[1]: Import-Module Microsoft.PowerShell.Utility
[2]: Get-Module

ModuleType Name                                ExportedCommands
---------- ----                                ----------------
Manifest   Microsoft.PowerShell.Utility        {Add-Member, ...}


[3]: Get-Command Get-Acl -module (Get-Module) # Passes one module
Get-Command : The term 'get-acl' is not recognized as the name of a
cmdlet, function, script file, or operable program. Check the
spelling of the name, or if a path was included, verify that the
path is correct and try again.

[4]: Get-Module

ModuleType Name                                ExportedCommands
---------- ----                                ----------------
Manifest   Microsoft.PowerShell.Management     {Add-Computer, ...}
Manifest   Microsoft.PowerShell.Utility        {Add-Member, ...}


[5]: Get-Command Get-Acl -module (Get-Module) # Passes two modules
Get-Command : The term 'get-acl' is not recognized as the name of a
cmdlet, function, script file, or operable program. Check the
spelling of the name, or if a path was included, verify that the
path is correct and try again.

[6]: Get-Module

ModuleType Name                                ExportedCommands
---------- ----                                ----------------
Manifest   Microsoft.PowerShell.Management     {Add-Computer, ...}
Manifest   Microsoft.PowerShell.Security       {ConvertFrom-Sec...}
Manifest   Microsoft.PowerShell.Utility        {Add-Member, ...}

[7]: # This time it will include Microsoft.PowerShell.Security!
[7]: Get-Command Get-Acl -module (Get-Module)

Capability      Name                ModuleName
----------      ----                ----------
Cmdlet          Get-Acl             Microsoft.PowerShell.Security
 

Working with multiple versions of PowerShell Modules

What follows is a brief explanation of modules, followed by an explanation of how I handle multiple versions of a module. My way isn’t the only way — and I’m rather annoyed that I have to do it — but it works, and might help someone, so here it is.

Some back-story

During development of a PowerShell module like ShowUI, I am usually not using a released version of the module, since I’m obviously working on the next version. In addition, sometimes I need to have “beta” and “stable” releases … so I might have two or three versions of the module that I need to be able to load in order to support users and actually do development.

In case you don’t know how modules work, here’s the deal: There’s an environment variable PSModulePath. It’s just like the Path environment variable, but for modules. The variable contains a semi-colon delimited list of folders paths, which PowerShell searches for modules. Some people think of a PowerShell module as basically a .dll (binary module), a .psm1 (script module), or a .psd1 (manifest module)... but it’s never just a file, it’s a *Folder* and a file. In order for PowerShell to find ShowUI when you write Import-Module ShowUI, you have to have a Folder in the PSModulePath named “ShowUI” and in it, a file (either .dll, .psm1, or .psd1) also named ShowUI.

What I would like

The Import-Module command has a -Version parameter. What I expected What I would like to be able to do is create a folder “ShowUI” and in there, create folders “1.0” and “1.1” and “1.2” and then be able to use Import-Module ShowUI -Version 1.1 to load a specific version.

Sadly, this doesn’t work. First of all, the -Version parameter on Import-Module, is a minimum only. Additionally, Import-Module requires the file and folder to match names exactly, so putting the module inside a subfolder doesn’t work even if there’s only one.

However, there are two things we can use to fix our problem:

1. Just like the PATH variable, PSModulePath works in order. PowerShell searches the listed paths one at a time and uses the first Module that meets the specified minimimum version.
2. Additionally, PowerShell allows you to specify a relative Path to a module that starts in a PSModulePath … I’ll explain more in a moment.

My actual working environment

All of my development projects are in $Home\Projects, so I put a “Modules” folder in there which is where I put modules that I’m actively working on. Then I add that folder to my PSModulePath environment variable: @$Env:PSModulePath += “;$Home\Projects” ...

Since I put it on the end, modules in there won’t be imported if there’s a module with the same name in my regular Module folder ( $Home\Documents\WindowsPowerShell\Modules ), unless I increment the version number in the psd1 metadata file and specify -Version when importing.

For example, I have two copies of WASP:

$Home\Documents\WindowsPowerShell\Modules\WASP\Wasp.psd1 (version 1.0)
$Home\Projects\Modules\WASP\Wasp.psd1 (version 2.0.0.2)

If I want to load the released version, I can just Import-Module WASP — this ensures that scripts that I have in my scripts folder generally load the released version of WASP. When I want to load the development version, I can Import-Module WASP -Version 2.0, and get the version from my Projects folder, because the released version’s version number is lower than 2.0.

More layers onion boy?

When I need to have more than one older version, this presents a problem, because I would need another folder in my path, and for the numbering scheme to work despite the fact that -Version is only a minimum value, I would need the released version (say 1.0) to come first in the path, then the beta version (say 2.0.1), then the development version (say 2.1) ... which means I’d need to add a $Home\Projects\BetaReleaseModules or something like that, and put it in my PSModulePath before the $Home\Projects\Modules folder. That would work, but you can imagine that it wouldn’t scale very well.

Here’s what I do: past the release and development versions, I start creating folders like this:

$Home\Documents\WindowsPowerShell\Modules\ShowUI1.0\ShowUI.psd1 (version 1.0)
$Home\Documents\WindowsPowerShell\Modules\ShowUI\ShowUI.psd1 (Release version: 1.1)
$Home\Documents\WindowsPowerShell\Modules\ShowUI1.1\ShowUI.psd1 (Release version: 1.1)
$Home\Documents\WindowsPowerShell\Modules\ShowUI1.2\ShowUI.psd1 (Beta version 1.2)
$Home\Projects\Modules\ShowUI\ShowUI.psd1 (Development version 1.3)

Now, if I Import-Module ShowUI, I’ll get 1.1 and if I specify Import-Module ShowUI -Version 1.2 I’ll get … version 1.3 (whoops).

To load ShowUI 1.2 with that structure I have to do this: Import-Module ShowUI1.2\ShowUI — specifying the folder name and the module name. Of course, I could use “ShowUIBeta” as the folder name instead, but usually when I get a request for help writing scripts, I can get people to give me the specific version that they’re working with, so I like the numbered folders instead.

A last important note is that I do not rename the psd1 or anything else.

I could rename the metadata file to $Home\Documents\WindowsPowerShell\Modules\ShowUI1.2\ShowUI1.2.psd1 and then I’d be able to Import-Module ShowUI1.2, but there’s a catch. If I rename it, then I’ve renamed the module. If I leave the metadata file as “ShowUI.psd1” then once I’ve imported the module I can call Get-Module ShowUI or Get-Command -Module ShowUI and they still work as they should.

In any case, this folder-naming stuff is less than ideal and will hopefully be fixed in a future release of PowerShell, but with a “Projects\Modules” folder I can have two versions, and usually a development and release version is enough. Personally, I keep old releases of major modules around in numbered folders just in case, but I rarely actually use them.

PowerShell Scripting Best Practices: Prefix A

I’m starting a new series of blog posts about Best Practices for scripting in PowerShell, and I was going to start at the beginning with a requirement that you should use [CmdletBinding()], but the explanation of that will have to wait for the next post, because a bug in PowerShell 2.0 has surfaced which can only be avoided by carefully following a couple of rules … and I’m going to issue those rules as the prefix for the best practices.

Rule #1: Never use Get-Module -ListAvailable in a module.

You need to run the command outside of your module. The simplest way to do that is to use Invoke-Command { Get-Module -List Available } … that should give you the same output, but without the nasty side effects.

There is a bug in the ListAvailable parameter, which causes PowerShell to mark other modules which are already loaded as being “nested” in your module.

It does that to all modules which have correct manifests, and the problem is that if your module gets unloaded later (using Remove-Module), it removes these nested modules as well … even though the user had loaded them separately.

[new] Edit: 2010-08-05

I should have mentioned this originally (so I’m adding it now): one of the reasons developers were using Get-Module with -ListAvailable was to get information about their own module during the initial load (such as to check module versions, or load data from the PrivateData). If you were doing that, you can use Test-ModuleManifest instead with the path to your manifest: Test-ModuleManifest $PSScriptRoot\ModuleName.psd1

Rule #2: Do not use Import-Module in a module.

If you have a dependency on another module, you should load it by specifying it as a NestedModules in your module manifest.

There is a bug in the way that Remove-Module unloads modules which causes modules which are loaded from inside your module (whether by Import-Module or by Get-Module -ListAvailable), to be unloaded completely even if they’ve been previously loaded in the console (global) scope interactively by the user (or via their profile).

You could possibly make an exception to this rule, if the module is actually in your module’s folder (and thus, not easily loadable from outside your module), but you’re making the assumption that no one will ever use that module outside of your module.

Modules loaded via the metadata file’s NestedModules property don’t have this problem, so you should always load nested modules that way.

How to Import Binary Modules from Network Shares

Note: This is from a wiki page I just wrote on Importing Binary Modules from Network Shares which discusses not just the solution below that works for .Net 2.0 but also how to solve the problem on .Net 4.0 (e.g.: in PoshConsole). I will most likely not keep this page up to date, so you should refer to that wiki if you need more information.

Almost every author of a binary module has probably had someone ask about this at some point, because there’s always someone who has their user profiles stored on a network location, and therefore installed their modules on that network path and can’t get them to load because they get a warning that .Net “Failed to grant minimum permission requests.”

Before we get into this any further let me just say: by far the simplest thing to do is to create a local folder on your local hard drive, add that to your environment PSModulePath variable, and just install your modules there.

Other than that, the solution depends on the version of .Net that you’re using (you can tell by checking the $PSVersionTable.CLRVersion

The .Net 2.0 framework (and 3.0 and 3.5 and 3.5 SP1)

The problem is not a PowerShell problem at all, it’s a .Net problem. The .Net framework 2.0 (remember that PowerShell targets 2.0, and is actually based on .Net 1.1) didn’t trust assemblies loaded from network shares. You can fix that for an individual assembly or for a whole share using the Caspol tool.

A complete discussion of that tool and it’s myriad command-line options is beyond me, but for a simple solution, you can run this command specifying the server and share you want to load from (in my example the “Modules” share on the “ProfileServer” server).

Set-Alias CasPol "$([Runtime.InteropServices.RuntimeEnvironment]::GetRuntimeDirectory())CasPol.exe"
CasPol -pp off -machine -addgroup 1.2 -url file://\ProfileServer\Modules\* FullTrust

Hopefully the only thing that needs explaining there is that 1.2 is the default “Local Intranet” group, and that CasPol.exe is in your Framework Runtime directory. Obviously you should customize the file:// path, but once you’ve run that, you’ll be able to import any modules that are in sub-directories of the specified share.

Note: You must run the version of CasPol.exe which is in the location defined by the GetRuntimeDirectory() command (it’s important to use the same version as the runtime you want to be affected).

You used to be able to read more about importing binary modules from network shares, including how it changed in .Net 3.5 SP1 and why it’s not automatically fixed in .Net 4 over on the PoshCode wiki, but it’s blank right now (I’m leaving up the link in hopes it will return). :)

A DSL for XML in PowerShell: New-XDocument

In July of last year I wrote a PowerShell script with the goal of allowing me to generate XML from PowerShell with a simple markup that would look a little like the resulting XML ... this week I was using that script again, and had a couple of issues that made me go back and look at the source.

While I was playing with the source and tweaking things a little bit to improve the way it handles namespaces, I started playing with the idea that I could improve the syntax. At the very least, I thought, I ought to be able to do away with all those “xe” aliases…

Well, I was able to. ( [new] new version) And what’s more, I managed to dramatically clean up the way namespaces work, and make it so that really, the only ugly part of the syntax is the initial declaration of namespaces! I’m going to start with two examples, and use them to walk you through the features :)

Example 1

The simplest example I could think of is to list all the files in a folder, with the file size and last modified stamp:

[string]$xml = New-XDocument folder -path $pwd {
   foreach($file in Get-ChildItem) {
      file -Modified $file.LastWriteTimeUtc -Size $file.Length { $file.Name }
   }
}

The output of that, when run on my formats folder, looks like this:

<folder path="C:\Users\Jaykul\Documents\WindowsPowerShell\formats">
  <file modified="2009-11-07T07:27:00Z" size="30474">CliXml.xsd</file>
  <file modified="2009-11-07T07:27:40.48001Z" size="14314">format.xsd</file>
  <file modified="2010-01-16T21:30:06.0562796Z" size="18275">NppExternalLexers.xml</file>
  <file modified="2009-03-18T21:28:51.6579351Z" size="5802">Recommender.Types.Format.ps1xml</file>
  <file modified="2009-11-07T07:27:40.518029Z" size="5107">types.xsd</file>
</folder>

You can immediately see what the script does: New-XDocument (which is aliased as ‘xml’) actually generates the root xml node, so the first argument to it is the name of that node, and any other arguments become attributes … except for the script block. That script block turns into the contents of the node.

Inside the script block, PowerShell code is parsed as usual, but whenever a command that doesn’t exist is encountered, it is turned into an xml node! Pretty simple, right? Of course, if you wanted to create a node with a name that’s already taken by a PowerShell command, you can just replace file with New-XElement file, or (using aliases) xe 'file', which explicitly creates an xml node with the given name.

That’s pretty much it for our first example, so let’s look at a more complicated example, with multiple namespaces, and deeper nesting.

Example 2

This time, we’ll create an Atom document, and we’ll include some namespace extensions (including a made up one for listing my files as we did above):

New-XDocument (([XNamespace]"http://www.w3.org/2005/Atom") + "feed")          `
              -fi ([XNamespace]"http://huddledmasses.org/schemas/FileInfo")   `
              -dc ([XNamespace]"http://purl.org/dc/elements/1.1")             `
              -$([XNamespace]::Xml +'lang') "en-US" -Encoding "UTF-16"        `
{
   title {"Huddled Masses: You can do more than breathe for free..."}
   link {"http://HuddledMasses.org/"}
   updated {(Get-Date -f u) -replace " ","T"}
   author {
      name {"Joel Bennett"}
      uri {"http://HuddledMasses.org/"}
   }
   id {"http://HuddledMasses.org/" }

   entry {
      title {"A DSL for XML in PowerShell: New-XDocument"}
      link {"http://HuddledMasses.org/A-DSL-for-XML-in-PowerShell-New-XDocument/" }
      id {"http://HuddledMasses.org/A-DSL-for-XML-in-PowerShell-New-XDocument/" }
      updated {(Get-Date 2010/03/03 -f u) -replace " ","T"}
      summary {"A while back, I posted a simple mini language for generating XML from PowerShell script. However, I was using it the other day, and I really just felt that the markup was ugly, since it was littered with 'xe' marks and such."}
      link -rel license -href "http://creativecommons.org/licenses/by/3.0/" -title "CC By-Attribution"
      dc:rights { "Copyright 2010, Some rights reserved (licensed under the Creative Commons Attribution 3.0 Unported license)" }
      category -scheme "http://huddledmasses.org/tag/" -term "xml"
      category -scheme "http://huddledmasses.org/tag/" -term "PowerShell"
      category -scheme "http://huddledmasses.org/tag/" -term "DSL"
      fi:folder -Path "~\Formats" {
         foreach($file in Get-ChildItem (Join-Path (Split-Path $profile) Formats)) {
            fi:file -Created $file.CreationTimeUtc -Modified $file.LastWriteTimeUtc -Size $file.Length { $file.Name }
         }
      }
   }
} | % { $_.Declaration.ToString(); $_.ToString() }

There are four things you should notice, in particular:

First: the initial tag has a [XNamespace] added to it. You can specify a tag name that has a namespace by adding them together this way, or by embedding the namespace in the string like "{http://www.w3.org/2005/Atom}feed" instead. Either way works. This initial namespace becomes the default namespace for the document. If you don’t specify a namespace on tags later, they automatically belong to that one.

Second: when you want to add additional namespaces, you can do so with a custom prefix like: -dc ([XNamespace]"http://purl.org/dc/elements/1.1"), and that prefix (dc) takes on a special meaning. When you want to have a tag later on that is part of that namespace, you just prefix the tag, like dc:rights —the same way you would in XML.

Third: any number of attributes can be specified using the -name value syntax, but anything in a {scriptblock} becomes the content — and is subject to the same rules as the outer sections.

Fourth: This generates an XDocument. When you cast an XDocument to string, the xml declaration is left off, so if you want it, you need to manually add it via $XDocument.Declaration. Incidentally, XDocuments are not XMLDocuments, but they are trivially castable to them.

The output of that particular section of New-XDocument is this:

<feed xmlns:dc="http://purl.org/dc/elements/1.1" xmlns:fi="http://huddledmasses.org/schemas/FileInfo" xml:lang="en-US" xmlns="http://www.w3.org/2005/Atom">
 
  <link />http://HuddledMasses.org/
  <updated>2010-03-04T00:44:31Z</updated>
  <author>
    <name>Joel Bennett</name>
    <uri>http://HuddledMasses.org/</uri>
  </author>
  <id>http://HuddledMasses.org/</id>
  <entry>
   
    <link />http://HuddledMasses.org/A-DSL-for-XML-in-PowerShell-New-XDocument/
    <id>http://HuddledMasses.org/A-DSL-for-XML-in-PowerShell-New-XDocument/</id>
    <updated>2010-03-03T00:00:00Z</updated>
    <summary>A while back, I posted a simple mini language for generating XML from PowerShell script. However, I was using it the other day, and I really just felt that the markup was ugly, since it was littered with 'xe' marks and such.</summary>
    <link rel="license" href="http://creativecommons.org/licenses/by/3.0/" title="CC By-Attribution" />
    <dc:rights>Copyright 2010, Some rights reserved (licensed under the Creative Commons Attribution 3.0 Unported license)</dc:rights>
    <category scheme="http://huddledmasses.org/tag/" term="xml">
    <category scheme="http://huddledmasses.org/tag/" term="PowerShell">
    <category scheme="http://huddledmasses.org/tag/" term="DSL">
    <fi:folder path="~\Formats">
      <fi:file created="2009-11-07T07:27:00Z" modified="2009-11-07T07:27:00Z" size="30474">CliXml.xsd</fi:file>
      <fi:file created="2009-11-07T07:27:40.4529965Z" modified="2009-11-07T07:27:40.48001Z" size="14314">format.xsd</fi:file>
      <fi:file created="2009-02-07T13:56:12Z" modified="2010-01-16T21:30:06.0562796Z" size="18275">NppExternalLexers.xml</fi:file>
      <fi:file created="2009-08-09T19:10:06.3647094Z" modified="2009-03-18T21:28:51.6579351Z" size="5802">Recommender.Types.Format.ps1xml</fi:file>
      <fi:file created="2009-11-07T07:27:40.4970185Z" modified="2009-11-07T07:27:40.518029Z" size="5107">types.xsd</fi:file>
    </fi:folder>
  </category></category></category></entry>
</feed>

The New-XDocument script itself is on PoshCode in the Xml Module 4 along with a few interesting functions like Select-XML (which improves over the built-in by being able to ignore namespaces when you write XPath) and Remove-XmlNamespace (which was instrumental in removing namespaces for Select-Xml). There’s also a Format-Xml for pretty-printing, and a Convert-Xml for processing XSL transformations.

I’ll probably post some more examples of this in the next week or two, and I really should write some commentary about the function itself, which uses the tokenizer to discover which “commands” are really xml nodes … but for now, I’ll leave you to enjoy.

Reblog this post [with Zemanta]