Passion * Technology * Ruthless Competence

Tuesday, February 20, 2007

Internal DSLs in PowerShell

(Harry is on a secret mission in uncharted space this week, so instead of the daily Morning Coffee post, you get a series of autoposted essays. This post combines both some leftover learnings about Ruby from Harry's Web 2.0 days with his recent obsession with PowerShell.)

My first introduction to the idea of internal DSLs was an article on Ruby Rake by Martin Fowler. Rake is Ruby's make/build utility. Like most build tools like Ant and MSBuild, Rake is a dependency management system. Unlike Ant and MSBuild, Rake doesn't use an XML based language. It uses Ruby itself, which has huge benefits when you start doing custom tasks. In Ant or MSBuild, building a custom task requires you to use a external environment (batch file, script file or custom compiled task object). In Rake, since it's just a Ruby file, you can start writing imperative Ruby code in place.

Here's the simple Rake sample from Fowler's article:

task :codeGen do 
  # do the code generation 
end 

task :compile => :codeGen do 
  # do the compilation 
end 

task :dataLoad => :codeGen do 
  # load the test data 
end 

task :test => [:compile:dataLoad] do 
  # run the tests 
end

The task keyword takes three parameters: the task name, an array containing the task dependencies and a script block containing the code to execute to complete the task. Ruby's flexible syntax allows you to specify task without any dependencies (:codegen), with a single dependency (:compile => :codegen), and with multiple dependencies (:test => [:compile,:dataLoad])

So what would this look like if you used Powershell instead of Ruby? How about this:

task codeGen { 
  # do the code generation 
}

task compile codeGen {
  # do the compilation 
}

task dataLoad codeGen { 
  # load the test data 
}

task test compile,dataLoad {
  # run the tests 
}

Not much different. PS uses brackets for script blocks while Ruby uses do / end, but that's just syntax. Since it lacks Ruby's concept of symbols (strings that start with a colon), PS has to use strings instead. Otherwise, it's almost identical. They even both use the # symbol to represent a line comment.

There is one significant difference. For tasks with dependencies, Rake uses a hash table to package the task name and its dependencies. The => syntax in Ruby creates a hash table. Since the hash table has only a single value, you can leave of the surrounding parenthesis. The key of this single item hash table is the task name while the value is an array of task names this task depends on. Again, Ruby's syntax is flexible, so if you have only a single dependency, you don't need to surround it in square brackets.

In Powershell, the hash table syntax isn't quite so flexible, you have to surround it with @( ). So using Rake's syntax directly would result in something that looked like "task @(test = compile,dataLoad) {...}" which is fairly ugly. You don't need to specify the square brackets on the array, but you having to add the @( is a non-starter, especially since you wouldn't have them on a task with no dependencies.

So instead, I thought a better approach would be to use PS's variable parameter support. Since all tasks have a name, the task function is defined simply as "function task ([string] $name)". This basically says there's a function called task with at least one parameter called $name. (All variables in PS start with a dollar sign.) Any parameters that are passed into the function that aren't specified in the function signature are passed into the function in the $args variable.

This approach does mean having to write logic in the function to validate the $args parameters. Originally, I specified all the parameters, so that it looked like this: "function global:task([string] $name, [string[]] $depends, [scriptblock] $taskDef)". That didn't work for tasks with no dependencies, since it tried to pass the script block in as the $depends parameter.

Here's a sample task function that implements the task function shown above. It validates the $args input and builds a custom object that represents the task. (Note, the various PS* objects are in the System.Management.Automation namespace. I omitted the namespaces to make the code readable.)

function task([string$name)     
{    
  if (($args.length -gt 2-or ([string]::isnullorempty($name)))    
  {    
    throw "task syntax: task name [<dependencies>] [<scriptblock>]"    
  }    
       
  if ($args[0-is [scriptblock])    
  {    
    $taskDef = $args[0]    
  }    
  elseif ($args[1-is [scriptblock])    
  {    
    $depends = [object[]]$args[0]    
    $taskDef = $args[1]    
  }    
  else    
  {    
    $depends = [object[]]$args[0]
    #if a script block isn't passed in, use an empty one    
    $taskDef = {} 
  }    

  $task = new-object PSObject    
  $nameProp = new-object PSNoteProperty Name,$name    
  $task.psobject.members.add($nameProp)    
        
  $dependsProp = new-object PSNoteProperty Dependencies,$depends    
  $task.psobject.members.add($dependsProp)    
        
  $taskMethod = new-object PSScriptMethod ExecuteTask,$taskDef    
  $task.psobject.members.add($taskMethod)    
        
  $task    
}

Of course, you would need much more than this if you were going to build a real build system like Rake in PowerShell. For example, you'd need code to collect the tasks, order them in the correct dependency order, execute them, etc. Furthermore, Rake supports other types of operations, like file tasks and utilities that you'd need to build.

However, the point of this post isn't to rebuild Rake in PS, but to show how PS rivals Ruby as a language for building internal DSLs. On that front, I think PowerShell performs beautifully.

I'm looking forward to using PowerShell's metaprogramming capabilities often in the future.

Posted By Harry Pierson at 11:08 PM Pacific Standard Time
Comments are closed.
Change Congress
Recent Bookmarks
Tags .NET Framework (2) __clrtype__ (9) ADO.NET (5) Agile (7) AJAX (3) Architecture (288) Guidance (6) Interop (2) Modelling (61) Patterns (7) Process (4) SOA (94) Web Services (5) ASP.NET (25) Async Messaging (2) Azure (1) Battlestar Galactica (3) BI (2) BizTalk (4) Blogging (117) dasBlog (11) Podcasting (4) BPM (1) C# (11) C++ (4) Capitals (5) CardSpace (3) CLR (2) CodePlex (1) College Football (10) Comedy Central (1) Community (81) Concurrency (6) Consumer Electronics (1) Database (13) Debugger (23) Dependency Injection (2) Development (122) C Plus Plus (1) Embedded (5) Lanugages (42) Media (2) P2P (11) Rotor (1) SharePoint (6) SOP (3) DIY (1) DLR (25) Domain Specific Languages (15) Durable Messaging (5) Dynamic Languages (12) Dynamic Silverlight (1) Education (3) Enterprise 2.0 (1) Entertainment (14) ETech (15) F# (51) Functional Programming (17) Game Development (2) Guidance Automation (3) Hardware (8) HawkCodeBox (1) HawkEye (3) Health (1) Hockey (31) Home Electronics (1) Home Network (5) Hosting API (1) Humor (5) IASA (1) Idempotence (3) infrastructure (5) Instrumentation (4) Integration (2) IronPython (112) IronRuby (16) Java (2) Job (3) Kodu (1) LangNET (2) Lightweight Debugger (5) LINQ (23) Live Framework (3) Live Mesh (2) Lost (1) Master Data Management (1) Media 2.0 (6) Microsoft (31) MIX06 (2) Mobile Phone (1) Monads (5) Morning Coffee (172) Object Oriented (4) Office (5) Open Source (8) Open Space (2) Operations (3) Other (135) Art (1) Books (1) Family (33) Games (18) General Geekery (27) Home Theater (1) Movies (23) Music (20) Politics (3) Society (1) Sports (37) Working at MSFT (19) Parallel Programming (3) Parsing Expression Grammar (16) patterns & practices (2) PDC08 (5) Politics (48) Polyglot (3) PowerPoint (2) PowerShell (39) Presentation (7) Projects (1) HawkWiki (1) Pygments (5) Python (6) Quote of the Day (4) Refactoring (1) Research (2) REST (18) Reuse (5) Robotics (2) Rock Band (4) Rome (5) Ruby (23) Ruby on Rails (1) Sci-Fi (2) Scripting (4) Security (3) Service Broker (14) SharePoint (2) Silverlight (20) Social Software (1) Software + Services (2) Software Design (2) Software Engineering (1) Software Factories (11) Software Industry (1) Space Elevator (1) Spark (1) SQL Server (2) Stephen Colbert (1) TechEd (7) TechEd06 (1) TechRec League (1) Television (6) Travel (7) Unified Client (1) Unit Testing (4) USC (1) UX (1) Virtual PC (2) Visual Basic (3) Visual Studio (20) Volta (2) Washington Capitals (37) WCF (31) Web 2.0 (67) Web Services (7) WF (21) Windows (3) Windows Live (29) Windows Live Writer (3) WPF (8) Xbox (1) Xbox 360 (54) XML (11) XNA (15) Zune (4)
Disclaimer: The information in this weblog is provided "AS IS" with no warranties, and confers no rights. This weblog does not represent the thoughts, intentions, plans or strategies of my employer. It is solely my opinion. Inappropriate comments will be deleted at the authors discretion.