Good morning kids and welcome to the PowerShell Day Care center. We offer a creative and nurturing environment for PowerShell professionals of all ages. Later there might even be juice and cookies. But first let's get out our blocks, our scriptblocks, and start building. I've written a number of posts on script blocks and today have a script to offer, that even if you don't need it as is, it might offer a few insights into PowerShell.
ManageEngine ADManager Plus - Download Free Trial
Exclusive offer on ADManager Plus for US and UK regions. Claim now!
A scriptblock is a small (usually) chunk of PowerShell code enclosed in curly braces. You see them all the time in cmdlets like Where-Object and Start-Job. Think of scriptblocks as modular and re-usable commands that write objects to the pipeline. Often I think you can use a scriptblock for simple tasks instead of creating a function. For example, suppose I want an easy way to get the current day of the year.
[cc lang="DOS"]
PS C:\> (Get-Date).DayOfyear
265
[/cc]
Short and simple. But I'm lazy. I don't want to have to type that all the time. And while I could save it as a variable, if my PowerShell session is open for a few days, the variable will become stale. This is a simple example anyway. I could create a function but perhaps wrapping this in a scriptblock would be a better approach.
[cc lang="PowerShell"]
$doy={(Get-Date).DayOfyear}
[/cc]
The variable, $doy, is the command.
[cc lang="DOS"]
PS C:\> $doy
(Get-Date).DayOfyear
PS C:\>
[/cc]
To run the scriptblock I can use the invoke operator &.
[cc lang="DOS"]
PS C:\> &$doy
265
[/cc]
Or perhaps we want to quickly get running services. We might create a scriptblock like this:
[cc lang="PowerShell"]
$run={get-service | where {$_.status -eq 'running'}}
[/cc]
If I invoke $run, the command will execute, just as if I had typed it. Where things get tricky is when you want to build a scriptblock from other variables. Remember, scriptblocks run in their own scope. I run into this more often in scripting where I'm building a scriptblock from variables that will execute later. Here's an example of the problem.
[cc lang="PowerShell"]
$log=Read-Host "Enter a log name, ie system"
[int]$count=Read-Host "How many entries do you want?"
$sb={get-eventlog -log $log -newest $count}
start-job $sb
[/cc]
The job gets created but fails because it can't find values for $log and $count. The solution I came up with is to create a string first, taking advantage of variable expansion. Then explicitly define a scriptblock.
[cc lang="PowerShell"]
$log=Read-Host "Enter a log name, ie system"
[int]$count=Read-Host "How many entries do you want?"
$cmd="get-eventlog -log $log -newest $count"
$sb=$executioncontext.InvokeCommand.NewScriptBlock($cmd)
start-job $sb
[/cc]
This is slightly more complicated, but $cmd is defined with the expanded variables and then I create the scriptblock. For you overachievers, you can combine steps and do this:
[cc lang="PowerShell"]
$sb=$executioncontext.InvokeCommand.NewScriptBlock("get-eventlog -log $log -newest $count")
[/cc]
Now, let's take this to the next level. I have a number of scriptblocks that I want to define in my PowerShell profile. I could simply define them in the profile or a dot sourced script but that's too easy. So I wrote a script that parses a text file of scriptblock commands and creates them. First, here's the text file that gets parsed.
[cc lang="DOS"]
#script block commands
#name delimiter command
#default delimiter is the ~
#each command must be on one line
dc01 ~ mstsc -v jdhit-dc01 /console
run ~ gsv | where {$_.status -eq "running"}
bigp ~ ps | where {$_.ws -gt 100mb}
disk ~ Param ([string]$computername=$env:computername) get-wmiobject win32_logicaldisk -filter "drivetype=3" -computername $computername | Select "DeviceID","VolumeName",@{Name="SizeGB";Expression={"{0:N4}" -f ($_.size/1GB)}},@{Name="Freespace";Expression={"{0:N4}" -f ($_.FreeSpace/1GB)}},@{Name="Utilization";Expression={ "{0:P2}" -f (1-($_.freespace -as [double])/($_.size -as [double]))}}
doy ~ (get-date).DayOfYear
up ~ Param([string]$computername=$env:computername) Get-WmiObject win32_operatingsystem -computername $computername |Select @{name="Computer";Expression={$_.CSName}},@{Name="LastBoot";Expression={$_.ConvertToDateTime($_.LastBootUpTime) }},@{Name="Uptime";Expression={(Get-Date)-($_.ConvertToDateTime($_.LastBootUpTime))}}
dirt ~ Param([string]$Path=$env:temp) Get-ChildItem $path -Recurse -force -ea "silentlycontinue" | measure-object -Property Length -sum | Select @{Name="Path";Expression={$path}},Count,@{Name="SizeMB";Expression={"{0:N4}" -f ($_.sum/1mb)}}
[/cc]
The structure of the file is scriptblock name, a delimiter (I"m using ~) and the scriptblock command. The command must be one line. Notice scriptblocks can also take parameters. This file is parsed by my LoadCmds.ps1 script.
[cc lang="PowerShell"]
[cmdletbinding()]
param (
[Parameter(Position=0)]
[ValidateNotNullorEmpty()]
[string]$Path="cmds.txt",
[string]$Delimiter="~",
[switch]$Passthru
)
Write-Verbose "Starting $($myinvocation.mycommand)"
if (Test-Path $path) {
Write-Verbose "Processing $path"
#filter out blanks and lines that start with a comment
Get-Content $path | Where { ($_ -match $delimiter) -AND (!($_.Trim().StartsWith("#"))) } |
foreach -begin { $script:newvariables=@() } -process {
#split the line at the ~
$data=$_.Split("~")
$sb=$data[0].Trim()
$cmd=$data[1].Trim()
Write-Verbose "Creating $sb"
#add the scriptblock name to the array
$script:newvariables+=$sb
#create a script block object and assign it to the value variable
$value=$executioncontext.InvokeCommand.NewScriptBlock($cmd)
#define the new variable
Set-Variable -name $sb -Value $value -Scope Global
} -end {
if ($Passthru) {
#write the new variables to the pipeline if -Passthru
get-variable -Name $script:newvariables
}
} #close end
} #close if
else {
Write-Warning "Failed to find $path"
}
Write-Verbose "Ending $($myinvocation.mycommand)"
#end
[/cc]
The script takes parameters for the filepath and delimiter. It also uses -Passthru if you want to see the variables that get created. Assuming the file is found blanks and commented lines are filtered out.
[cc lang="PowerShell"]
Get-Content $path | Where { ($_ -match $delimiter) -AND (!($_.Trim().StartsWith("#"))) } |
[/cc]
Each line is piped to ForEach-Object where I take advantage of the Begin, Process and End parameters. In the Begin scriptblock I define a script variable for an array to hold each command I'm going to create. In the Process scriptblock each line is split into an array on the delimiter.
[cc lang="PowerShell"]
foreach -begin { $script:newvariables=@() } -process {
#split the line at the ~
$data=$_.Split("~")
$sb=$data[0].Trim()
$cmd=$data[1].Trim()
[/cc]
The variable name is added to the array and the scriptblock defined.
[cc lang="PowerShell"]
$script:newvariables+=$sb
#create a script block object and assign it to the value variable
$value=$executioncontext.InvokeCommand.NewScriptBlock($cmd)
[/cc]
All that is left is to define the scriptblock variable and set it to the global scope.
[cc lang="PowerShell"]
#define the new variable
Set-Variable -name $sb -Value $value -Scope Global
[/cc]
After each line in the text file has been processed the End script block runs and if -Passthru was used, the array with new scriptblock names is written to the pipeline.
[cc lang="PowerShell"]
-end {
if ($Passthru) {
#write the new variables to the pipeline if -Passthru
get-variable -Name $script:newvariables
}
} #close end
[/cc]
Whenever I want to add something, I update the text file. One advantage to the text file is that it is not a PowerShell executable so my execution policy doesn't apply.
If you want to try this out you can download a copy of LoadCmds. Otherwise, see what you can build with scriptblocks and be sure to share.
Question:
Is there any reason to prefer this:
$sb=$executioncontext.InvokeCommand.NewScriptBlock($cmd)
over
$sb = [scriptblock]::create($cmd) ?
Interesting bit:
Given that same scriptblock that returns multiple objects, this will return [object[]]
&$sb
and this will return a generic collection
$sb.invoke()
Thanks for the feedback. There isn’t any specific reason for creating the scriptblock the way I did. I think it is merely old cold that I re-purposed without considering any newer alternatives. That’s a good lesson right there!
Thanks! I use that method when I need to create script blocks dynamically. I’ve used the older method, but I find the create meathod much easier to read (and remember).
Something else interesting I found with V2 was that, while they added the getnewclosure method for re-closing a scriptblock around a set of variables, it actually tested faster to create a brand new script block using an expandable string with those variables than to invoke that method on an existing scriptblock.
Can you give a few examples that explain?
I did a blog article on that some time back:
http://mjolinor.wordpress.com/2011/02/13/getnewclosure-vs-scriptblockcreate/