While many of your scripts will be designed to work in isolation, you will often find it helpful to give your script information about its execution environment: its name, current working directory, environment variables, common system paths, and more.
PowerShell offers several ways to get at this information—from its cmdlets and built-in variables to features that it offers from the .NET Framework.
To interact with environment variables, access them in
almost the same way that you access regular PowerShell variables.
The only difference is that you place env:
between the dollar sign ($) and the variable
name:
PS > $env:Username Lee
You can modify environment variables this way, too. For example, to temporarily add the current directory to the path:
PS > Invoke-DemonstrationScript The term 'Invoke-DemonstrationScript' is not recognized as the name of a cmdlet, function, script file, or operable program. Check the spelling of the name, or if a path was included, verify that the path is correct and try again. At line:1 char:27 + Invoke-DemonstrationScript <<<< + CategoryInfo : ObjectNotFound: (Invoke-DemonstrationScript :String) [], CommandNotFoundException + FullyQualifiedErrorId : CommandNotFoundException Suggestion [3,General]: The command Invoke-DemonstrationScript was not found, but does exist in the current location. Windows PowerShell doesn't load commands from the current location by default. If you trust this command, instead type ".\Invoke-DemonstrationScript". See "get-help about_Command_ Precedence" for more details. PS > $env:PATH = $env:PATH + ".;" PS > Invoke-DemonstrationScript The script ran!
In batch files, environment variables are the primary way to store temporary information or to transfer information between batch files. PowerShell variables and script parameters are more effective ways to solve those problems, but environment variables continue to provide a useful way to access common system settings, such as the system’s path, temporary directory, domain name, username, and more.
PowerShell surfaces environment variables through its environment provider: a container that lets you work with environment variables much as you would work with items in the filesystem or registry providers. By default, PowerShell defines an env: drive (much like c: or d:) that provides access to this information:
PS > dir env: Name Value ---- ----- Path c:\progra~1\ruby\bin;C:\WINDOWS\system32;C:\ TEMP C:\DOCUME~1\Lee\LOCALS~1\Temp SESSIONNAME Console PATHEXT .COM;.EXE;.BAT;.CMD;.VBS;.VBE;.JS;.JSE;.WSF; (...)
Since it is a regular PowerShell drive, the full way to get the value of an environment variable looks like this:
PS > Get-Content Env:\Username Lee
When it
comes to environment variables, though, that is a syntax you will
almost never need to use, because of PowerShell’s support for the
Get-Content
and Set-Content
variable syntax, which shortens that
to:
PS > $env:Username Lee
This syntax works for all drives but is used most commonly to access environment variables. For more information about this syntax, see Access Information About Your Command’s Invocation.
Some environment
variables actually get their values from a combination of two
places: the machine-wide settings and the current-user settings. If
you want to access environment variable values specifically
configured at the machine or user level, use the [Environment]::GetEnvironmentVariable()
method.
For example, if you’ve defined a tools
directory in your path, you might see:
PS > [Environment]::GetEnvironmentVariable("Path", "User") d:\lee\tools
To set these
machine- or user-specific environment variables permanently, use
the [Environment]::SetEnvironmentVariable()
method:
[Environment]::SetEnvironmentVariable(<name>, <value>, <target>
)
The target
parameter
defines where this variable should be stored: User
for the current user and Machine
for all users on the machine. For example,
to permanently add your tools directory
to your path:
$pathElements = @([Environment]::GetEnvironmentVariable("Path", "User") -split ";") $pathElements += "d:\tools" $newPath = $pathElements -join ";" [Environment]::SetEnvironmentVariable("Path", $newPath, "User")
For more information about modifying the system path, see Modify the User or System Path.
For more information about the Get-Content
and Set-Content
variable syntax, see Variables.
For more information about the environment provider, type Get-Help About_Environment
.
Use the [Environment]::SetEnvironmentVariable()
method to
set the PATH
environment variable.
$scope = "User" $pathElements = @([Environment]::GetEnvironmentVariable("Path", $scope) -split ";") $pathElements += "d:\tools" $newPath = $pathElements -join ";" [Environment]::SetEnvironmentVariable("Path", $newPath, $scope)
In Windows, the PATH
environment variable describes the list of
directories that applications should search when looking for
executable commands. As a convention, items in the path are
separated by the semicolon character.
As mentioned in View and Modify Environment
Variables, environment variables have two scopes: systemwide
variables, and per-user variables. The PATH
variable that you see when you type
$env:PATH
is the result of combining
these two.
When you want to modify
the path, you need to decide if you want the path changes to apply
to all users on the system, or just yourself. If you want the
changes to apply to the entire system, use a scope of Machine
in the example given by the Solution. If
you want it to apply just to your user account, use a scope of
User
.
As mentioned, elements in the path are separated by the
semicolon character. To update the path, the Solution first uses
the -split
operator to create a list
of the individual directories that were separated by semicolons. It
adds a new element to the path, and then uses the -join
operator to recombine the elements with the
semicolon character. This helps prevent doubled-up semicolons,
missing semicolons, or having to worry whether the semicolons go
before the path element or after.
For more information about working with environment variables, see View and Modify Environment Variables.
To access information about how the user invoked your
command, use the $PSScriptRoot
,
$PSCommandPath
, and $my
Invocation
variables:
"Script's path: $PSCommandPath" "Script's location: $PSScriptRoot" "You invoked this script by typing: " + $myInvocation.Line
The $PSScriptRoot
and $PSCommandPath
variables provide quick access to
the information a command most commonly needs about itself: its
full path and location.
In addition, the $myInvocation
variable provides a great deal of information about the current
script, function, or script block—and the context in which it was
invoked:
MyCommand
-
Information about the command (script, function, or script block) itself.
ScriptLineNumber
-
The line number in the script that called this command.
ScriptName
-
In a function or script block, the name of the script that called this command.
Line
-
The verbatim text used in the line of script (or command line) that called this command.
InvocationName
-
The name that the user supplied to invoke this command. This will be different from the information given by
MyCommand
if the user has defined an alias for the command. PipelineLength
-
The number of commands in the pipeline that invoked this command.
PipelinePosition
-
The position of this command in the pipeline that invoked this command.
One important point about working with the $myInvocation
variable is that it changes
depending on the type of command from which you call it. If you
access this information from a function, it provides information
specific to that function—not the script from which it was called.
Since scripts, functions, and script blocks are fairly unique,
information in the $myInvocation.MyCommand
variable changes slightly
between the different command types.
Definition
andPath
-
The full path to the currently running script
Name
-
The name of the currently running script
CommandType
-
Always ExternalScript
Definition
andScriptBlock
-
The source code of the currently running function
Options
-
The options (
None
,ReadOnly
,Constant
,Private
,AllScope
) that apply to the currently running function Name
-
The name of the currently running function
CommandType
-
Always Function
When you’re experimenting with the information available through
the $myInvocation
variable, it is
helpful to see how this information changes between scripts,
functions, and script blocks. For a useful deep dive into the
resources provided by the $myInvocation
variable, review the output of
Example 16-1.
Example 16-1. Get-InvocationInfo.ps1
############################################################################## ## ## Get-InvocationInfo ## ## From Windows PowerShell Cookbook (O'Reilly) ## by Lee Holmes (http://www.leeholmes.com/guide) ## ############################################################################## <# .SYNOPSIS Display the information provided by the $myInvocation variable #> param( ## Switch to no longer recursively call ourselves [switch] $PreventExpansion ) Set-StrictMode -Version 3 ## Define a helper function, so that we can see how $myInvocation changes ## when it is called, and when it is dot-sourced function HelperFunction { " MyInvocation from function:" "-"*50 $myInvocation " Command from function:" "-"*50 $myInvocation.MyCommand } ## Define a script block, so that we can see how $myInvocation changes ## when it is called, and when it is dot-sourced $myScriptBlock = { " MyInvocation from script block:" "-"*50 $myInvocation " Command from script block:" "-"*50 $myInvocation.MyCommand } ## Define a helper alias Set-Alias gii .\Get-InvocationInfo ## Illustrate how $myInvocation.Line returns the entire line that the ## user typed. "You invoked this script by typing: " + $myInvocation.Line ## Show the information that $myInvocation returns from a script "MyInvocation from script:" "-"*50 $myInvocation "Command from script:" "-"*50 $myInvocation.MyCommand ## If we were called with the -PreventExpansion switch, don't go ## any further if($preventExpansion) { return } ## Show the information that $myInvocation returns from a function "Calling HelperFunction" "-"*50 HelperFunction ## Show the information that $myInvocation returns from a dot-sourced ## function "Dot-Sourcing HelperFunction" "-"*50 . HelperFunction ## Show the information that $myInvocation returns from an aliased script "Calling aliased script" "-"*50 gii -PreventExpansion ## Show the information that $myInvocation returns from a script block "Calling script block" "-"*50 & $myScriptBlock ## Show the information that $myInvocation returns from a dot-sourced ## script block "Dot-Sourcing script block" "-"*50 . $myScriptBlock ## Show the information that $myInvocation returns from an aliased script "Calling aliased script" "-"*50 gii -PreventExpansion
For more information about running scripts, see Run Programs, Scripts, and Existing Tools.
To determine the full path and
filename of the currently executing script, use the $PSCommandPath
variable. To determine the text
that the user actually typed to invoke your script (for example, in
a “Usage” message), use the $myInvocation.InvocationName
variable.
Because it is so commonly used,
PowerShell provides access to the script’s full path through the
$PSCommandPath
variable. If you want
to know just the name of the script (rather than its full path),
use the Split-Path
cmdlet:
$scriptName = Split-Path -Leaf $PSCommandPath
However, the $PSCommandPath
variable was introduced in PowerShell version 3. If you need to
access this information in PowerShell version 2, use this
function:
function Get-ScriptName { $myInvocation.ScriptName }
By placing the $myInvocation.ScriptName
statement in a function,
we drastically simplify the logic it takes to determine the name of
the currently running script. If you don’t want to use a function,
you can invoke a script block directly, which also simplifies the
logic required to determine the current script’s name:
$scriptName = & { $myInvocation.ScriptName }
Although this is a fairly complex way to get access to the current script’s name, the alternative is a bit more error-prone. If you are in the body of a script, you can directly get the name of the current script by typing:
$myInvocation.Path
If you are in a function or script block, though, you must use:
$myInvocation.ScriptName
Working with the $myInvocation.InvocationName
variable is sometimes
tricky, as it returns the script name when called directly in the
script, but not when called from a function in that script. If you
need this information from a function, pass it to the function as a
parameter.
For more information about working with the $myInvocation
variable, see Access
Information About Your Command’s Invocation.
To determine the location of the
currently executing script, use the $PSScriptRoot
variable. For example, to load a
data file from the same location as your script:
$dataPath = Join-Path $PSScriptRoot data.clixml
Or to run a command from the same location as your script:
$helperUtility = Join-Path $PSScriptRoot helper.exe & $helperUtility
Because it is so commonly used,
PowerShell provides access to the script’s location through the
$PSScriptRoot
variable. However, this
variable was introduced in PowerShell version 3. If you need to
access this information in PowerShell version 2, use this
function:
function Get-ScriptPath { Split-Path $myInvocation.ScriptName }
Once we know
the full path to a script, the Split-Path
cmdlet makes it easy to determine its
location. Its sibling, the Join-Path
cmdlet, makes it easy to form new paths from their components as
well.
By accessing the $myInvocation.ScriptName
variable in a function,
we drastically simplify the logic it takes to determine the
location of the currently running script. For a discussion about
alternatives to using a function for this purpose, see Find Your Script’s Name.
For more information about working with the $myInvocation
variable, see Access
Information About Your Command’s Invocation.
For more information about the Join-Path
cmdlet, see Safely Build File
Paths Out of Their Components.
You want to know the location of common system paths and special folders, such as My Documents and Program Files.
To determine the location of common
system paths and special folders, use the [Environment]
::GetFolderPath()
method:
PS > [Environment]::GetFolderPath("System") C:\WINDOWS\system32
For paths
not supported by this method (such as All
Users Start Menu), use the WScript.Shell
COM object:
$shell = New-Object -Com WScript.Shell $allStartMenu = $shell.SpecialFolders.Item("AllUsersStartMenu")
The [Environment]::GetFolderPath()
method lets you
access the many common locations used in Windows. To use it,
provide the short name for the location (such as System
or Personal
).
Since you probably don’t have all these short names memorized, one
way to see all these values is to use the [Enum]::GetValues()
method, as shown in
Example 16-2.
Example 16-2. Folders supported by the [Environment]::GetFolderPath() method
PS > [Enum]::GetValues([Environment+SpecialFolder]) Desktop Programs Personal Favorites Startup Recent SendTo StartMenu MyMusic DesktopDirectory MyComputer Templates ApplicationData LocalApplicationData InternetCache Cookies History CommonApplicationData System ProgramFiles MyPictures CommonProgramFiles
Since this is such a common task for all enumerated constants, though, PowerShell actually provides the possible values in the error message if it is unable to convert your input:
PS > [Environment]::GetFolderPath("aouaoue") Cannot convert argument "0", with value: "aouaoue", for "GetFolderPath" to type "System.Environment+SpecialFolder": "Cannot convert value "aouaoue" to type "System.Environment+SpecialFolder" due to invalid enumeration values. Specify one of the following enumeration values and try again. The possible enumeration values are "Desktop, Programs, Personal, MyDocuments, Favorites, Startup, Recent, SendTo, StartMenu, MyMusic, DesktopDirectory, MyComputer, Templates, ApplicationData, LocalApplicationData, InternetCache, Cookies, History, CommonApplicationData, System, ProgramFiles, MyPictures, CommonProgramFiles"." At line:1 char:29 + [Environment]::GetFolderPath( <<<< "aouaoue")
Although this method provides access to the most-used common
system paths, it does not provide access to all of them. For the paths that the
[Environment]::GetFolderPath()
method
does not support, use the WScript.Shell
COM object. The WScript.Shell
COM object supports the following
paths: AllUsersDesktop,
AllUsersStartMenu,
AllUsersPrograms,
AllUsersStartup,
Desktop, Favorites, Fonts, MyDocuments, NetHood, PrintHood, Programs, Recent, SendTo, StartMenu, Startup, and Templates.
It would be nice if you could use either the [Environment]::GetFolderPath()
method or the WScript.Shell
COM object, but each of them
supports a significant number of paths that the other does not, as
Example 16-3 illustrates.
Example 16-3. Differences between folders supported by [Environment]::GetFolderPath() and the WScript.Shell COM object
PS > $shell = New-Object -Com WScript.Shell PS > $shellPaths = $shell.SpecialFolders | Sort-Object PS > PS > $netFolders = [Enum]::GetValues([Environment+SpecialFolder]) PS > $netPaths = $netFolders | Foreach-Object { [Environment]::GetFolderPath($_) } | Sort-Object PS > ## See the shell-only paths PS > Compare-Object $shellPaths $netPaths | Where-Object { $_.SideIndicator -eq "<=" } InputObject SideIndicator ----------- ------------- C:\Documents and Settings\All Users\Desktop <= C:\Documents and Settings\All Users\Start Menu <= C:\Documents and Settings\All Users\Start Menu\Programs <= C:\Documents and Settings\All Users\Start Menu\Programs\... <= C:\Documents and Settings\Lee\NetHood <= C:\Documents and Settings\Lee\PrintHood <= C:\Windows\Fonts <= PS > ## See the .NET-only paths PS > Compare-Object $shellPaths $netPaths | Where-Object { $_.SideIndicator -eq "=>" } InputObject SideIndicator ----------- ------------- => C:\Documents and Settings\All Users\Application Data => C:\Documents and Settings\Lee\Cookies => C:\Documents and Settings\Lee\Local Settings\Application... => C:\Documents and Settings\Lee\Local Settings\History => C:\Documents and Settings\Lee\Local Settings\Temporary I... => C:\Program Files => C:\Program Files\Common Files => C:\WINDOWS\system32 => d:\lee => D:\Lee\My Music => D:\Lee\My Pictures =>
For more information about working with classes from the .NET Framework, see Work with .NET Objects.
To determine the current location, use
the Get-Location
cmdlet:
PS > Get-Location Path ---- C:\temp PS > $currentLocation = (Get-Location).Path PS > $currentLocation C:\temp
In addition, PowerShell
also provides access to the current location through the
$pwd
automatic variable:
PS > $pwd Path ---- C:\temp PS > $currentLocation = $pwd.Path PS > $currentLocation C:\temp
One problem that sometimes impacts scripts that work with the .NET Framework is that PowerShell’s concept of “current location” isn’t always the same as the PowerShell.exe process’s “current directory.” Take, for example:
PS > Get-Location Path ---- C:\temp PS > Get-Process | Export-CliXml processes.xml PS > $reader = New-Object Xml.XmlTextReader processes.xml PS > $reader.BaseURI file:///C:/Documents and Settings/Lee/processes.xml
PowerShell keeps these concepts separate because it supports multiple pipelines of execution. The process-wide current directory affects the entire process, so you would risk corrupting the environment of all background tasks as you navigate around the shell if that changed the process’s current directory.
When you use
filenames in most .NET methods, the best practice is to use fully
qualified pathnames. The Resolve-Path
cmdlet makes this easy:
PS > Get-Location Path ---- C:\temp PS > Get-Process | Export-CliXml processes.xml PS > $reader = New-Object Xml.XmlTextReader (Resolve-Path processes.xml) PS > $reader.BaseURI file:///C:/temp/processes.xml
If you want
to access a path that doesn’t already exist, use the Join-Path
cmdlet in combination with the
Get-Location
cmdlet:
PS > Join-Path (Get-Location) newfile.txt C:\temp\newfile.txt
For more information about the Join-Path
cmdlet, see Safely Build File
Paths Out of Their Components.
To join elements of a path together,
use the Join-Path
cmdlet:
PS > Join-Path (Get-Location) newfile.txt C:\temp\newfile.txt
The usual way to create new paths is by combining strings for each component, placing a path separator between them:
PS > "$(Get-Location)\newfile.txt" C:\temp\newfile.txt
Unfortunately, this approach suffers from a handful of problems:
-
What if the directory returned by
Get-Location
already has a slash at the end? -
What if the path contains forward slashes instead of backslashes?
-
What if we are talking about registry paths instead of filesystem paths?
Fortunately, the Join-Path
cmdlet
resolves these issues and more.
For more information about the Join-Path
cmdlet, type Get-Help Join-Path
.
You want to store information in the PowerShell environment so that other scripts have access to it.
To make a variable available to the
entire PowerShell session, use a $GLOBAL:
prefix when you store information in that
variable:
## Create the web service cache, if it doesn't already exist
if(-not (Test-Path Variable:\Lee.Holmes.WebServiceCache))
{
${GLOBAL:Lee.Holmes.WebServiceCache} = @{}
}
The primary guidance when it comes to storing information in the session’s global environment is to avoid it when possible. Scripts that store information in the global scope are prone to breaking other scripts and prone to being broken by other scripts.
This is a common practice in batch file programming, but script parameters and return values usually provide a much cleaner alternative.
Most scripts that use global variables do that to maintain state between invocations. PowerShell handles this in a much cleaner way through the use of modules. For information about this technique, see Write Commands That Maintain State.
If you do need to write variables to the global scope, make sure that you create them with a name unique enough to prevent collisions with other scripts, as illustrated in the Solution. Good options for naming prefixes are the script name, author’s name, or company name.
For more information about setting variables at the global scope (and others), see Control Access and Scope of Variables and Other Items.
You want information about the current PowerShell version, CLR version, compatible PowerShell versions, and more.
Access the
$PSVersionTable
automatic
variable:
PS > $psVersionTable Name Value ---- ----- PSVersion 3.0 WSManStackVersion 3.0 SerializationVersion 1.1.0.1 CLRVersion 4.0.30319.18010 BuildVersion 6.2.9200.16384 PSCompatibleVersions {1.0, 2.0, 3.0} PSRemotingProtocolVersion 2.2
The $PSVersionTable
automatic variable holds version
information for all of PowerShell’s components: the PowerShell
version, its build information, Common Language Runtime (CLR)
version, and more.
This automatic variable was introduced in version 2 of
PowerShell, so if your script might be launched in PowerShell
version 1, you should use the Test-Path
cmdlet to test for the existence of the
$PSVersionTable
automatic variable if
your script needs to change its behavior:
if(Test-Path variable:\PSVersionTable) { ... }
This technique isn’t completely sufficient for writing scripts that work in all versions of PowerShell, however. If your script uses language features introduced by newer versions of PowerShell (such as new keywords), the script will fail to load in earlier versions.
If the ability to run your script in multiple versions of PowerShell is a strong requirement, the best approach is to simply write a script that works in the oldest version of PowerShell that you need to support. It will automatically work in newer versions.
You have a script that will fail if not run from an administrative session and want to detect this as soon as the script starts.
Use the IsInRole()
method of the System.Security.Principal.WindowsPrincipal
class:
$identity = [System.Security.Principal.WindowsIdentity]::GetCurrent() $principal = [System.Security.Principal.WindowsPrincipal] $identity $role = [System.Security.Principal.WindowsBuiltInRole] "Administrator" if(-not $principal.IsInRole($role)) { throw "This script must be run from an elevated shell." }
Testing for administrative rights, while seemingly simple, is a much trickier task than might be expected.
Before PowerShell, many batch files tried to simply write a file
into the operating system’s installation directory. If that worked,
you’re an administrator so you can clean up and move on. If not,
generate an error. But if you use C:\Windows
as the path, your script will fail when
somebody installs the operating system on a different drive. If
you use the
%SYSTEMROOT%
environment variable, you
still might trigger suspicion from antivirus programs.
As an improvement to that technique, some batch files try to
parse the output of the NET LOCALGROUP
Administrators
command. Unfortunately, this fails on
non-English machines, where the group name might be NET LOCALGROUP Administratoren
. Most importantly,
it detects only if the user is part of the Administrators group,
not if his current shell is elevated and he can act as one.
Given that PowerShell has full access to the .NET Framework, the
command becomes much simpler. The System.Security.Principal.WindowsPrincipal
class
provides a method to let you detect if the current session is
acting in its administrative capacity.
This method isn’t without its faults, though. Most examples that
you’ll find on the Internet are simply wrong. The most common
example of applying this API uses this as the command: $principal.IsInRole("Administrators")
. If you
examine the method definitions, though, you’ll see that the common
example ends up calling the first overload definition that takes a
string:
PS > $principal.IsInRole OverloadDefinitions ------------------- bool IsInRole(string role) bool IsInRole(System.Security.Principal.WindowsBuiltInRole role) bool IsInRole(int rid) bool IsInRole(System.Security.Principal.SecurityIdentifier sid) bool IPrincipal.IsInRole(string role)
If you look up the documentation, this string-based overload
suffers from the same flaw that the NET
LOCALGROUP Administrators
command does: it relies on group
names that change when the operating system language changes.
Fortunately, the API offers an overload that takes a
System.Security.Principal.WindowsBuiltInRole
enumeration, and those values don’t change between languages. This
is the approach that the Solution relies upon.
For more information about dealing with .NET objects, see Work with .NET Objects.