I'll take an example of refactoring to make a basic function in a more powershell one.
I have the following function word2pdf that converts a word file to a PDF.
function word2pdf([String]$dir) { $wdFormatPDF = 17 $word = New-Object -ComObject word.application $word.visible = $false foreach($file in $dir) { $doc = $word.documents.open($file.Fullname) $pdfFilename = $file.Fullname + ".pdf" $doc.saveas([ref] $pdfFilename, [ref]$wdFormatPDF) } $doc.close() $word.Quit() }
The function is called like this:
word2pdf "."
This is working, but I'd like to be able to send the list of files in the pipeline instead of providing a whole directory. Here are the changes:
function word2pdf([System.IO.FileInfo]$file) { $wdFormatPDF = 17 $word = New-Object -ComObject word.application $word.visible = $false $doc = $word.documents.open($file.Fullname) $pdfFilename = $file.Fullname + ".pdf" $doc.saveas([ref] $pdfFilename, [ref]$wdFormatPDF) $doc.close() $word.Quit() }
The function is called like this:
dir *.docx | foreach-object { word2pdf $_ }
So far so good. However, as you can see, word2pdf is called through a foreach-object. If you have a look at powershell built-in functions, they directly take the pipeline as their input. This is what we will do.
First, we convert our function to an advanced function. We add CmdletBinding, define parameters with the Param directive
and enclose the source code in the Process section. What does it mean?
CmdletBinding means our function is an advanced function, among other things, it will be able to process the pipeline.
In the Param directive, we list the parameters, in our case we have one mandatory parameter that can be input from the pipeline.
How is the pipeline dealt with in advanced functions? Very easily. For each object incoming in the pipeline, the parameter ($file in our case)
is set to the object and Process section is called.
If the pipeline contains one object, Process section is called once. If it contains 10 objects, it is called 10 times.
You do not have to implement loops by yourself, great isn't it?
function word2pdf() { [CmdletBinding()] Param([Parameter(Mandatory=$True,ValueFromPipeline=$True)]$file) Process { $wdFormatPDF = 17 $word = New-Object -ComObject word.application $word.visible = $false $doc = $word.documents.open($file.Fullname) $pdfFilename = $file.Fullname + ".pdf" $doc.saveas([ref] $pdfFilename, [ref]$wdFormatPDF) $doc.close() $word.Quit() } } dir *.docx | word2pdf
Note: Do not forget the Processs primitive, otherwise the function will only process the first item of the pipeline.
As you may have noticed, this version will instantiate a new word object each time a file is processed. This is less efficient that the original version that was performing only one instantiation.
Advanced functions provide the handy Begin and End sections to deal with this matter. They behave exactly like Junit's setup and teardown methods.
Before the loop, Begin section is executed and all the variables declared in the Begin section will be available in the Process and End sections. Once the loop is finished, End section is executed.
Here is the final implementation:
function word2pdf() { [CmdletBinding()] Param([Parameter(Mandatory=$True,ValueFromPipeline=$True)]$file) Begin { $wdFormatPDF = 17 $word = New-Object -ComObject word.application $word.visible = $false } Process { $doc = $word.documents.open($file.Fullname) $pdfFilename = $file.Fullname + ".pdf" $doc.saveas([ref] $pdfFilename, [ref]$wdFormatPDF) $doc.close() } End { $word.Quit() } } dir *.docx | word2pdf
As you can see, we now have a function that accepts pipeline input and efficiently processes it. The change from the original version implementation to the last one, is straightforward once you have done it a couple of times.
I like the advanced functions for several reasons. First of all, I can use the pipeline easily, no need of a special variable or anything else. Powershell let me use usual variables and will call Process section natively. Moreover, the final source code clearly describes my intent, which is always a good point.
No comments:
Post a Comment