In previous chapters you learned step by step how to use various PowerShell command types and mechanisms. After 11 chapters, we have reached the end of the list. You'll now put together everything you've seen. All of it can actually be reduced to just two PowerShell basic principles: command discovery and scriptblocks.
The purpose of this chapter is to tie up the many loose ends of previous chapters and to weave them into a larger whole: the basics are complete and the remaining chapters will put the knowledge gained to the test of daily tasks.
Topics Covered:
Command Discovery
From the user's point of view, it's rather easy to assign tasks to PowerShell: you type the command in the console, press (Enter), and the command is immediately carried out. But much more complex things are happening behind the scenes. PowerShell has to first find out which command you actually meant. This operation is called command discovery and is usually performed automatically. Use the Get-Command cmdlet if you want to run command discovery yourself to understand what is actually taking place.
If you'd like to know what the Dir command actually is, pass it to Get-Command:
Get-Command Dir
CommandType Name Definition
----------- ---- ----------
Alias Dir Get-ChildItem
Get-Command correctly identifies your command in the CommandType column as Alias and reports in the Definition column which actual command PowerShell invoked. Basically, this is the way it functions for all commands—even if you invoke the external programs:
Get-Command ping
CommandType Name Definition
----------- ---- ----------
Application PING.EXE C:\Windows\system32\PING.EXE
This time the CommandType column reports the Application type, and in the definition the exact path is output to the external program.
In fact, Get-Command returns a CommandInfo object that contains much more information. From Chapter 5 you know how to make all object properties visible: send the object to a formatting cmdlet like Format-List and type an asterisk after it:
$info = Get-Command ping
$info.GetType().FullName
System.Management.Automation.ApplicationInfo
$info | Format-List *
FileVersionInfo : File: C:\Windows\system32\PING.EXE
InternalName: ping.exe
OriginalFilename: ping.exe.mui
FileVersion: 6.0.6000.16386 (vista_rtm.061101-2205)
FileDescription: TCP/IP Ping Command
Product: Microsoft® Windows® operating system
ProductVersion: 6.0.6000.16386
Debug: False
Patched: False
PreRelease: False
PrivateBuild: False
SpecialBuild: False
Language: English (United States)
Path : C:\Windows\system32\PING.EXE
Extension : .EXE
Definition : C:\Windows\system32\PING.EXE
Name : PING.EXE
CommandType : Application
Command discovery gets really interesting when there are several commands that have the same name. The question is which of these commands PowerShell is executing:
Get-Command more
CommandType Name Definition
----------- ---- ----------
Function more param([string[]]$paths); If(($paths -ne...
Application more.com C:\Windows\system32\more.com
As you see, there are two commands called more. One is a PowerShell function (CommandType: Function) and the other an external program called more.com (CommandType: Application). If you use more as a command, PowerShell will automatically choose the one it uses based on its own internal priority list from among several commands having the same name. Because PowerShell functions have a higher priority than external applications, the internal PowerShell functions will always be the first in line:
Dir | more
If you'd prefer using the external program more.com, you must specify it explicitly:
Dir | more.com
That works because if you specify the command name more.com there's no danger of confusing the names:
Get-Command more.com
CommandType Name Definition
----------- ---- ----------
Application more.com C:\Windows\system32\more.com
However, there's no guarantee because there could be an alias called more.com on your system. That's why you'll soon learn better methods to execute exactly the command you want to execute with utter precision. But first you'll have to know how PowerShell actually invokes commands.
The Call Operator "&"
The little call operator "&" gives you great discretionary power over the execution of PowerShell commands. If you place this operator in front of a string (or a string variable), the string will be interpreted as a command and executed just as if you had input it directly into the console.
$command = "Dir"
$command
Dir
& $command
Directory: Microsoft.PowerShell.Core\FileSystem::C:\Users\Tobias Weltner
Mode LastWriteTime Length Name
---- ------------- ------ ----
d---- 10.01.2007 16:09 Application Data
d---- 07.26.2007 11:03 Backup
(...)
The call operator also comes to the rescue when the command name contains special characters like white space and can't be input directly into the console. Put the name in quotation marks to turn it into string and use the call operator to run it:
& "A command with whitespace"
The Call Operator Only Accepts Single Commands
However, the call operator won't run an entire instruction line but always precisely one command. If you had assigned not just a single command, like Dir in the variable but several commands, or if you had also specified arguments for the command, an error would have been generated:
$command = "Dir C:\"
& $command
The term "Dir c:\" is not recognized as a cmdlet, function,
operable program, or script file. Verify the term and try again.
At line:1 Char:2
+ & <<<< $command
Why is that? The reason: in the murky depths of PowerShell, the call operator calls Get-Command to find out what it is supposed to actually be running. Get-Command always gets only a single command, never entire instruction lines:
Get-Command "Dir"
CommandType Name Definition
----------- ---- ----------
Alias Dir Get-ChildItem
Get-Command "Dir C:\"
Get-Command : The term "Dir c:\" is not recognized as a
cmdlet, function, operable program, or script file. Verify
the term and try again.
At line:1 Char:12
+ Get-Command <<<< "Dir C:\"
The Call Operator Executes CommandInfo Objects
The call operator initially passes what you specify as a command to Get-Command, which returns a CommandInfo object that the call operator then executes. In fact, the call operator can also accept a CommandInfo object directly and save itself the roundabout Get-Command:
$command = Get-Command ping
$command
CommandType Name Definition
----------- ---- ----------
Application PING.EXE C:\Windows\system32\PING.EXE
& $command -n 1 -w 100 10.10.10.10
Pinging 10.10.10.10 with 32 bytes of data:
Reply from 10.10.10.10: Bytes=32 Time<1ms TTL=128
Ping statistics for 10.10.10.10:
Packets: Sent = 1, Received = 1, Lost = 0 (0% Loss),
Ca. time in millisec:
Minimum = 2ms, Maximum = 2ms, Average = 2ms
The & $command calls the command in $command. You may specify any arguments after it, but you can't wrap the arguments directly in $command because the call operator always executes only one single command without arguments.
& "Dir C:\"
Did you just have a little déjà-vu experience? Aliases behave exactly the same way and can provide only single commands under another name, but not commands with arguments. Aliases are nothing more than named call operators. If you input the alias, PowerShell will internally invoke the call operator for the command that you assigned to the alias.
Identically Named Commands: Which is Running?
PowerShell supports a great many commands of the most diverse types, cmdlets, functions, aliases, or external commands. Within this range of command types, command names should not be ambiguous as there can never be more than one function or alias having the same name. However, among the various command types, names can be identical; usually, that's even highly desirable.
If there are several commands having identical names, PowerShell will examine its own internal priority list (Table 12.1) and decide which command will be executed. For example, you can use aliases to set up "command redirection" because aliases have a higher priority than external programs.
ping -n 1 10.10.10.10
Pinging 10.10.10.10 with 32 bytes of data:
Reply from 10.10.10.10: Bytes=32 Time<1ms TTL=128
Ping statistics for 10.10.10.10:
Packets: Sent = 1, Received = 1, Lost = 0 (0% Loss),
Ca. time in millisec:
Minimum = 2ms, Maximum = 2ms, Average = 2ms
function Ping { "Ping is not allowed." }
ping -n 1 10.10.10.10
Ping is not allowed.
PowerShell functions have a higher priority than external commands, and that's why PowerShell has executed its new Ping function instead of the old Ping command. You have seemingly brought the Ping command to a halt. Instead of a function, you could also have created an alias, which has an even higher priority so that your newly created function would then no longer be invoked.
Set-Alias ping echo
ping -n 1 10.10.10.10
-n
1
10.10.10.10
Now, Ping calls the Echo command, which is an alias for Write-Output and simply outputs the parameters that you may have specified after Ping in the console.
If you'd like to see all the commands of a particular type, specify the -commandType parameter. The next statement lists all commands of the Filter type:
Get-Command -commandType Filter
| CommandType |
Description |
Priority |
| Alias |
An alias for another command added by using Set-Alias |
1 |
| Function |
A PowerShell function defined by using function |
2 |
| Filter |
A PowerShell filter defined by using filter (a function with a process block) |
2 |
| Cmdlet |
A PowerShell cmdlet from a registered snap-in |
3 |
| Application |
An external Win32 application |
4 |
| ExternalScript |
An external script file with the file extension ".ps1" |
5 |
| Script |
A scriptblock |
- |
Table 12.1: Various PowerShell command types
If you enter the Ping command in this example, Get-Command will first find out which commands are possible:
Get-Command Ping
CommandType Name Definition
----------- ---- ----------
Function Ping "Ping is not allowed."
Alias ping echo
Application PING.EXE C:\Windows\system32\PING.EXE
Based on the internal PowerShell priority list, the command of the alias type is selected from these three commands and executed. If you'd rather run another Ping command, you will have to circumvent automatic selection.
You've seen that the call operator accepts commands in two ways: either as a string (in which case it tasks Get-Command with automatically choosing an appropriate command) or as a CommandInfo object (in which case it is clear which command is meant). So, if you'd like to run a particular command yourself, get its CommandInfo object. That will retrieve Get-Command. If you'd like to run the original Ping command, the third array element is suitable:
$commands = Get-Command Ping
& $commands[2] -n 1 10.10.10.10
Pinging 10.10.10.10 with 32 bytes of data:
Reply from 10.10.10.10: Bytes=32 Time<1ms TTL=128
Ping statistics for 10.10.10.10:
Packets: Sent = 1, Received = 1, Lost = 0 (0% Loss),
Ca. time in millisec:
Minimum = 2ms, Maximum = 2ms, Average = 2ms
However, calling by means of an array index is usually not a good idea because you don't know whether several identically named commands exist, and if they do, in which order the commands were defined. It's better to specify right from the beginning the type you want, which Get-Command always reports in the CommandType column. Name conflicts are out of the question because there can be only one command having a particular name for each type.
The original Ping command is of the Application type. So, if you'd like to invoke this command, instruct Get-Command to retrieve for you the Ping command of the Application type. It shouldn't be important to you at all whether there are any other identically named commands of other types. PowerShell will start the original Ping command in any case:
$command = Get-Command -commandType Application Ping
& $command -n 1 10.10.10.10
Pinging 10.10.10.10 with 32 bytes of data:
Reply from 10.10.10.10: Bytes=32 Time<1ms TTL=128
Ping statistics for 10.10.10.10:
Packets: Sent = 1, Received = 1, Lost = 0 (0% Loss),
Ca. time in millisec:
Minimum = 2ms, Maximum = 2ms, Average = 2ms
& (Get-Command -commandType Application Ping) -n 1 10.10.10.10
Pinging 10.10.10.10 with 32 bytes of data:
Reply from 10.10.10.10: Bytes=32 Time<1ms TTL=128
Ping statistics for 10.10.10.10:
Packets: Sent = 1, Received = 1, Lost = 0 (0% Loss),
Ca. time in millisec:
Minimum = 2ms, Maximum = 2ms, Average = 2ms
You now know how PowerShell finds out which command is supposed to be run and how you can use the call operator to invoke your own commands. However, the call operator does have one nasty limitation: it can never execute more than one single command, nor can it execute any instruction lines, nor commands with arguments. If the call operator is calling the shots behind the scenes, how can it execute the entire instruction lines that you type in the console? To clear that up, you'll need another very important PowerShell basic element - scriptblocks.
Using Scriptblocks
The scriptblock is a special form of command. The scriptblock can contain as much PowerShell code as you like. It is defined by braces. The smallest possible scriptblock is just the minimal amount of PowerShell code in braces. You can use the previously described call operator to execute a scriptblock:
& { "Today's date: " + (get-date) }
Today's date: 10/07/2007 12:32:39
Executing Entire Instruction Lines
Perhaps you're beginning to realize how scriptblocks enable the call operator to execute not just single commands, but entire instruction lines. The call operator normally runs just single commands, but among the permitted commands, according to Table 12.1, are commands of the script type, the scriptblocks. This is the solution to running whole lines of instructions since scriptblocks can consist of any number of commands. In the next example, the call operator runs several statements in the line:
& {Get-Process | Where-Object { $_.Name -like 'a*'}}
This is the way command entry works in the PowerShell console: if you type an instruction line in the console, PowerShell will turn the line into a scriptblock and execute it just the way it did in the previous example. Scriptblocks are the universal basic element of PowerShell. Many PowerShell commands and structures, upon closer examination, are nothing more than scriptblocks. Let's take a look at all the places where scriptblocks are hidden in PowerShell.
Invoke-Expression
You've seen above that the call operator can process whole instruction lines with the help of a scriptblock. Actually, this function corresponds to the Invoke-Expression cmdlet, which is nothing more than a scriptblock that is passed to the call operator:
Invoke-Expression 'Get-Process | Where-Object { $_.Name -like "a*"}'
Handles NPM(K) PM(K) WS(K) VM(M) CPU(s) Id ProcessName
------- ------ ----- ----- ----- ------ -- -----------
36 2 712 48 21 2616 agrsmsvc
311 9 10988 3324 112 464 AppSvc32
105 3 1044 736 37 1228 Ati2evxx
130 5 2056 3916 48 1732 Ati2evxx
79 4 4612 1092 58 2,75 2064 ATSwpNav
99 3 11892 7600 45 1432 audiodg
Just remember to put code after Invoke-Expression in single quotation marks. If you use double quotation marks, PowerShell will replace all the variable names in the string with the variable contents. Because part of the $_ variable in the last example is part of the code to be executed, it would be incorrectly replaced with "null" and generate an error:
Invoke-Expression "Get-Process |
Where-Object { $_.Name -like 'a*'}"
The term ".Name" is not recognized as a cmdlet,
function, operable program, or script file. Verify
the term and try again.
At line:1 Char:35
+ Get-Process | Where-Object { .Name <<<< -like 'a*'}
The following statement is completely identical:
& {Get-Process | Where-Object { $_.Name -like 'a*'}}
Pipeline: ForEach-Object
In Chapter 5, you used the ForEach-Object cmdlet in the pipeline, which loops over every pipeline object one by one. PowerShell code follows ForEach-Object in braces, so it is actually a scriptblock. The following scriptblock was executed for every object in the pipeline:
Get-Process | ForEach-Object { $_.name }
Loops: If and For
Do you remember Chapter 7? You worked with conditions, which also use scriptblocks, as they do in this example:
$age = 21
If ($age -lt 21)
{
"You're too young."
}
Else
{
"You may drink a wine."
}
The If statement uses two scriptblocks. The first is executed if the condition after If is met; the second, if it is not met. You saw much the same thing for the loops in Chapter 8:
For ($x=1; $x -le 10; $x++)
{
$x
}
Again, here's a scriptblock in braces that iterates until the termination condition of the loop is met.
Functions Are Named "Scriptblocks"
A new light is shed on the functions from Chapter 9, because functions and scriptblocks are basically identical. Functions are nothing more than named scriptblocks that you can call directly through a set name. Take a look:
function Test { "Hello world!" }
The identifier function sets a name for the scriptblock that follows it in braces. That's why this scriptblock will be run when you specify the assigned name:
Test
Hello world!
You can see that the function actually consists of just one conventional scriptblock when you get the scriptblock of the function:
$scriptblock = $function:Test
$scriptblock
"Hello world!"
$scriptblock.GetType().Name
ScriptBlock
You could reprogram the function by allocating another scriptblock to it:
$function:Test = { "Morning!" }
Test
Morning!
$function:Test = ' "Morning!" '
Test
Morning!
$function:Test = "{ 'Morning!' }"
Test
'Morning!'
Just remember not to use braces in a string. If you do anyway the braces will not delimit the scriptblock but ensure that any special characters in the string are not evaluated as special characters. That's why the Test function in the last example outputs a string along with the quotation marks.
If you like, you could use your newly acquired skills to even create functions entirely without the Function statement:
New-Item function:newFunction -value {"Hello world!"} -force
CommandType Name Definition
----------- ---- ----------
Function newFunction "Hello world!"
newFunction
Hello world!
Building Scriptblocks
Because functions are nothing more than named scriptblocks, which support all the features that distinguish functions. Let's see whether that's really true.
Passing Arguments to Scriptblocks
Parameters may be specified in parentheses after the name of a function so that the user of the function can pass additional arguments to it later. The following simple function example defines a parameter called $text and outputs only what was passed to the function as an argument:
function TextOutput($text)
{
$text
}
TextOutput "Hello"
Hello
How can a scriptblock offer the same functionality? After all, a scriptblock doesn't have a function statement after which you could define a parameter. In reality, every function is only a scriptblock. Have a look here to see how a scriptblock embeds parameters:
$function:TextOutput
param($text) $text
A scriptblock uses the param statement to define a parameter. Think of Chapter 10 and scripts, which do precisely the same thing. So scripts are also nothing more than scriptblocks, although they tend to be very extensive ones. You could easily define your own anonymous scriptblock (i.e., one you don't have to name) that processes arguments. The following scriptblock accepts two parameters and multiplies them:
{ param($value1, $value2) $value1 * $value2 }
To invoke the scriptblock, use the call operator again:
& { param($value1, $value2) $value1 * $value2 } 10 5
50
& { param($value1, $value2) $value1 * $value2 } "Hello" 10
HelloHelloHelloHelloHelloHelloHelloHelloHelloHello
Begin, Process, End Pipeline Blocks
A further characteristic of functions is their ability to define three blocks called begin, process, and end in order to process PowerShell pipeline results in real time. Do you still remember Chapter 9? If a function is used inside a pipeline, it initially runs code in the begin block, then once again in the process block for every pipeline object, and finally in the end block. If the three blocks aren't defined, the function can't process pipeline results in real time, but blocks the pipeline until all results are available.
Scriptblocks can also define these three blocks and be used in the pipeline. In fact, the ForEach-Object cmdlet is basically nothing more than a scriptblock that has in itself a process block:
Get-Process | ForEach-Object { $_.Name }
Get-Process | & { process { $_.Name } }
The Where-Object cmdlet works in a similar way:
Get-Process | Where-Object { $_.Name -like "a*" }
Get-Process | & { process { If ($_.Name -like "a*") { $_ } } }
Validity of Variables
All the variables that are created inside a function are private and valid (only inside the function) unless you expressly specify another validity in the variable name. In the following example, the Test function defines two variables. The variable $value1 is created without any particular validity identifier and consequently is private. This variable is valid only inside the function. On the other hand, the variable $value2, is created with the global: validity identifier and consequently is also valid outside the function:
function Test
{
$value1 = 10
$global:value2 = 20
}
Test
$value1
$value2
20
Let's try the same thing with a scriptblock:
& { $value1 = 10; $global:value2 = 20 }
$value1
$value2
20
As it turns out, scriptblocks determine the validity of variables. All the variables that you define without any particular validity identifier inside a scriptblock are valid only inside the scriptblock. This behavior is not confined to functions but applies in all scriptblocks invoked by using the "&" call operator. In contrast, PowerShell runs scriptblocks executed inside loops or conditions in the current context. That's why the $text variable is valid outside the condition as well:
If ($age -ge 18)
{
$text = "You are of age"
}
Else
{
$text = "You are under age"
}
$text
You are under age
ExecutionContext
PowerShell provides a very special object, the automatic variable $ExecutionContext, which you will rarely need but which will help you better understand PowerShell internal operations. This object offers two main properties:InvokeCommand and SessionState.
InvokeCommand
By now, you should be familiar with three important special characters that PowerShell uses in the console. Double quotation marks define not only a string but also ensure at the same time that variable names in the string are replaced with variable contents. The ampersand, "&", is the call operator and runs commands. Finally, braces create new scriptblocks.
In fact, behind these special characters are internal methods that perform the actual tasks. You can control these methods directly. The automatic variable $ExecutionContext makes these methods accessible through its InvokeCommand property. It is important to know how PowerShell works internally, even though you usually won't need these methods because the special characters are easier to get to.
| Special Character |
Definition |
Internal Method |
| " |
Resolves variables in a string |
ExpandString() |
| & |
Executes commands |
InvokeScript() |
| {} |
Creates a new scriptblock |
NewScriptBlock() |
Table 12.2: Important special characters and the internal methods underlying them
Resolving Variables
Whenever you assign a string in double quotation marks to a variable, PowerShell resolves the variable and replaces it with matching variable contents:
$name = 'Tobias Weltner'
$text = "Your name is $name"
$text
Your name is Tobias Weltner
The method ExpandString() carries out this resolution internally. This means that variables can also be resolved in the following way:
$name = 'Tobias Weltner'
$text = 'Your name is $name'
$text
Your name is $name
$executioncontext.InvokeCommand.ExpandString($text)
Your name is Tobias Weltner
Creating Scriptblocks
If you place PowerShell code in braces, PowerShell will make a scriptblock out of the code. You've seen how you can either use the call operator to immediately execute a scriptblock or to assign it to a function. The method NewScriptBlock() is used to generate new scriptblocks:
$sb = { 4*5 }
$sb.GetType().Name
ScriptBlock
& $sb
20
$sb = $executioncontext.InvokeCommand.NewScriptBlock('4*5')
$sb.GetType().Name
ScriptBlock
& $sb
20
Executing Instruction Lines
Input instruction lines are executed internally by the InvokeScript() method. The following three commands accomplish the same thing:
Invoke-Expression '4*5'
20
& { 4*5 }
20
$executioncontext.InvokeCommand.InvokeScript('4*5')
20
SessionState
SessionState is an object that reflects the current state of your PowerShell environment. You can likewise locate this object in the $ExecutionContext automatic variable:
$executioncontext.SessionState | Format-List *
Drive : System.Management.Automation.DriveManagementIntrinsics
Provider : System.Management.Automation.CmdletProviderManagementIntrinsics
Path : System.Management.Automation.PathIntrinsics
PSVariable : System.Management.Automation.PSVariableIntrinsics
The four properties Drive, Provider, Path and PSVariable, are subobjects that you can use to query the current state of these PowerShell areas as well as to modify them.
Managing Variables
PSVariable will retrieve the value of any variable and can also be used to modify variables:
$value = "Test"
$executioncontext.SessionState.PSVariable.GetValue("value")
Test
$executioncontext.SessionState.PSVariable.Set("value", 100)
$value
100
Managing Drives
Drive manages drives in PowerShell. You could define the current drive in the following way:
$executioncontext.SessionState.Drive.Current
Name Provider Root CurrentLocation
---- -------- ---- ---------------
C FileSystem C:\ Users\Tobias Weltner
GetAll() lists all available drives and as such is equivalent to the Get-PSDrive cmdlet:
$executioncontext.SessionState.Drive.GetAll()
Name Provider Root CurrentLocation
---- -------- ---- ---------------
Alias Alias
Env Environment
C FileSystem C:\ Users\Tobias Weltner
D FileSystem D:\
Function Function
HKLM Registry HKEY_LOCAL_MACHINE
HKCU Registry HKEY_CURRENT_USER
Variable Variable
cert Certificate \
If you are interested only in the drives of a particular provider, such as only in genuine data file drives, use GetAllForProvider() and specify the provider you want:
$executioncontext.SessionState.Drive.GetAllForProvider("FileSystem")
Name Provider Root CurrentLocation
---- -------- ---- ---------------
C FileSystem C:\ Users\Tobias Weltner
D FileSystem
Path Specifications
Path returns several methods that cover all aspects of path names that are usually taken care of by cmdlets (Table 12.3). Moreover, the object offers some additional methods that you can use:
$executioncontext.SessionState.Path.Combine("C:", "test.txt")
C:\test.txt
| Method |
Description |
Cmdlet |
| CurrentLocation |
Current working directory |
Get-Location |
| PopLocation() |
Retrieve stored directory |
Pop-Location |
| PushCurrentLocation() |
Store current working directory |
Push-Location |
| SetLocation() |
Set new directory as current working directory |
Set-Location |
| GetResolvedPSPathFromPSPath() |
Return absolute path name for specified relative path name |
Resolve-Path |
Table 12.3: Path cmdlets and underlying low-level methods of the SessionState object
Summary
Whenever you assign the task of running a command to PowerShell, it relies on command discovery to look up which command is intended. If the command is unclear because several commands have the same name, PowerShell uses a priority list (Table 12.1) and automatically selects a command from it.
You can use the call operator character, "&", to run commands that you do not directly input in the console. The call operator carries out the same command discovery process as the console does for direct command inputs. Alternatively, you can use Get-Command to carry out command discovery and to pass the result directly to the call operator. This allows you to determine which identically named commands should be executed so that the choice is made by you and not by the integrated PowerShell priority list.
Put everything together in a scriptblock if you would like to invoke more than a single command or to pass arguments to a command. Scriptblocks are nothing more than any piece of PowerShell code enclosed in braces. You can run scriptblocks by using the call operator. It is interesting to note that scriptblocks are the foundation of PowerShell. They provide the basis for many cmdlets and are the "soul" of every function and script.
If you'd like to take a look behind the scenes to see how PowerShell actually does run commands or create scriptblocks, you will need the object in the $ExecutionContext automatic variable. It offers you access to many low-level functions, which actually perform the tasks involved when you create scriptblocks or use the call operator (Table 12.2).
Posted
Mar 30 2009, 08:03 AM
by
ps1