In this case, output ofget-childitem *.txtcontains a property namedFullNameas shown below, which is bound to thepathparameter ofthetouch-filecmdlet: PS C:\user\gxie> get-childitem *.txt
Trang 1}set{number = value;
}}
protected override void ProcessRecord()
{
}
}
Now try running theGet-EvenNumbercmdlet with an odd argument value for theNumberparameter:
PS C:\user\gxie> get-evennumber -number 13
Get-EvenNumber : Cannot validate argument on parameter ’Number’ Not an even num
In cmdlet development, parameters can be defined as any NET types, from simple types such asstring,
intto complicated types such asSystem.Process One common task, however, is to design the cmdlet
so that parameter values can be easily typed in from the command line
For example, assume that you want to develop a cmdletUnite-Rectangle, which calculates the union of
Rectangle rectangle1 = new Rectangle(0,0,0,0);
[Parameter(Mandatory = true, Position = 1)]
public Rectangle Rectangle1
80
Trang 2Rectangle rectangle2 = new Rectangle(0, 0, 0, 0);
[Parameter(Mandatory = true, Position = 2)]
public Rectangle Rectangle2
You can see that both parametersRectangle1andRectangle2have the typeSystem.Drawing
Rectangle To specify rectangle values for these command parameters, you would have to create
rect-angle objects first (using thenew-objectcmdlet) and then pass them to the new cmdlet, as shown in thefollowing example:
PS C:\user\gxie> $r1 = new-object system.drawing.rectangle 1,2,1,1
PS C:\user\gxie> $r2 = new-object system.drawing.rectangle 3,4,1,1
Trang 3Top : 2
Bottom : 5
IsEmpty : False
The first command creates a rectangle object with the left-bottom corner set to (1,2) The second command
creates a rectangle object with the left-bottom corner set to (3,4) Both rectangles have a width and height
of 1 After the rectangles are united, the smallest rectangle that can cover them both has a left-bottom
corner of (1,2), with a width and a height of 3 The math works correctly, but having to create two
rect-angles beforehand is not desirable
It would be nice to allow users to type a list, a string, or a hash table from the command line, which you
would automatically convert into rectangles To achieve this, parameter transformation comes in handy.
Basically, a custom parameter transformation attribute can be defined with logic to convert parameter
values from one format (for example, list) to another format (for example, rectangle) Then the attribute
can be associated with a parameter so that this kind of transformation is done automatically during
if (input is IList){
IList list = input as IList;
if (list.Count == 4){
return new Rectangle((int)list[0], (int)list[1],
(int)list[2], (int)list[3]);
}}return inputData;
}
}
In the preceding example, you can see that this class is derived from the
ArgumentTransformation-Attributeclass The bulk of the work for this class is overriding theTranformmethod to transform
parameter values from one format to another Inside theTranformmethod, transformation is done
82
Trang 4selectively More explicitly, you create a newrectangleobject only ifinputDatais a list of four
inte-gers Otherwise,inputDatawill be passed through as it is This implementation is chosen so that you
don’t mistakenly convertinputDataif it is already aRectangle In addition, passing throughinputData
allows another parameter transformation attribute down the chain to also process the data
Now, use this attribute in theUnite-Rectanglecmdlet:
[Cmdlet("Unite", "Rectangle")]
public class UniteRectangleCommand : PSCmdlet
{
Rectangle rectangle1 = new Rectangle(0,0,0,0);
[Parameter(Mandatory = true, Position = 1)]
Rectangle rectangle2 = new Rectangle(0, 0, 0, 0);
[Parameter(Mandatory = true, Position = 2)]
Trang 5In addition, you can verify that directly passing aRectangleobject into either parameter will continue
In summary, this section described how to declare a parameter to be mandatory and positional, how to
use parameter sets, and how to validate and transform parameter values In next section, you will learn
how to make a parameter take pipeline input values
Processing Pipeline Input
One of most popular PowerShell features is pipelining objects from one command to another command
For a cmdlet to be used in a pipeline, it needs to be able to handle pipeline input and generate pipeline
output In this section, you will learn techniques to handle pipeline input in a cmdlet
PowerShell cmdlets can bind pipeline input to a parameter and access the parameter in the
Process-Record() method of the cmdlet The following example extends thetouch-filecmdlet:
[Cmdlet("Touch", "File", DefaultParameterSetName = "Path")]
public class TouchFileCommand : PSCmdlet
}set{fileInfo = value;
}}
84
Trang 6protected override void ProcessRecord()
Comparing the preceding code with the example from the ‘‘Parameter Validation’’ section, the only
change here is setting theValueFromPipelineparameter totruefor theFileInfoparameter This
informs the PowerShell engine that the parameterFileInfowill bind to pipeline input in case it is not
specified from the command line
Use the following to run this cmdlet:
PS C:\user\gxie> get-childitem *.txt | Touch-File -date 7/1/2007
PS C:\user\gxie> get-childitem *.txt
Directory: Microsoft.PowerShell.Core\FileSystem::C:\user\gxie
In the first command of the preceding example, for each output object fromget-childitem*.txt, the
PowerShell engine will bind theTouch-Filecmdlet’sFileInfoparameter and call itsProcessRecord()
method to update the timestamp of the file
Cmdlets parameter can also bind to a property of a pipeline input object Following is an example that
binds thepathparameter to a property of the pipeline input object:
[Cmdlet("Touch", "File", DefaultParameterSetName = "Path")]
public class TouchFileCommand : PSCmdlet
{
private string path = null;
[Parameter(ParameterSetName = "Path", Mandatory=true, Position=1,
ValueFromPipelineByPropertyName = true)]
[Alias("FullName")]
[ValidateNotNullOrEmpty]
Trang 7public string Path
{
get{return path;
}set{path = value;
}}
private FileInfo fileInfo = null;
[Parameter(ParameterSetName = "FileInfo", Mandatory = true, Position = 1)]
public FileInfo FileInfo
{
get{return fileInfo;
}set{fileInfo = value;
}}
DateTime date = DateTime.Now;
[Parameter]
public DateTime Date
{
get{return date;
}set{date = value;
}}
protected override void ProcessRecord()
{
if (fileInfo != null){
fileInfo.LastWriteTime = date;
}
if (File.Exists(path)){
File.SetLastWriteTime(path, date);
}}
}
86
Trang 8In this example, instead of letting theFileInfoparameter take its value from the pipeline, you set
TakeValueFromPipelineByPropertyNameto betruefor the parameterpath Furthermore, you define
the aliasFullNamefor the parameterpath Now, if the pipeline input object has either apathproperty or
aFullNameproperty, then that property value will be bound to thepathparameter
Run thetouch-filecommand much as you did before:
PS C:\user\gxie> get-childitem *.txt | Touch-File -date 7/1/2007
PS C:\user\gxie> get-childitem *.txt
Directory: Microsoft.PowerShell.Core\FileSystem::C:\user\gxie
You can see that the timestamp of both txtfiles are updated In this case, output ofget-childitem
*.txtcontains a property namedFullName(as shown below), which is bound to thepathparameter ofthetouch-filecmdlet:
PS C:\user\gxie> get-childitem *.txt | get-member -membertype property
TypeName: System.IO.FileInfo
-Attributes Property System.IO.FileAttributes Attributes {get;set;}
CreationTime Property System.DateTime CreationTime {get;set;}
CreationTimeUtc Property System.DateTime CreationTimeUtc {get;set;}
Directory Property System.IO.DirectoryInfo Directory {get;}
DirectoryName Property System.String DirectoryName {get;}
IsReadOnly Property System.Boolean IsReadOnly {get;set;}
LastAccessTime Property System.DateTime LastAccessTime {get;set;}
LastAccessTimeUtc Property System.DateTime LastAccessTimeUtc {get;set;}
LastWriteTime Property System.DateTime LastWriteTime {get;set;}
LastWriteTimeUtc Property System.DateTime LastWriteTimeUtc {get;set;}
Pipeline Parameter Binding
Cmdlet parameters can be bound either to command arguments from the command line or to input
objects from the pipeline Command-line parameter binding happens once for each cmdlet invocation
It is performed before theBeginProcessing()method of the cmdlet implementation class is called
Trang 9Conversely, pipeline parameter binding happens once for each pipeline input object It is performed
before each call to theProcessRecord()method of the cmdlet class
Similar to command-line parameter binding, pipeline parameter binding also needs to pick the parameter
to bind from valid parameter sets It uses the following process to decide which parameter to bind first:
1. Prepare parameter lists:Unbound pipeline parameters from valid parameter sets are
organized into two lists: One list (let’s call itValueFromPipeline) is for pipeline parameters
taking pipeline input (i.e., theValueFromPipelineproperty of the parameter attribute is set
to true); another list (let’s call itValudFromPipelineByPropertyName) is for pipeline
parame-ters taking pipeline input by property name (i.e.,ValueFromPipelineByPropertyNameis set
to true) Pipeline parameters from default parameter sets are put at the beginning of these
two lists so that they are considered for binding first
2. Bind next parameter:The pipeline parameter binder uses four steps to determine which
parameter to bind:
a. Bind parameters from theValueFromPipelinelist with no type conversion In this
step, if one parameter from the list has exactly the same type as pipeline input object,then it will be bound Otherwise, parameter binding goes to the next step
b. Bind parameters from theValueFromPipelineByPropertyNamelist with no type
con-version In this step, if one parameter from the list matches a property’s name of thepipeline input object and the parameter type matches the property type, then thisparameter will be bound Otherwise, parameter binding goes to the next step
c. Bind parameters from theValueFromPipelinelist with type conversion In this step,
if the pipeline input object can be converted into a type of parameter in the list, thatparameter will be bound Otherwise, parameter binding goes to the next step
d. Bind parameters from theValueFromPipelineByPropertyNamelist with no type
con-version In this step, if the name of one property of the pipeline input object matches
a parameter in the list and the property type can be converted to the parameter type,this parameter will be bound
3. Narrow down valid parameter sets:If there is a parameter bound in the preceding steps,
then parameter sets for the parameter bound will be used for narrowing down valid
param-eter sets Then the pipeline binder will recalculate the unbound pipeline paramparam-eter list and
bind the next parameter This process will continue until no parameter can be bound
To illustrate the process of pipeline parameter binding, let’s expand thetouch-filecmdlet to specify
that bothPathandFileInfotake their value from the pipeline:
[Cmdlet("Touch", "File", DefaultParameterSetName = "Path")]
public class TouchFileCommand : PSCmdlet
{
private string path = null;
[Parameter(ParameterSetName = "Path", Mandatory=true, Position=1,
ValueFromPipeline = true, ValueFromPipelineByPropertyName = true)]
[Alias("FullName")]
88
Trang 10private FileInfo fileInfo = null;
[Parameter(ParameterSetName = "FileInfo", Mandatory = true, Position = 1,
Trang 11if (File.Exists(path)){
File.SetLastWriteTime(path, date);
}}
}
Please note that the parameterPathis defined to take the value from either the pipeline object or a
property of the pipeline object In the preparation stage of pipeline parameter binding, the two pipeline
parameter lists can be constructed as follows:
❑ ValueFromPipeline List: Path, FileInfo
❑ ValueFromPipelineByPropertyName List: Path
Now consider the following commands:
PS C:\user\gxie> $a = ’c:\user\gxie\readme.txt’
PS C:\user\gxie> $b = get-childitem readme2.txt
PS C:\user\gxie> $c = add-member -InputObject 0 -MemberType NoteProperty -Name P
ath -Value ’c:\user\gxie\readme3.txt’ -passThru
PS C:\user\gxie> $a,$b,$c | touch-file -date 7/1/2007
PS C:\user\gxie> get-childitem
Directory: Microsoft.PowerShell.Core\FileSystem::C:\user\gxie
The first pipeline object is$a, which is a string Because the type of thePathparameter is a string, it will
be bound to take the value of$aduring the step of binding parameters from theValueFromPipelinelist
with no type conversion With this, the timestamp of filec:\user\gxie\readme.txtwill be updated
The second pipeline object is$b, which is of typeFileInfo This matches the type for parameter
FileInfo,FileInfo, so this parameter will take the value of$bduring the step of binding parameters
from theValueFromPipelinelist with no type conversion With this, the timestamp of filec:\user\gxie\
readme2.txtwill be updated
The third pipeline object is$c, which is a wrapped integer with a property namedPath First, an attempt
to bind parameters from theValueFromPipelinelist will fail because neither parameterPathnor
FileInfois of type integer During the step of binding parameters from the
ValueFromPipelineByProp-ertyNamelist (with no type conversion), thePathparameter will be bound to a value of$c.Pathbecause
of type match and name match With this, the timestamp of filec:\user\gxie\readme3.txtwill also be
updated
90
Trang 12At this point, you know how to make a cmdlet handle command-line input and pipeline input throughcommand parameters In the following sections, we discuss how to show cmdlet execution results to
users This includes cmdlet output and cmdlet execution errors
Generating Pipeline Output
PowerShell cmdlets can write objects to the output pipe by using theWriteObject()method The
fol-lowing example extends theTouch-Filecmdlet to writeFileInfoobjects to the output pipe:
[Cmdlet("Touch", "File", DefaultParameterSetName = "Path")]
public class TouchFileCommand : PSCmdlet
{
private string path = null;
[Parameter(ParameterSetName = "Path", Mandatory=true, Position=1,
ValueFromPipeline = true, ValueFromPipelineByPropertyName = true)]
}set{path = value;
}}
private FileInfo fileInfo = null;
[Parameter(ParameterSetName = "FileInfo", Mandatory = true, Position = 1,
ValueFromPipeline = true)]
public FileInfo FileInfo
{
get{return fileInfo;
}set{fileInfo = value;
}}
DateTime date = DateTime.Now;
[Parameter]
public DateTime Date
Trang 13get{return date;
}set{date = value;
}}
protected override void ProcessRecord()
fileInfo.LastWriteTime = date;
WriteObject(fileInfo);
}}
Repor ting Errors
Cmdlet execution can encounter exceptions from different sources, including the following:
❑ NET common language runtime (or CLR) or PowerShell — for example, the out of memory
exception from CLR or the pipeline stopped exception from PowerShell
❑ Cmdlet logic itself
❑ Components on which the cmdlet depends
Cmdlets normally don’t need to be concerned about exceptions from the CLR or PowerShell These kinds
of exceptions can be better handled by PowerShell For exceptions from the other two sources, it is the
cmdlet’s responsibility to wrap the exceptions into error records and to report them
92
Trang 14There are two kinds of errors in PowerShell:
❑ Non-terminating errors:This kind of error is usually specific to the current pipeline object on
which the cmdlet is operating As a result, the cmdlet can skip the current object and move on toprocess the next object from the pipeline
❑ Terminating errors:This kind of error indicates an issue with the cmdlet that prevents it from
handling any pipeline objects For example, theStart-Servicecmdlet depends on the
ser-vice controller for starting a serser-vice If the serser-vice controller is not running, theStart-Service
cmdlet will not be able to start any service As a result, the whole cmdlet needs to be stopped
For normal shells, error handling focuses on reporting errors PowerShell also allows analyzing and
acting upon the errors Usually, errors during PowerShell command execution are accumulated into anarray Then users can analyze the error, fix the problem, and resend the objects not processed through thepipeline To support this capability, PowerShell provides theErrorRecordandErrorDetailclasses
❑ Target object:This is normally the current pipeline object With this, you can determine which
pipeline objects were not successfully processed and need to be processed again
❑ InvocationInfo:This provides context about this error It includes information such as the cmdlet,the pipeline, and which line of a script file was being executed when the error happened
To create anErrorRecordobject, just fill in information about the exception, error category, error ID, andtarget object, as shown in the following example:
[Cmdlet("Touch", "File", DefaultParameterSetName = "Path")]
public class TouchFileCommand : PSCmdlet
{
private string path = null;
[Parameter(ParameterSetName = "Path", Mandatory = true, Position = 1,
ValueFromPipeline = true, ValueFromPipelineByPropertyName = true)]
}set{path = value;
}}
Trang 15private FileInfo fileInfo = null;
[Parameter(ParameterSetName = "FileInfo", Mandatory = true, Position = 1,
ValueFromPipeline = true)]
public FileInfo FileInfo
{
get{return fileInfo;
}set{fileInfo = value;
}}
DateTime date = DateTime.Now;
[Parameter]
public DateTime Date
{
get{return date;
}set{date = value;
}}
protected override void ProcessRecord()
{
FileInfo myFileInfo = fileInfo;
if (myFileInfo == null && File.Exists(path)){
myFileInfo = new FileInfo(path);
}
if (myFileInfo != null){
try{myFileInfo.LastWriteTime = date;
}catch (UnauthorizedAccessException uae){
ErrorRecord errorRecord = new ErrorRecord(uae,
"UnauthorizedFileAccess",ErrorCategory.PermissionDenied,myFileInfo.FullName);
94
Trang 16There is no need to provideInvocationInfoduringErrorRecordconstruction Information about thatwill be filled in when the error record is reported
Use the following to run this command:
PS C:\user\gxie> get-childitem readme.txt
Directory: Microsoft.PowerShell.Core\FileSystem::C:\user\gxie
PS C:\user\gxie> get-childitem readme.txt | touch-file
Touch-File : Access to the path ’C:\user\gxie\readme2.txt’ is denied
At line:1 char:17
+ dir | touch-file <<<<
This reported error message is constructed from the exception message andInvocationInfo The
optionalInvocationInfoprovides the line, character, and script block shown at the bottom of this
example error message
ErrorDetails
Frequently, cmdlet developers find that error messages from the exception of the error record are too
general to help users understand and troubleshoot the issue To resolve this, error details can be attached
to error records, as shown in the following example:
[Cmdlet("Touch", "File", DefaultParameterSetName = "Path")]
public class TouchFileCommand : PSCmdlet
{
protected override void ProcessRecord()
Trang 17FileInfo myFileInfo = fileInfo;
if (myFileInfo == null && File.Exists(path)){
myFileInfo = new FileInfo(path);
}
if (myFileInfo != null){
try{myFileInfo.LastWriteTime = date;
}catch (UnauthorizedAccessException uae){
ErrorRecord errorRecord = new ErrorRecord(uae,
"UnauthorizedFileAccess",ErrorCategory.PermissionDenied,myFileInfo.FullName);
string detailMessage = String.Format("Not able to touch file
’{0}’ Please check whether it is readonly.",myFileInfo.FullName);
errorRecord.ErrorDetails = new ErrorDetails(detailMessage);
WriteError(errorRecord);
return;
}WriteObject(myFileInfo);
}}
}
There are two ways to construct anErrorDetailsobject The simplest way is to directly construct the
object using a message string A more complicated way is to construct the object based on a resource
string and some placeholder arguments To support internationalization, using a resource string is
recommended
Now if you run the command again, you will see that the message from theErrorDetailsobject is
reported from the console:
PS C:\user\gxie> get-childitem readme.txt | touch-file
Touch-File : Not able to touch file ’C:\user\gxie\readme2.txt’ Please check
whether it is readonly
At line:1 char:17
+ dir | touch-file <<<<
96