Showing posts with label Dotsource. Show all posts
Showing posts with label Dotsource. Show all posts

Tuesday, July 8, 2008

Don't Believe Everything You Read

I made a boo-boo on my previous post about importing scripts as libraries.  In researching the dotsource operator I picked up some misinformation.  My brain was nagging at me that I might have gotten the wrong information already, but my tests had seemed to be working.

The dotsource operator does not run the script in the global scope.  It creates all variables and functions in the current scope.  If you're running a script at the command-line then this has the effect of running the script in the global scope, but in my example profile I made a mistake.  Using the dotsource operator where I put it ends up creating the functions inside the scope of the scriptblock that it is called in, not the global scope.

As a temporary measure I made the scripts that I want to import create their functions in the global scope, but that's just a temporary condition, dig*?

How did I make that mistake?  I forgot that I had a script in my path with the same name as the function I thought I was importing, making it look like the script had imported the function into the global scope.


* Sorry, every once in a while I channel George Clinton when I type.

Sunday, July 6, 2008

Importing Scripts as Libraries, Part Deux

In this post I showed how using the dotsource operator lets us run a script and import all of its named objects from the script scope into the global scope. The effect is as if instead of calling a script you just typed its contents at the prompt.

This set off a lightbulb in my head, and gave me the solution to something I'd been pondering for a while: how to import scripts as libraries (modules, etc, whatever we're calling them these days). You see, if I come up with a really neat function I don't want to have to cut and paste it into every script that uses it, and if I come up with a lot of content, I'd like to be able to share them with people.

This is what I came up with, and it incorporates a few new tricks I picked up along the way to trying to solve this problem. The desired result: I can type in 'import-script scriptname.ps1' and it is automatically imported without having to know the full path to the file.

Some highlights:
  • Test-Path is a nice cmdlet for checking the existence of a path on any PSDrive, so that means any Enviornment variable, registry key, file object, etc.
  • Exceptions work, but are really hard to get straight. Check out this link for some details.
  • The (, (command)) syntax is how I'm forcing the variable to be an array. I might get rid of it later, I think now that I'm using boolean comparison instead of looking for the length of the array to check if any files were found.
  • In most examples people use Write-Error instead of echo to print error messages, but I wanted to have the error show up clearly without all of the red words and extra garbage.
  • Note that I'm searching everything under the folder pointed to by the PSPATH environment variable. I point mine to C:\PSLibs by default.

function Import-Script ([string]$script) {
  $local:ReportErrorShowSource = 0

  # Clean up the Exception messages a bit
  trap [Exception] {
    $errmsg = "`n`tError importing '$script': "
    $errmsg += ($_.Exception.Message + "`n")
echo $errmsg
break
  }

  # Check PSPATH
  if (-not (Test-Path "Env:pspath")) {
    throw "PSPath environment variable not set."
  }elseif (-not (Test-Path $env:pspath)) {
    throw "PSPATH environment variable points to invalid path." 
  }

  $files = (, (dir $env:pspath $script -Recurse))

  # Make sure we find a single matching file
  if ($files.Length -gt 1) {
    throw ([string]$files.Length + " files of name '$script' found in PSPATH.")
  }elseif (-not $files) {
    throw "No files named '$script' found in PSPATH."
  }

  # Do the needful
  . $files[0].FullName
}

The DotSource Operator

A while back I asked Stephen Ng if he had seen any documentation on importing scripts as modules or libraries, and he hadn't. I sort of filed it away for future reference, knowing that if we went down this path we'd probably want to find some way to keep a repository of PowerShell scripts for common use.

Fast forward to a few days ago, when the latest entry from The PowerShell Guy showed up in my Google Reader. In his Get-IpConfig function (which I'll discuss in a later post) he has a line with an open parenthesis followed by a dot.wasn't the most readable line of code in the world anyway, but as I was mentally parsing it I hit a snag. I had no idea what that dot was doing just after the open parenthesis.

As it turns out, this is what's known as the dotsource operator. The dotsource operator (I wish people would stop and think a bit when choosing these terrible names) is like the ampersand (&) operator, in that it executes the script or code block that comes after it, but it also declares all variables in the global scope. Why would you want to do this?

Let's use a terrible example and say I have a script that will get a list of users and groups on a computer. I have a script, Get-UsersAndGroups.ps1. It creates two variables, $users and $groups, and exits. I want to be able to use those variables, so I run the following:

PS C:\> .\Get-UsersAndGroups.ps1
PS C:\> $users

I get nothing back, even though I know I created the variable. $users was created in the script's scope, though, and once the script finishes running it takes its variables with it.

I have three options here:
  1. Change my script to send the variables to the output pipeline. Maybe this script is being used by other people, though, so I don't necessarily want to change the pipeline.
  2. Explicitly declare the variables as $global:users and $global:groups in the script. The problem with this is that I don't necessarily always want to declare these variables in the global namespace.
  3. Use the dotsource operator.
The way to use the dotsource operator is like this:

PS C:\> . .\Get-UsersAndGroups.ps1
PS C:\> $users
Tim
Administrator
Alfred E. Neuman

(Note that there is a space between the dots there, otherwise I'd just be telling PowerShell to look in the parent directory.)

To wrap this up, we can take all of this newfound knowledge and see how you can create libraries in PowerShell. By dotsourcing (yes, people even use it as a verb, ugh) a script with functions and constants in it, we can import the functions in a way that makes them usable by our scripts, that way we can reuse the code.

The one big problem is that PowerShell has no concept of a /lib directory that it looks for modules in by default, so you must know the path to the script you will be importing. I'll leave that as a future exercise, but I have a spark of an idea in my head of how we can create a function that looks up a script by its name to import and distribute it using a common company Profile so that all company PowerShell Scripters have a common location for libraries, like sitecustomize.py in Python.