October 2nd, 2008

Hey, Scripting Guy! How Can I Reuse Functions?

 

Hey, Scripting Guy! Question

Hey, Scripting Guy! I have a bunch of functions I wrote in PowerShell that I would like to use in other scripts. I am getting tired of cutting and pasting these things into various scripts over and over again. Surely there has got to be an easier way to do this! Have any ideas?

– JB

SpacerHey, Scripting Guy! Answer

Hi JB,

Of course, I have an idea…and don’t call me Shirley. So you have written a bunch of functions and you would like to reuse them in other scripts. Code reuse is a great idea. It is what I call an environmental approach to scripting (shamelessly stolen from the waste hierarchy): Reduce the amount of work, recycle your code, and reuse your functions. As you mentioned, the easiest way to do this is to simply copy and paste the function from one script into another script. Suppose you have a script that contains code that will perform a conversion from Celsius to Fahrenheit, and you wish to use the algorithm to create another script with different capabilities. You can simply write your script, and copy the code from your other script. If you did, you would come up with something that looks like this script (we’ll call it ConvertToFahrenheit.ps1): ):

Param($Celsius)
Function ConvertToFahrenheit($Celsius)
{
 "$Celsius Celsius equals $((1.8 * $Celsius) + 32) Fahrenheit"
} #end ConvertToFahrenheit
ConvertToFahrenheit($Celsius)

There is nothing really wrong with this script. It does one thing, and it does it pretty well. To use the script, we supply a command-line parameter. The good thing is that we do not need to type the entire parameter name when calling the script. This is seen here:

PS C:\> C:\ScriptingGuys\ConvertToFahrenheit.ps1 -c 24
24 celsius equals 75.2 fahrenheit

So with this bonzer (I am learning Australian while I am down under ) script, why would we ever want to do anything else? Well, suppose you were a Microsoft Scripting Guy and you were hanging out in Australia for a month. You might need to be able to do more metric conversions than simply Celsius to Fahrenheit. You might actually want to write several different scripts that use one or more conversion functions that you might write. Now suppose you misspell the word “Celsius” in your function. If you then decide you wish to correct it, you would need to identify every place where the misspelled word occurs, and then correct it. Or suppose you decide you want temperature to be displayed only to two decimal points. Here again you would need to find all instances of the function and make the change, or else you would end up with many slightly different versions of the functions.

What is the solution? One approach is to place all your functions in a single script. Maybe you make a script such as the one seen here (we’ll call it ConversionFunctions.ps1): ):

Function ConvertToMeters($feet)
{
  "$feet feet equals $($feet*.31) meters"
} #end ConvertToMeters
Function ConvertToFeet($meters)
{
 "$meters meters equals $($meters * 3.28) feet"
} #end ConvertToFeet
Function ConvertToFahrenheit($celsius)
{
 "$celsius celsius equals $((1.8 * $celsius) + 32 ) fahrenheit"
} #end ConvertToFahrenheit
Function ConvertTocelsius($fahrenheit)
{
 "$fahrenheit fahrenheit equals $( (($fahrenheit - 32)/9)*5 ) celsius"
} #end ConvertTocelsius
Function ConvertToMiles($kilometer)
{
  "$kilometer kilometers equals $( ($kilometer *.6211) ) miles"
} #end convertToMiles
Function ConvertToKilometers($miles)
{
  "$miles miles equals $( ($miles * 1.61) ) kilometers"
} #end convertToKilometers

If we need to use one of the conversion functions, we would include it in the script by placing a period in front of the path to the script. When we include the script containing the conversion functions, we now have access to all the functions and can use them directly as if they were in the actual file itself. The ConvertToFahrenheit_Include.ps1 script illustrates this technique. We are still using the command-line parameter $celsius to supply the temperature we wish to convert. We then use the period followed by the path to the script for the include file. Last, we call the function by name and supply it with the value that came into the script via the command line. The revised ConvertToFahrenheit.ps1 script is seen here:

You can see that the script is much cleaner and it is less cluttered. Because of the minimized clutter, the script is easier to read. And because it is easier to read, the script is easier to understand and will therefore be easier to maintain. There are, of course, two downsides to this equation. The first is that the two scripts are now married. A change in one script could affect a change in the other script. More importantly, however, is that both scripts now must travel together as both are now needed to have a single working script. This outside dependency can become rather difficult to troubleshoot if you are not expecting it or have not planned for it.

One way we can make the script easier to troubleshoot is to use the Test-Path cmdlet to determine if the include file is present. If the include file is missing, we can generate a message to that effect. This will alert us to the missing file and simplify the troubleshooting scenario. As a best practice, I always recommend using the Test-Path cmdlet whenever you use the include file scenario. The revised ConvertToFahrenheit_Include2.ps1 script illustrates this technique and is seen here:

Param($Celsius)
$includeFile = "c:\data\scriptingGuys\ConversionFunctions.ps1"
if(!(test-path -path $includeFile))
  {
   "Unable to find $includeFile"
   Exit
  }
. $includeFile
ConvertToFahrenheit($Celsius) 

As you can see, this begins to be a bit ridiculous. We have a nine line script to allow us to use a three line function. You have to make the call if you want to use the include file or not. When writing a more substantial script that uses an included file, the payoff in terms of simplicity and actual code length becomes more evident. In the ConvertUseFunctions.ps1 script, we create a function named ParseAction that evaluates the action and value that is supplied from the command line. It then calls the appropriate function. The ConvertUseFunctions.ps1 script is seen here:

Param($action,$value,[switch]$help)
Function GetHelp()
{
  if($help)
  {
   "choose conversion: M(eters), F(eet) C(elsius),Fa(renheit),Mi(les),K(ilometers) and value"
   " Convert -a M -v 10 converts 10 meters to feet."
  } #end if help
} #end getHelp
Function GetInclude()
{
 $includeFile = "c:\data\scriptingGuys\ConversionFunctions.ps1"
 if(!(test-path -path $includeFile))
   {
    "Unable to find $includeFile"
    Exit
   }
. $includeFile
} #end GetInclude
Function ParseAction()
{ 
 switch ($action)
 {
  "M" { ConvertToFeet($value) }
  "F"  { ConvertToMeters($value) }
  "C" { ConvertToFahrenheit($value) }
  "Fa" { ConvertToCelsius($value) }
  "Mi" { ConvertToKilometers($value) }
  "K"  { ConvertToMiles($value) }
  DEFAULT { "Dude illegal value." ; GetHelp ; exit }
 } #end action
} #end ParseAction
# *** Entry Point ***
If($help) { GetHelp ; exit }
if(!$action) { "Missing action" ; GetHelp ; exit }
GetInclude
ParseAction

One thing to keep in mind is that we needed to make a change to the include file. Because we are loading the functions from within a function, the functions are scoped by default into that function. They are not available from a different function—only from child items. To avoid the inheritance issue, we added a script tag to each function when we created it. This script tag is seen here:

Function Script:ConvertToMeters($feet)
{
  "$feet feet equals $($feet*.31) meters"
} #end ConvertToMeters

JB, I hope this helps to explain how to reuse code in your scripts. And remember that recycling is not just for the curbside.

Ed Wilson and Craig Liebendorfer, Scripting Guys

Author

0 comments

Discussion are closed.