PowerShell Ebook
PowerShell Ebook
Starting PowerShell
First Steps with the Console
o Incomplete and Multi-Line Entries
o Important Keyboard Shortcuts
o Deleting Incorrect Entries
o Overtype Mode
o Command History: Reusing Entered Commands
o Automatically Completing Input
o Scrolling Console Contents
o Selecting and Inserting Text
o QuickEdit Mode
o Standard Mode
Customizing the Console
o Opening Console Properties
o Defining Options
o Specifying Fonts and Font Sizes
o Setting Window and Buffer Size
o Selecting Colors
o Directly Assigning Modifications in PowerShell
o Saving Changes
Piping and Routing
o Piping: Outputting Information Page by Page
o Redirecting: Storing Information in Files
Summary
Starting PowerShell
On Windows 7 and Server 2008 R2, Windows PowerShell is installed by default. To use PowerShell on older
systems, you need to download and install it. The update is free. The simplest way to find the appropriate download
is to visit an Internet search engine and search for "KB968930 Windows XP" (replace the operating system with the
one you use). Make sure you pick the correct update. It needs to match your operating system language and
architecture (32-bit vs. 64-bit).
After you installed PowerShell, you'll find PowerShell in the Accessories program group. Open this program group,
click on Windows PowerShell and then launch the PowerShell executable. On 64-bit systems, you will also find a
version marked as (x86) so you can run PowerShell both in the default 64-bit environment and in an extra 32-bit
environment for backwards compatibility.
You can also start PowerShell directly. Just press (Windows)+(R) to open the Run window and then enter
powershell (Enter). If you use PowerShell often, you should open the program folder for Windows PowerShell and
right-click on Windows PowerShell. That will give you several options:
Add to the start menu: On the context menu, click on Pin to Start Menu so that PowerShell will be
displayed directly on your start menu from now on and you won't need to open its program folder first.
Quick Launch toolbar: Click Add to Quick Launch toolbar if you use Windows Vista and would like to
see PowerShell right on the Quick Launch toolbar inside your taskbar. Windows XP lacks this command so
XP users will have to add PowerShell to the Quick Launch toolbar manually.
Jump List: On Windows 7, after launching PowerShell, you can right-click the PowerShell icon in your
taskbar and choose Pin to Taskbar. This will not only keep the PowerShell icon in your taskbar so you can
later easily launch PowerShell. It also gives access to its new "Jump List": right-click the icon (or pull it
upwards with your mouse). The jump list contains a number of useful PowerShell functions: you can
launch PowerShell with full administrator privileges, run the PowerShell ISE, or open the PowerShell help
file. By the way: drag the pinned icon all to the left in your taskbar. Now, pressing WIN+1 will always
launch PowerShell. And here are two more tips: hold SHIFT while clicking the PowerShell icon in your
taskbar will open a new instance, so you can open more than one PowerShell console. Holding
SHIFT+CTRL while clicking the PowerShell icon opens the PowerShell console with full Administrator
privileges (provided User Account Control is enabled on your system).
Keyboard shortcuts: Administrators particularly prefer using a keyboard instead of a mouse. If you select
Properties on the context menu, you can specify a key combination in the hot-key field. Just click on this
field and press the key combination intended to start PowerShell, such as (Alt)+(P). In the properties
window, you also have the option of setting the default window size to start PowerShell in a normal,
minimized, or maximized window.
PowerShell's advantage is its tremendous flexibility since it allows you to control and display nearly all the
information and operations on your computer. The command cls deletes the contents of the console window and the
exit command ends PowerShell.
If you've mistyped something, press (Backspace) to delete the character to the left of the blinking cursor. (Del)
erases the character to the right of the cursor. And you can use (Esc) to delete your entire current line.
The hotkey (Ctrl)+(Home) works more selectively: it deletes all the characters at the current position up to the
beginning of the line. Characters to the right of the current position (if there are any) remain intact. (Ctrl)+(End)
does it the other way around and deletes everything from the current position up to the end of the line. Both
combinations are useful only after you've pressed (Arrow left) to move the cursor to the middle of a line, specifically
when text is both to the left and to the right of the cursor.
Overtype Mode
If you enter new characters and they overwrite existing characters, then you know you are in type-over mode. By
pressing (Insert) you can switch between insert and type-over modes. The default input mode depends on the
console settings you select. You'll learn more about console settings soon.
An especially important key is (Tab). It will save you a great deal of typing (and typing errors). When you press this
key, PowerShell will attempt to complete your input automatically. For example, type:
cd (Tab)
The command cd changes the directory in which you are currently working. Put at least one space behind the
command and then press (Tab). PowerShell suggests a sub-directory. Press (Tab) again to see other suggestions. If
(Tab) doesn't come up with any suggestions, then there probably aren't any sub-directories available.
This feature is called Tab-completion, which works in many places. For example, you just learned how to use the
command Get-Process, which lists all running processes. If you want to know what other commands there are that
begin with "Get-", then type:
Get-(Tab)
Just make sure that there's no space before the cursor when you press (Tab). Keep hitting (Tab) to see all the
commands that begin with "Get-".
A more complete review of the Tab-completion feature is available in Chapter 9.
Tab-completion works really well with long path names that require a lot of typing. For example:
c:\p(Tab)
Every time you press (Tab), PowerShell will prompt you with a new directory or a new file that begins with "c:\p."
So, the more characters you type, the fewer options there will be. In practice, you should type in at least four or five
characters to reduce the number of suggestions.
When the list of suggestions is long, it can take a second or two until PowerShell has compiled all the possible
suggestions and displays the first one.
Wildcards are allowed in path names. For example, if you enter c:\pr*e (Tab) in a typical Windows system,
PowerShell will respond with "c:\Program Files".
PowerShell will automatically put the entire response inside double quotation marks if the response contains
whitespace characters.
QuickEdit Mode
QuickEdit is the default mode for selecting and copying text in PowerShell. Select the text using your mouse and
PowerShell will highlight it. After you've selected the text, press (Enter) or right-click on the marked area. This will
copy the selected text to the clipboard which you can now paste into other applications. To unselect press (Esc).
You can also insert the text in your console at the blinking command line by right-clicking your mouse.
Standard Mode
If QuickEdit is turned off and you are in Standard mode, the simplest way to mark and copy text is to right-click in
the console window. If QuickEdit is turned off, a context menu will open.
Select Mark to mark text and Paste if you want to insert the marked text (or other text contents that you've copied to
the clipboard) in the console.
It's usually more practical to activate QuickEdit mode so that you won't have to use the context menu.
Defining Options
Under the heading Options are four panels of options:
Edit options: You should select the QuickEdit mode as well as the Insert mode. We've already discussed
the advantages of the QuickEdit mode: it makes it much easier to select, copy, and insert text. The Insert
mode makes sure that new characters don't overwrite existing input so new characters will be added without
erasing text you've already typed in when you're editing command lines.
Cursor size: Here is where you specify the size of the blinking cursor.
Display options: Determine whether the console should be displayed as a window or full screen. The
"window" option is best so that you can switch to other windows when you're working. The full screen
display option is not available on all operating systems.
Command history: Here you can choose how many command inputs the console remembers. This
allows you to select a command from the list by pressing (Arrow up) or (F7). The option Discard Old
Duplicates ensures that the list doesn't have any duplicate entries. So, if you enter one command twice, it
will appear only once in the history list.
On the Layout tab, you can specify how large the screen buffer should be, meaning how much information the
console should "remember" and how far back you can scroll with the scroll bars.
You should select a width of at least 120 characters in the window buffer size area with the height should be at least
1,000 lines or larger. This gives you the opportunity to use the scroll bars to scroll the window contents back up so
that you can look at all the results of your previous commands.
Selecting Colors
On the Colors tab, you can select your own colors for four areas:
You have a palette of 16 colors for these four areas. So, if you want to specify a new font color, you should first
select the option Screen Text and click on one of the 16 colors. If you don't like any of the 16 colors, then you can
mix your own special shade of color. Just click on a palette color and choose your desired color value at the upper
right from the primary colors red, green, and blue.
Saving Changes
Once you've successfully specified all your settings in the dialog box, you can close the dialog box. If you're using
Windows Vista or above, all changes will be saved immediately, and when you start PowerShell the next time, your
new settings will already be in effect. You may need Admin rights to save settings if you launched PowerShell with
a link in your start menu that applies for all users.
If you're using Windows XP, you'll see an additional window and a message asking you whether you want to save
changes temporarily (Apply properties to current window only) or permanently (Modify shortcut that started this
window).
.\help.txt (Enter)
Or, to make it even simpler, you can use Tab-completion and hit (Tab) after the file name:
help.txt(Tab)
The file name will automatically be completed with the absolute path name, and then you can open it by pressing
(Enter):
& "C:\Users\UserA\help.txt" (Enter)
You can also append data to an existing file. For example, if you'd like to supplement the help information in the file
with help on native commands, you can attach this information to the existing file with the redirection symbol ">>":
Cmd /c help >> help.txt (Enter)
If you'd like to directly process the result of a command, you won't need traditional redirection at all because
PowerShell can also store the result of any command to a variable:
$result = Ping 10.10.10.10
$result
Reply from 10.10.10.10: bytes=32 time<1ms TTL=128
Reply from 10.10.10.10: bytes=32 time<1ms TTL=128
Reply from 10.10.10.10: bytes=32 time<1ms TTL=128
Reply from 10.10.10.10: bytes=32 time<1ms TTL=128
Ping statistics for 10.10.10.10:
Packets: Sent = 4, Received = 4, Lost = 0 (0% loss),
Approximate round trip times in milli-seconds:
Minimum = 0ms, Maximum = 0ms, Average = 0ms
Variables are universal data storage and variable names always start with a "$". You'll find out more about variables
in Chapter 3.
Summary
PowerShell is part of the operating system starting with Windows 7 and Server 2008 R2. On older operating systems
such as Windows XP or Server 2003, it is an optional component. You will have to download and install PowerShell
before using it.
The current version is 2.0, and the easiest way to find out whether you are using the most current PowerShell
version is to launch the console and check the copyright statement. If it reads "2006", then you are still using the old
and outdated PowerShell 1.0. If it reads "2009", you are using the correct version. There is no reason why you
should continue to use PowerShell 1.0, so if you find it on your system, update to 2.0 as soon as possible. If you
wanted to find out your current PowerShell version programmatically, output the automatic variable $psversiontable
(simply by entering it). It not only tells you the current PowerShell version but also the versions of the core
dependencies. This variable was introduced in PowerShell version 2.0, so on version 1.0 it does not exist.
The PowerShell console resembles the interactive part of PowerShell where you can enter commands and
immediately get back results. The console relies heavily on text input. There are plenty of special keys listed in
Table 1.1.
Key
Meaning
(Alt)+(F7)
(PgUp), (PgDn)
(Enter)
(End)
(Del)
(Esc)
(F2)
(F4)
(F7)
(F8)
(F9)
(Left arrow), (Right
arrow)
(Arrow up), (Arrow
down), (F5), (F8)
(Home)
(Backspace)
(Ctrl)+(C)
(Ctrl)+(End)
(Ctrl)+(Arrow left),
(Ctrl)+(Arrow right)
(Ctrl)+(Home)
(Tab)
Table 1.1: Important keys and their meaning in the PowerShell console
You will find that the keys (Arrow up), which repeats the last command, and (Tab), which completes the current
entry, are particularly useful. By hitting (Enter), you complete an entry and send it to PowerShell. If PowerShell
can't understand a command, an error message appears highlighted in red stating the possible reasons for the error.
Two special commands are cls (deletes the contents of the console) and exit (ends PowerShell).
You can use your mouse to select information in the console and copy it to the Clipboard by pressing (Enter) or by
right-clicking when you have the QuickEdit mode turned on. With QuickEdit mode turned off, you will have to
right-click inside the console and then select Mark in a context menu.
The basic settings of the consoleQuickEdit mode as well as colors, fonts, and font sizescan be customized in
the properties window of the console. This can be accessed by right-clicking the icon to the far left in the title bar of
the console window. In the dialog box, select Properties.
Along with the commands, a number of characters in the console have special meanings and you have already
become acquainted with three of them:
Piping: The vertical bar "|" symbol pipes the results of a command to the next. When you pipe the results
to the command more, the screen output will be paused once the screen is full, and continued when you
press a key.
Redirection: The symbol ">" redirects the results of a command to a file. You can then open and view the
file contents. The symbol ">>" appends information to an existing file.
PowerShell 2.0 also comes with a simple script editing tool called "ISE" (Integrated Script Environment). You find
it in PowerShells jump list (if you are using Windows 7), and you can also launch it directly from PowerShell by
entering ise ENTER. ISE requires .NET Framework 3.5.1. On Windows Server 2008 R2, it is an optional feature
that needs to be enabled first in your system control panel. You can do that from PowerShell as well:
Import-Module ServerManager Add-WindowsFeature ISE -IncludeAll
PowerShell has two faces: interactivity and script automation. In this chapter, you will first learn how to work with
PowerShell interactively. Then, we will take a look at PowerShell scripts.
Topics Covered:
PowerShell as a Calculator
o Calculating with Number Systems and Units
Executing External Commands
o Starting the "Classic" Console
o Discovering Useful Console Commands
o Security Restrictions
o Special Places
Cmdlets: PowerShell Commands
o Using Parameters
o Using Named Parameters
o Switch Parameter
o Positional Parameters
o Common Parameters
Aliases: Shortcuts for Commands
o Resolving Aliases
o Creating Your Own Aliases
o Removing or Permanently Keeping an Alias
o Overwriting and Deleting Aliases
Functions: PowerShell-"Macros"
o Calling Commands with Arguments
Invoking Files and Scripts
o Starting Scripts
Running Batch Files
Running VBScript Files
Running PowerShell Scripts
Summary
PowerShell as a Calculator
You can use the PowerShell console to execute arithmetic operations the same way you use a calculator. Just enter a
math expression and PowerShell will give you the result:
2+4 (Enter)
6
You can use all of the usual basic arithmetic operations. Even parentheses will work the same as when you use your
pocket calculator:
1mb (Enter)
1048576
These units can be in upper or lower case PowerShell does not care. However, whitespace characters do matter
because they are always token delimiters. The units must directly follow the number and must not be separated from
it by a space. Otherwise, PowerShell will interpret the unit as a new command and will get confused because there is
no such command.
Take a look at the following command line:
12 + 0xAF (Enter)
187
PowerShell can easily understand hexadecimal values: simply prefix the number with "0x":
0xAFFE (Enter)
45054
Here is a quick summary:
Operators: Arithmetic problems can be solved with the help of operators. Operators evaluate the two
values to the left and the right. For basic operations, a total of five operators are available, which are also
called "arithmetic operators" (Table2.1).
Brackets: Brackets group statements and ensure that expressions in parentheses are evaluated first.
Decimal point: Fractions use a point as a decimal separator (never a comma).
Comma: Commas create arrays and are irrelevant for normal arithmetic operations.
Special conversions: Hexadecimal numbers are designated by the prefix "0x", which ensures that they are
automatically converted into decimal values. If you add one of the KB, MB, GB, TB, or PB units to a
number, the number will be multiplied by the unit. Whitespace characters aren't allowed between numbers
and values.
Results and formats: Numeric results are always returned as decimal values. You can use a format
operator like -f if you'd like to see the results presented in a different way. This will be discussed in detail
later in this book.
Operator Description
+
Adds two values
example
5 + 4.5
2gb + 120mb
0x100 + 5
"Hello " + "there"
5 - 4.5
12gb - 4.5gb
200 - 0xAB
5 * 4.5
4mb * 3
12 * 0xC0
"x" * 5
5 / 4.5
1mb / 30kb
0xFFAB / 0xC
result
9.5
2273312768
261
"Hello there"
0.5
8053063680
29
22.5
12582912
2304
"xxxxx"
1.11111111111111
34.1333333333333
5454,25
0.5
Security Restrictions
Special Places
You won't need to provide the path name or append the file extension to the command name if the program is
located in a folder that is listed in the PATH environment variable. That's why common programs, such as regedit,
notepad, powershell,or ipconfig work as-is and do not require you to type in the complete path name or a relative
path.
You can put all your important programs in one of the folders listed in the environment variable Path. You can find
this list by entering:
$env:Path
C:\Windows\system32;C:\Windows;C:\Windows\System32\Wbem;C:\program
Files\Softex\OmniPass;C:\Windows\System32\WindowsPowerShell\v1.0\;c
:\program Files\Microsoft SQL Server\90\Tools\binn\;C:\program File
s\ATI Technologies\ATI.ACE\Core-Static;C:\program Files\MakeMsi\;C:
\program Files\QuickTime\QTSystem\
You'll find more on variables, as well as special environment variables, in the next chapter.
As a clever alternative, you can add other folders containing important programs to your Path environment
variables, such as:
$env:path += ";C:\programs\Windows NT\accessories"
wordpad.exe
After this change, you can launch WordPad just by entering its program name. Note that your change to the
environment variable Path is valid only in the current PowerShell session. If you'd like to permanently extend Path,
you will need to update the path environment variable in one of your profile scripts. Profile scripts start
automatically when PowerShell starts and customize your PowerShell environment. Read more about profile scripts
in Chapter 10.
Watch out for whitespace characters: If whitespace characters occur in path names, you can enclose the
entire path in quotes so that PowerShell doesn't interpret whitespace characters as separators. Stick to single
quotes because PowerShell "resolves" text in double quotation marks, replacing variables with their values,
and unless that is what you want you can avoid it by using single quotes by default.
Specifying a path: You must tell the console where it is if the program is located somewhere else. To do
so, specify the absolute or relative path name of the program.
The "&" changes string into commands: PowerShell doesn't treat text in quotes as a command. Prefix a
string with "&" to actually execute it. The "&" symbol will allow you to execute any string just as if you
had entered the text directly on the command line.
& ("note" + "pad")
If you have to enter a very long path names, remember (Tab), the key for automatic completion:
C:\(Tab)
Press (Tab) again and again until the suggested sub-directory is the one you are looking for. Add a "\" and press
(Tab) once again to specify the next sub-directory.
The moment a whitespace character turns up in a path, the tab-completion quotes the path and inserts an "&" before
it.
PowerShells internal commands are called cmdlets. The mother of all cmdlets is called Get-Command:
Get-Command -commandType cmdlet
It retrieves a list of all available cmdlets, whose names always consist of an action (verb) and something that is acted
on (noun). This naming convention will help you to find the right command. Let's take a look at how the system
works.
If you're looking for a command for a certain task, you can first select the verb that best describes the task. There are
relatively few verbs that the strict PowerShell naming conditions permit (Table 2.2). If you know that you want to
obtain something, the proper verb is "get." That already gives you the first part of the command name, and now all
you have to do is to take a look at a list of commands that are likely candidates:
Get-Command -verb get
CommandType Name Definition
----------- ---- ---------Cmdlet Get-Acl Get-Acl [[-Path] <String[]>]...
Cmdlet Get-Alias Get-Alias [[-Name] <String[]...
Cmdlet Get-AuthenticodeSignature Get-AuthenticodeSignature [-...
Cmdlet Get-ChildItem Get-ChildItem [[-Path] <Stri...
Cmdlet Get-Command Get-Command [[-ArgumentList]...
Cmdlet Get-ComputerRestorePoint Get-ComputerRestorePoint [[-...
Cmdlet Get-Content Get-Content [-Path] <String[...
Cmdlet Get-Counter Get-Counter [[-Counter] <Str...
Cmdlet Get-Credential Get-Credential [-Credential]...
Cmdlet Get-Culture Get-Culture [-Verbose] [-Deb...
Cmdlet Get-Date Get-Date [[-Date] <DateTime>...
Cmdlet Get-Event Get-Event [[-SourceIdentifie...
Cmdlet Get-EventLog Get-EventLog [-LogName] <Str...
Cmdlet Get-EventSubscriber Get-EventSubscriber [[-Sourc...
Cmdlet Get-ExecutionPolicy Get-ExecutionPolicy [[-Scope...
Cmdlet Get-FormatData Get-FormatData [[-TypeName] ...
Cmdlet Get-Help Get-Help [[-Name] <String>] ...
Cmdlet Get-History Get-History [[-Id] <Int64[]>...
Cmdlet Get-Host Get-Host [-Verbose] [-Debug]...
Cmdlet Get-HotFix Get-HotFix [[-Id] <String[]>...
Cmdlet Get-Item Get-Item [-Path] <String[]> ...
Cmdlet Get-ItemProperty Get-ItemProperty [-Path] <St...
Cmdlet Get-Job Get-Job [[-Id] <Int32[]>] [-...
Cmdlet Get-Location Get-Location [-PSProvider <S...
Cmdlet Get-Member Get-Member [[-Name] <String[...
Cmdlet Get-Module Get-Module [[-Name] <String[...
Cmdlet Get-PfxCertificate Get-PfxCertificate [-FilePat...
Cmdlet Get-Process Get-Process [[-Name] <String...
Cmdlet Get-PSBreakpoint Get-PSBreakpoint [[-Script] ...
Cmdlet Get-PSCallStack Get-PSCallStack [-Verbose] [...
Cmdlet Get-PSDrive Get-PSDrive [[-Name] <String...
Cmdlet Get-PSProvider Get-PSProvider [[-PSProvider...
Cmdlet Get-PSSession Get-PSSession [[-ComputerNam...
Cmdlet Get-PSSessionConfiguration Get-PSSessionConfiguration [...
Cmdlet Get-PSSnapin Get-PSSnapin [[-Name] <Strin...
Cmdlet Get-Random Get-Random [[-Maximum] <Obje...
Cmdlet Get-Service Get-Service [[-Name] <String...
Cmdlet Get-TraceSource Get-TraceSource [[-Name] <St...
Cmdlet Get-Transaction Get-Transaction [-Verbose] [...
Using Parameters
Parameters add information so a cmdlet knows what to do. Once again, Get-Help will show you which parameters
are supported by any given cmdlet. For example, the cmdlet Get-ChildItem lists the contents of the current subdirectory. The contents of the current folder will be listed if you enter the cmdlet without additional parameters:
Get-ChildItem
For example, if you'd prefer to get a list of the contents of another sub-directory, you can enter the sub-directory
name after the cmdlet:
Get-ChildItem c:\windows
You can use Get-Help to output full help on Get-ChildItem to find out which parameters are supported:
Get-Help Get-ChildItem -Full
This will give you comprehensive information as well as several examples. Of particular interest is the "Parameters"
section that you can also retrieve specifically for one or all parameters:
Get-Help Get-ChildItem -Parameter *
-Exclude <string[]>
Omits the specified items. The value of this parameter qualifies the Path parameter. Enter a path element or pattern,
such as "*.txt". Wildcards are permitted.
Required?
Position?
Default value
Accept pipeline input?
Accept wildcard characters?
false
named
false
false
-Filter <string[]>
Specifies a filter in the provider's format or language. The value of this parameter qualifies the Path parameter. The
syntax of the filter, including the use of wildcards, depends on the provider. Filters are more efficient than other
parameters, because the provider applies them when retrieving the objects, rather than having Windows PowerShell
filter the objects after they are retrieved.
Required?
Position?
Default value
Accept pipeline input?
Accept wildcard characters?
false
2
false
false
-Force <string[]>
Allows the cmdlet to get items that cannot otherwise not be accessed by the user, such as hidden or system files.
Implementation varies from provider to provider. For more information, see about_Providers. Even using the Force
parameter, the cmdlet cannot override security restrictions.
Required?
Position?
Default value
Accept pipeline input?
Accept wildcard characters?
false
named
false
false
-Include <string[]>
Retrieves only the specified items. The value of this parameter qualifies the Path parameter. Enter a path element or
pattern, such as "*.txt". Wildcards are permitted.
The Include parameter is effective only when the command includes the Recurse parameter or the path leads to the
contents of a directory, such as C:\Windows\*, where the wildcard character specifies the contents of the
C:\Windows directory.
Required?
Position?
Default value
false
named
true
1
true (ByPropertyName)
false
-Name <string[]>
Retrieves only the names of the items in the locations. If you pipe the output of this command to another command,
only the item names are sent.
Required?
Position?
Default value
Accept pipeline input?
Accept wildcard characters?
false
1
false
false
(...)
At line:1 char:14
+ Get-ChildItem <<<< -pa c:\windows -f *.exe -r -n
You can also turn off parameter recognition. This is necessary only rarely when the argument reads like a parameter
name
Write-Host -BackgroundColor
Write-Host : Missing an argument for parameter
'BackgroundColor'. Specify a parameter of type
"System.consoleColor" and try again.
At line:1 char:27
+ Write-Host -BackgroundColor <<<<
You can always quote the text. Or you can expressly turn off parameter recognition by typing "--". Everything
following these two symbols will no longer be recognized as a parameter:
Write-Host "-BackgroundColor"
-BackgroundColor
Write-Host -- -BackgroundColor
-BackgroundColor
Switch Parameter
Sometimes, parameters really are no key-value pairs but simple yes/no-switches. If they're specified, they turn on a
certain functionality. If they're left out, they don't turn on the function. For example, the parameter -recurse ensures
that Get-ChildItem searches not only the -path specified sub-directories, but all sub-directories. And the switch
parameter -name makes Get-ChildItem output only the names of files (as string rather than rich file and folder
objects).
The help on Get-ChildItem will clearly identify switch parameters and place a "<SwitchParameter>" after the
parameter name:
Get-Help Get-Childitem -parameter recurse
-recurse <SwitchParameter>
Gets the items in the specified locations and all child
items of the locations.
(...)
Positional Parameters
For some often-used parameters, PowerShell assigns a "position." This enables you to omit the parameter name
altogether and simply specify the arguments. With positional parameters, your arguments need to be submitted in
just the right order according to their position numbers.
That's why you could have expressed the command we just discussed in one of the following ways:
Get-ChildItem c:\windows *.exe -recurse -name
Get-ChildItem -recurse -name c:\windows *.exe
Get-ChildItem -name c:\windows *.exe -recurse
In all three cases, PowerShell will identify and eliminate the named arguments -recurse and -name first because they
are clearly specified. The remaining arguments are "unnamed" and need to be assigned based on their position:
Common Parameters
Cmdlets also support a set of generic "CommonParameters":
<CommonParameters>
This cmdlet supports the common parameters: -Verbose,
-Debug, -ErrorAction, -ErrorVariable, and -OutVariable.
For more information, type "get-help about_commonparameters".
These parameters are called "common" because they are permitted for (nearly) all cmdlets and behave the same way.
Common
Parameter
Type Description
-Verbose
Switch
-Debug
-ErrorAction
Generates as much information as possible. Without this switch, the cmdlet restricts itself
to displaying only essential information
Outputs additional warnings and error messages that help programmers find the causes of
Switch
errors. You can find more information in Chapter 11.
Determines how the cmdlet responds when an error occurs. Permitted values:
NotifyContinue: Reports error and continues (default)
NotifyStop: Reports error and stops
Value SilentContinue: Displays no error message, continues
SilentStop: Displays no error message, stops
Inquire: Asks how to proceed
You can find more information in Chapter 11.
Name of a variable in which in the event of an error information about the error is stored.
You can find more information in Chapter 11.
Name of a variable in which the result of a cmdlet is to be stored. This parameter is
usually superfluous because you can directly assign the value to a variable. The difference
is that it will no longer be displayed in the console if you assign the result to a variable.
Value $result = Get-ChildItem
It will be output to the console and stored in a variable if you assign the result additionally
to a variable:
Get-ChildItem -OutVariable result
-ErrorVariable Value
-OutVariable
Historical: NFind and use important cmdlets by using familiar command names you know from older
shells
Speed: Fast access to cmdlets using short alias names instead of longer formal cmdlet names
Resolving Aliases
Use these lines if you'd like to know what "genuine" command is hidden in an alias:
$alias:Dir
Get-ChildItem
$alias:ls
Get-ChildItem
Get-Command Dir
Get-Command Dir
CommandType Name Definition
----------- ---- ---------Alias dir Get-ChildItem
$alias:Dir lists the element Dir of the drive alias:. That may seem somewhat surprising because there is no drive
called alias: in the classic console. PowerShell supports many additional virtual drives, and alias: is only one of
them. If you want to know more, the cmdlet Get-PSDrive lists them all. You can also list alias: like any other drive
with Dir. The result would be a list of aliases in their entirety:
Dir alias:
CommandType Name Definition
----------- ---- ---------alias ac Add-Content
alias asnp Add-PSSnapin
alias clc Clear-Content
(...)
Get-Command can also resolve aliases. Whenever you want to know more about a particular command, you can
submit it to Get-Command, and it will tell you the command type and where it is located.
You can also get the list of aliases using the cmdlet Get-Alias. You will receive a list of individual alias definitions:
Get-alias -name Dir
Get-ChildItem
You can use the parameter -Definition to list all aliases for a given cmdlet.
Get-alias -definition Get-ChildItem
This will get you all aliases pointing to the cmdlet or command you submitted to -Definition.
As it turns out, there's even a third alias for Get-ChildItem called "gci". There are more approaches to the same
result. The next examples find alias definitions by doing a keyword search and by grouping:
Dir alias: | Out-String -Stream | Select-String "Get-ChildItem"
Count Name Group
----- ---- ----1 Add-Content {ac}
1 Add-PSSnapin {asnp}
1 Clear-Content {clc}
1 Clear-Item {cli}
1 Clear-ItemProperty {clp}
1 Clear-Variable {clv}
3 Copy-Item {cpi, cp, copy}
1 Copy-ItemProperty {cpp}
1 Convert-Path {cvpa}
1 Compare-Object {diff}
1 Export-Alias {epal}
1 Export-Csv {epcsv}
1 Format-Custom {fc}
1 Format-List {fl}
2 ForEach-Object {foreach, %}
1 Format-Table {ft}
1 Format-Wide {fw}
1 Get-Alias {gal}
3 Get-Content {gc, cat, type}
3 Get-ChildItem {gci, ls, Dir}
1 Get-Command {gcm}
1 Get-PSDrive {gdr}
3 Get-History {ghy, h, history}
1 Get-Item {gi}
2 Get-Location {gl, pwd}
1 Get-Member {gm}
1 Get-ItemProperty {gp}
2 Get-Process {gps, ps}
1 Group-Object {group}
1 Get-Service {gsv}
1 Get-PSSnapin {gsnp}
1 Get-Unique {gu}
1 Get-Variable {gv}
1 Get-WmiObject {gwmi}
1 Invoke-Expression {iex}
2 Invoke-History {ihy, r}
1 Invoke-Item {ii}
1 Import-Alias {ipal}
1 Import-Csv {ipcsv}
3 Move-Item {mi, mv, move}
1 Move-ItemProperty {mp}
1 New-Alias {nal}
2 New-PSDrive {ndr, mount}
1 New-Item {ni}
1 New-Variable {nv}
1 Out-Host {oh}
1 Remove-PSDrive {rdr}
6 Remove-Item {ri, rm, rmdir, del...}
2 Rename-Item {rni, ren}
1 Rename-ItemProperty {rnp}
1 Remove-ItemProperty {rp}
1 Remove-PSSnapin {rsnp}
1 Remove-Variable {rv}
1 Resolve-Path {rvpa}
1 Set-Alias {sal}
1 Start-Service {sasv}
1 Set-Content {sc}
1 Select-Object {select}
1 Set-Item {si}
3 Set-Location {sl, cd, chdir}
1 Start-Sleep {sleep}
1 Sort-Object {sort}
1 Set-ItemProperty {sp}
2 Stop-Process {spps, kill}
1 Stop-Service {spsv}
2 Set-Variable {sv, set}
1 Tee-Object {tee}
2 Where-Object {where, ?}
2 Write-Output {write, echo}
2 Clear-Host {clear, cls}
1 Out-Printer {lp}
1 Pop-Location {popd}
1 Push-Location {pushd}
Set-Alias adds additional alias definitions. You can actually override commands with aliases since aliases have
precedence over other commands. Take a look at the next example:
Edit
Set-Alias edit notepad.exe
Edit
Edit typically launches the console-based Editor program. You can press (Alt)+(F) and then (X) to exit without
completely closing the console window.
If you create a new alias called "Edit" and set it to "notepad.exe", the command Edit will be re-programmed. The
next time you enter it, PowerShell will no longer run the old Editor program, but the Notepad.
$alias:edit
Manually each time: Set your aliases after every start manually using Set-Alias. That is, of course, rather
theoretical.
Automated in a profile: Let your alias be set automatically when PowerShell starts: add your aliases to a
start profile. You'll learn how to do this in Chapter 10.
Import and export: You can use the built-in import and export function for aliases.
For example, if you'd like to export all currently defined aliases as a list to a file, enter:
Export-Alias
Because you haven't entered any file names after Export-Alias, the command will ask you what the name are under
which you want to save the list. Type in:
alias1 (Enter)
The list will be saved. You can look at the list afterwards and manipulate it. For example, you might want the list to
include a few of your own alias definitions:
Notepad alias1
You can import the list to activate the alias definitions:
Import-Alias alias1
Import-Alias : Alias not allowed because an alias with the
name "ac" already exists.
At line:1 char:13
+ Import-Alias <<<< alias1
Import-Alias will notify you that it cannot create some aliases of the list because these aliases already exist. Specify
additionally the option -Force to ensure that Import-Alias overwrites existing aliases:
Functions: PowerShell-"Macros"
Aliases are simple shortcuts to call commands with another name (shortcut names), or to make the transition to
PowerShell easier (historic aliases). However, the arguments of a command can never be included in an alias. You
will need to use functions if you want that.
Set-Alias qp quickping
qp 10.10.10.10
Pinging 10.10.10.10 with 32 bytes of data:
Reply from 10.10.10.10: bytes=32 time<1ms TTL=128
Ping statistics for 10.10.10.10:
Packets: Sent = 1, Received = 1, Lost = 0 (0% loss),
Approximate round trip times in milli-seconds:
Minimum = 0ms, Maximum = 0ms, Average = 0ms
Unlike alias definitions, functions can run arbitrary code that is placed in brackets. Any additional information a user
submitted to the function can be found in $args if you don't specify explicit parameters. $args is an array and holds
every piece of extra information submitted by the caller as separate array elements. You'll read more about functions
later.
Starting Scripts
Scripts and batch files are pseudo-executables. The script itself is just a plain text file, but it can be run by its
associated script interpreter.
pause
Dir %windir%\system
Save the text and close Notepad. Your batch file is ready for action. Try to launch the batch file by entering its
name:
Ping
The batch file won't run. Because it has the same name and you didn't specify any IP address or Web site address,
the ping command spits out its internal help message. If you want to launch your batch file, you're going to have to
specify either the relative or absolute path name.
.\ping
Your batch file will open and then immediately runs the commands it contains.
PowerShell has just defended a common attack. If you were using the classic console, you would have been tricked
by the attacker. Switch over to the classic console to see for yourself:
Cmd
Ping 10.10.10.10
An attacker can do dangerous things here
Press any key . . .
If an attacker had smuggled a batch file named "ping.bat" into your current folder, then the ping command, harmless
though it might seem, could have had catastrophic consequences. A classic console doesn't distinguish between files
and commands. It will look first in the current folder, find the batch file, and execute it immediately. Such a mix-up
will never happen in the PowerShell console. So, return to your much-safer PowerShell environment:
Exit
MachinePolicy Undefined
UserPolicy
Undefined
Process
Undefined
CurrentUser
RemoteSigned
LocalMachine Restricted
You now see all execution policies. The first two are defined by Group Policy so a corporation can centrally control
execution policy. The scope "Process" refers to your current session only. So, you can use this scope if you want to
only temporarily change the execution policy. No other PowerShell session will be affected by your change.
"CurrentUser" will affect only you, but no other users. That's how you can change this scope without special
privileges. "LocalMachine," which is the only scope available in PowerShell v.1, will affect any user on your
machine. This is the perfect place for companies to set initial defaults that can be overridden. The default setting for
this scope is "Restricted."
The effective execution policy is the first policy from top to bottom in this list that is not set to "Undefined." If all
policies are set to "Undefined," then scripts are prohibited.
Note: To turn off signature checking altogether, you can set the execution policy to "Bypass." This can be useful if
you must run scripts regularly that are stored on file servers outside your domain. Otherwise, you may get security
warnings and confirmation dialogs. Always remember: execution policy exists to help and protect you from
potentially malicious scripts. If you are confident you can safely identify malicious scripts, then nothing is wrong by
turning off signature checking. However, we recommend not using the "Bypass" setting if you are new to
PowerShell.
Summary
The PowerShell console can run all kinds of commands interactively. You simply enter a command and the console
will return the results.
Cmdlets are PowerShell's own internal commands. A cmdlet name is always composed of a verb (what it does) and
a noun (where it acts upon).
To find a particular command, you can either guess or use Get-Command. For example, this will get you a list if you
wanted to find all cmdlets dealing with event logs:
Get-Command -Noun EventLog
Search for the verb "Stop" to find all cmdlets that stop something:
Get-Command -Verb Stop
You can also use wildcards. This will list all cmdlets with the keyword "computer":
Get-Command *computer* -commandType cmdlet
Once you know the name of a particular cmdlet, you can use Get-Help to get more information. This function will
help you view help information page by page:
Get-Help Stop-Computer
Help Stop-Computer -examples
Help Stop-Computer -parameter *
Cmdlets are just one of six command types you can use to get work done:
If commands are ambiguous, PowerShell will stick to the order of that list. So, since the command type "Alias" is at
the top of that list, if you define an alias like "ping", it will be used instead of ping.exe and thus can override any
other command type.
It is time to combine commands whenever a single PowerShell command can't solve your problem. One way of
doing this is by using variables. PowerShell can store results of one command in a variable and then pass the
variable to another command. In this chapter, we'll explain what variables are and how you can use them to solve
more complex problems.
Topics Covered:
Personal Variables
o Selecting Variable Names
o Assigning and Returning Values
o Assigning Multiple Variable Values
o Exchanging the Contents of Variables
o Assigning Different Values to Several Variables
Listing Variables
o Finding Variables
o Verify Whether a Variable Exists
o Deleting Variables
Using Special Variable Cmdlets
o Write-Protecting Variables: Creating Constants
o Variables with Description
"Automatic" PowerShell Variables
Environment Variables
o Reading Environment Variables
o Searching for Environment Variables
o Modifying Environment Variables
o Permanent Modifications of Environment Variables
Scope of Variables
o Automatic Restriction
o Changing Variable Visibility
o Setting Scope
Variable Type and "Strongly Typing"
o Strongly Typing
o The Advantages of Specialized Types
Variable Management: Behind the Scenes
o Modification of Variable Options
o Write Protecting Variables
o Examining Strongly Typed Variables
o Validating Variable Contents
Summary
Personal Variables
Variables store pieces of information. This way, you can first gather all the information you may need and store
them in variables. The following example stores two pieces of information in variables and then calculates a new
result:
# Create variables and assign to values
$amount = 120
$VAT = 0.19
# Calculate:
$result = $amount * $VAT
# Output result
$result
22.8
# Replace variables in text with values:
$text = "Net amount $amount matches gross amount $result"
$text
Net amount 120 matches gross amount 142.8
Of course, you can have hard-coded the numbers you multiplied. However, variables are the prerequisite for
reusable code. By assigning your data to variables, you can easily change the information, either by manually
assigning different values to your variables or by assigning user-defined values to your variables. By simply
replacing the first two lines, your script can interactively ask for the variable content:
[Int]$amount = "Enter amount of money"
[Double]$VAT = "Enter VAT rate"
Note that I strongly-typed the variables in this example. You will hear more about variable typing later in that
character , but whenever you use Read-Host or another method that accepts user input, you have to specify the
variable data type or else PowerShell will treat your input as simple string. Simple text is something very different
from numbers and you cannot calculate with pieces of text.
PowerShell creates new variables automatically so there is no need to specifically "declare" variables. Simply assign
data to a variable. The only thing you do need to know is that variable names are always prefixed with a "$" to
access the variable content.
You can then output the variable content by entering the variable name or you can merge the variable content into
strings. Just make sure to use double-quotes to do that. Single-quoted text will not expand variable values.
The assignment operator "=" assigns a value to a variable. You can assign almost anything to a variable, even
complete command results:
# Temporarily store results of a cmdlet:
$listing = Get-ChildItem c:\
$listing
Directory: Microsoft.PowerShell.Core\FileSystem::C:\
$Value1 = 10
$Value2 = 20
$Temp = $Value1
$Value1 = $Value2
$Value2 = $Temp
With PowerShell, swapping variable content is much easier because you can assign multiple values to multiple
variables. Have a look:
# Exchange variable values:
$Value1 = 10; $Value2 = 20
$Value1, $Value2 = $Value2, $Value1
Listing Variables
PowerShell keeps a record of all variables, which is accessible via a virtual drive called variable:. Here is how you
see all currently defined variables:
Dir variable:
Aside from your own personal variables, you'll see many more. PowerShell also defines variables and calls them
"automatic variables." You'll learn more about this soon.
Finding Variables
Using the variable: virtual drive can help you find variables. If you'd like to see all the variables containing the word
"Maximum," try this:
Dir variable:*maximum*
Name
Value
-------MaximumErrorCount
256
MaximumVariableCount 4096
MaximumFunctionCount 4096
MaximumAliasCount
4096
MaximumDriveCount
4096
MaximumHistoryCount 1000
The solution isn't quite so simple if you'd like to know which variables currently contain the value 20. It consists of
several commands piped together.
dir variable: | Out-String -stream | Select-String " 20 "
value2 20
$ 20
Here, the output from Dir is passed on to Out-String, which converts the results of Dir into string. The parameter Stream ensures that every variable supplied by Dir is separately output as string. Select-String selects the lines that
include the desired value, filtering out the rest. White space is added before and after the number 20 to ensure that
only the desired value is found and not other values that contain the number 20 (like 200).
Deleting Variables
PowerShell will keep track of variable use and remove variables that are no longer used so there is no need for you
to remove variables manually. If you'd like to delete a variable immediately, again, do exactly as you would in the
file system:
# create a test variable:
$test = 1
# delete variable:
del variable:\test
New-Variable enables you to specify options, such as a description or write protection. This makes a
variable into a constant. Set-Variable does the same for existing variables.
2.
Cmdlet
ClearVariable
GetVariable
NewVariable
Description
Example
Clear-Variable
Clears the contents of a variable, but not the variable itself. The subsequent value of the
a
variable is NULL (empty). If a data or object type is specified for the variable, by using
same as: $a =
Clear-Variable the type of the objected stored in the variable will be preserved.
$null
Gets the variable object, not the value in which the variable is stored.
Creates a new variable and can set special variable options.
RemoveVariable
Deletes the variable, and its contents, as long as the variable is not a constant or is
created by the system.
SetVariable
Resets the value of variable or variable options, such as a description and creates a
variable if it does not exist.
Get-Variable a
New-Variable
value 12
RemoveVariable a
same as: del
variable:\a
Set-Variable a
12
same as: $a =
12
created with New-Variable. If a variable already exists, you cannot make it constant anymore because youll get an
error message:
#New-Variable cannot write over existing variables:
New-Variable test -value 100 -description `
"test variable with copy protection" -option Constant
New-Variable : A variable named "test" already exists.
At line:1 Char:13
+ New-Variable <<<< test -value 100 -description
"test variable with copy protection" -option Constant
# If existing variable is deleted, New-Variable can create
# a new one with the "Constant" option:
del variable:\test -force
New-Variable test -value 100 -description `
"test variable with copy protection" `
-option Constant
# variables with the "Constant" option may neither be
# modified nor deleted:
del variable:\test -force
Remove-Item : variable "test" may not be removed since it is a
constant or write-protected. If the variable is write-protected,
carry out the process with the Force parameter.
At line:1 Char:4
+ del <<<< variable:\test -force
You can overwrite an existing variable by using the -Force parameter of New-Variable if the existing variable
wasn't created with the Constant option. Variables of the constant type are unchangeable once they have been
created and -Force does not change this:
# Parameter -force overwrites existing variables if these do not
# use the "Constant" option:
New-Variable test -value 100 -description "test variable" -force
New-Variable : variable "test" may not be removed since it is a
constant or write-protected.
At line:1 char:13
+ New-Variable <<<< test -value 100 -description "test variable"
# normal variables may be overwritten with -force without difficulty.
$available = 123
New-Variable available -value 100 -description "test variable" -force
Name
Value
-------myvariable 100
Get-Variable myvariable
Name
Value
-------myvariable 100
Environment Variables
There is another set of variables maintained by the operating system: environment variables.
Working with environment variables in PowerShell is just as easy as working with internal PowerShell variables.
All you need to do is add the prefix to the variable name: env:.
You can modify environment variables by simply assigning new variables to them. Modifying environment
variables can be useful to change the way your machine acts. For example, all programs and scripts located in a
folder that is listed in the "PATH" environment variable can be launched by simply submitting the file name. You
no longer need to specify the complete path or a file extension.
The next example shows how you can create a new folder and add it to the PATH environment variable. Any script
you place into that folder will then be accessible simply by entering its name:
# Create a special folder:
md c:\myTools
# Create and example script in this folder:
" 'Hello!' " > c:\myTools\sayHello.ps1
# Typically, you would have to specify a qualified path name:
C:\myTools\sayHello.ps1
Hello!
# The folder is now added to the path environment:
$env:path += ";C:\myTools"
# All scripts and commands in this folder can be launched by entering their name now:
sayHello
Hello!
You should only change environment variables permanently when there is no other way. For most purposes, it is
completely sufficient to change the temporary process set from within PowerShell. You can assign it the value of
$null to remove a value.
Scope of Variables
PowerShell variables can have a "scope," which determines where a variable is available. PowerShell supports four
special variable scopes: global, local, private, and script. These scopes allow you to restrict variable visibility in
functions or scripts.
Automatic Restriction
Typically, a script will use its own variable scope and isolate all of its variables from the console. So when you run a
script to do some task, it will not leave behind any variables or functions defined by that script once the script is
done.
Setting Scope
While the user of a script can somewhat control scope by using dot-sourcing, a script developer has even more
control over scope by prefixing variable and function names. Let's use the scope modifiers private, local, script, and
global.
Scope
allocation
$private:test
=1
Description
The variable exists only in the current scope. It cannot be accessed in any other scope.
Variables will be created only in the local scope. That's the default for variables that are specified
without a scope. Local variables can be read from scopes originating from the current scope, but
they cannot be modified.
$script:test = This scope represents the top-level scope in a script. All functions and parts of a script can share
1
variables by addressing this scope.
$global:test = This scope represents the scope of the PowerShell console. So if a variable is defined in this scope,
1
it will still exist even when the script that is defining it is no longer running.
$local:test =
1
Script blocks represent scopes in which variables and functions can live. The PowerShell console is the basic scope
(global scope). Each script launched from the console creates its own scope (script scope) unless the script is
launched "dot-sourced." In this case, the script scope will merge with the callers scope.
Functions again create their own scope and functions defined inside of other functions create additional sub-scopes.
Here is a little walk-through. Inside the console, all scopes are the same, so prefixing a variable will not make much
difference:
$test = 1
$local:test
1
$script:test = 12
$global:test
12
$private:test
12
Differences become evident only once you create additional scopes, such as by defining a function:
# Define test function:
Function test { "variable = $a"; $a = 1000 }
# Create variable in console scope and call test function:
$a = 12
Test
variable = 12
# After calling test function, control modifications in console scope:
$a
12
When you don't use any special scope prefix, a child scope can read the variables of the parent scope, but not change
them. If the child scope modifies a variable that was present in the parent scope, as in the example above, then the
child scope actually creates a completely new variable in its own scope, and the parent scope's variable remains
unchanged.
There are exceptions to this rule. If a parent scope declares a variable as "private," then it is accessible only in that
scope and child scopes will not see the variable.
# Define test function:
Function test { "variable = $a"; $a = 1000 }
# Create variable in console scope and call test function:
$private:a = 12
Test
variable =
# Check variable for modifications after calling test function in console scope:
$a
12
Only when you create a completely new variable by using $private: is it in fact private. If the variable already
existed, PowerShell will not reset the scope. To change scope of an existing variable, you will need to first remove it
and then recreate it: Remove-Variable a would remove the variable $a. Or, you can manually change the variable
options: (Get-Variable a).Options = "Private." You can change a variable scope back to the initial default "local by
assigning (Get-Variable a).Options = "None."
Type safety: If you have assigned a type to a variable yourself, then the type will be preserved no matter
what and will never be automatically changed to another data type. You can be absolutely sure that a value
of the correct type is stored in the variable. If someone later on wants to mistakenly assign a value to the
variable that doesn't match the originally chosen type, this will cause an exception.
Special variable types: When automatically assigning a variable type, PowerShell will choose from
generic variable types like Int32 or String. Often, it's much better to store values in a specialized and more
meaningful variable type like DateTime.
Strongly Typing
You can enclose the type name in square brackets before the variable name to assign a particular type to a variable.
For example, if you know that a particular variable will hold only numbers in the range 0 to 255, you can use the
Byte type:
[Byte]$flag = 12
$flag.GetType().Name
Byte
The variable will now store your contents in a single byte, which is not only very memory-efficient, but it will also
raise an error if a value outside the range is specified:
$flag = 300
The value "300" cannot be converted to the type "System.Byte".
Error: "The value for an unsigned byte was too large or too small."
At line:1 char:6
+ $flag <<<< = 300
$list.servers.server
name ip
---- -PC1 10.10.10.10
PC2 10.10.10.12
# Even changes to the XML contents are possible:
$list.servers.server[0].ip = "10.10.10.11"
$list.servers
name ip
---- -PC1 10.10.10.11
PC2 10.10.10.12
# The result could be output again as text, including the
# modification:
$list.get_InnerXML()
<servers><server name="PC1" ip="10.10.10.11" />
<server name="PC2" ip="10.10.10.12" /></servers>
Variable
Description
type
[array]
An array
[bool]
Yes-no value
[byte]
Unsigned 8-bit integer, 0...255
[char]
Individual unicode character
[datetime]
Date and time indications
Example
[boolean]$flag = $true
[byte]$value = 12
[char]$a = "t"
[datetime]$date = "12.Nov 2004 12:30"
[decimal]$a = 12
$a = 12d
$amount = 12.45
[guid]$id = [System.Guid]::NewGuid()
$id.toString()
[decimal]
Decimal number
[double]
[guid]
[hashtable]
[int16]
[int32], [int]
[int64],
[long]
Hash table
16-bit integer with characters
32-bit integers with characters
[int16]$value = 1000
[int32]$value = 5000
[int64]$value = 4GB
[Nullable``1[[System.DateTime]]]$test =
Get-Date
$test = $null
[psobject]
[regex]
Regular expression
[sbyte]
[scriptblock]
[single],
[float]
[string]
[switch]
[timespan]
Time interval
[type]
[uint16]
Type
Unsigned 16-bit integer
[nullable]
[single]$amount = 44.67
String
PowerShell switch parameter
[string]$text = "Hello"
[timespan]$t = New-TimeSpan $(Get-Date)
"1.Sep 07"
[uint16]$value = 1000
[uint32]
[uint64]
[xml]
[uint32]$value = 5000
[uint64]$value = 4GB
Name Description
---- ----------test Subsequently added description
# Get PSVariable object and directly modify the description:
(Get-Variable test).Description =
"An additional modification of the description."
Dir variable:\test | Format-Table name, description
Name Description
---- ----------test An additional modification of the description.
# Modify a description of an existing variable with Set-Variable:
Set-Variable test -description "Another modification"
Dir variable:\test | Format-Table name, description
Name Description
---- ----------test Another modification
As you can see in the example above, you do not need to store the PSVariable object in its own variable to access its
Description property. Instead, you can use a sub-expression, i.e. a statement in parentheses. PowerShell will then
evaluate the contents of the sub-expression separately. The expression directly returns the required PSVariable
object so you can then call the Description property directly from the result of the sub-expression. You could have
done the same thing by using Set-Variable. Reading the settings works only with the PSVariable object:
(Get-Variable test).Description
An additional modification of the description.
Write-Protecting Variables
For example, you can add the ReadOnly option to a variable if you'd like to write-protect it:
$Example = 10
# Put option directly in PSVariable object:
(Get-Variable Example).Options = "ReadOnly"
# Modify option as wish with Set-Variable; because the variable
# is read-only, -force is required:
Set-Variable Example -option "None" -force
# Write-protection turned off again; variable contents may now
# be modified freely:
$Example = 20
The Constant option must be set when a variable is created because you may not convert an existing variable into a
constant.
# A normal variable may not be converted into a constant:
$constant = 12345
(Get-Variable constant).Options = "Constant"
Exception in setting "Options": "The existing variable "constant"
may not be set as a constant. Variables may only be set as
constants when they are created."
At line:1 char:26
+ (Get-Variable constant).O <<<< options = "Constant"
Option
Description
"None"
NO option (default)
"ReadOnly" Variable contents may only be modified by means of the -force parameter
Variable contents can't be modified at all. This option must already be specified when the variable is
"Constant"
created. Once specified this option cannot be changed.
"Private" The variable is visible only in a particular context (local variable).
"AllScope" The variable is automatically copied in a new variable scope.
Table 3.6: Options of a PowerShell variable
In the above example Add() method added a new .NET object to the attributes with New-Object. You'll learn more
about New-Object in Chapter 6. Along with ValidateLengthAttribute, there are additional restrictions that you can
place on variables.
Restriction
Variable may not be zero
Variable may not be zero or empty
Variable must match a Regular Expression
Variable must match a particular number range
Variable may have only a particular set value
Category
ValidateNotNullAttribute
ValidateNotNullOrEmptyAttribute
ValidatePatternAttribute
ValidateRangeAttribute
ValidateSetAttribute
Summary
Variables store information. Variables are by default not bound to a specific data type, and once you assign a value
to a variable, PowerShell will automatically pick a suitable data type. By strongly-typing variables, you can restrict a
variable to a specific data type of your choice. You strongly-type a variable by specifying the data type before the
variable name:
# Strongly type variable a:
[Int]$a = 1
You can prefix the variable name with "$" to access a variable. The variable name can consist of numbers,
characters, and special characters, such as the underline character "_". Variables are not case-sensitive. If you'd like
to use characters in variable names with special meaning to PowerShell (like parenthesis), the variable name must be
enclosed in brackets. PowerShell doesn't require that variables be specifically created or declared before use.
There are pre-defined variables that PowerShell will create automatically. They are called "automatic variables."
These variables tell you information about the PowerShell configuration. For example, beginning with PowerShell
2.0, the variable $psversiontable will dump the current PowerShell version and versions of its dependencies:
PS > $PSVersionTable
Name
Value
-------CLRVersion
2.0.50727.4952
BuildVersion
6.1.7600.16385
PSVersion
2.0
WSManStackVersion
2.0
PSCompatibleVersions
{1.0, 2.0}
SerializationVersion
1.1.0.1
PSRemotingProtocolVersion 2.1
You can change the way PowerShell behaves by changing automatic variables. For example, by default PowerShell
stores only the last 64 commands you ran (which you can list with Get-History or re-run with Invoke-History). To
make PowerShell remember more, just adjust the variable $MaximumHistoryCount:
PS > $MaximumHistoryCount
64
PS > $MaximumHistoryCount = 1000
PS > $MaximumHistoryCount
1000
PowerShell will store variables internally in a PSVariable object. It contains settings that write-protect a variable or
attach a description to it (Table 3.6). It's easiest for you to set this special variable options by using the NewVariable or Set-Variable cmdlets (Table 3.1).
Every variable is created in a scope. When PowerShell starts, an initial variable scope is created, and every script
and every function will create their own scope. By default, PowerShell accesses the variable in the current scope, but
you can specify other scopes by adding a prefix to the variable name\: local:, private:, script:, and global:.
Whenever a command returns more than one result, PowerShell will automatically wrap the results into an array. So
dealing with arrays is important in PowerShell. In this chapter, you will learn how arrays work. We will cover
simple arrays and also so-called "associative arrays," which are also called "hash tables."
Topics Covered:
Discovering Arrays
You can check the data type to find out whether a command will return an array:
$a = "Hello"
$a -is [Array]
False
$a = ipconfig
$a -is [Array]
True
An array will always supports the property Count, which will return the number of elements stored in that array:
$a.Count
53
Here, the ipconfig command returned 53 single results that were all stored in $a. If youd like to examine a single
array element, you can specify its index number. If an array has 53 elements, then its valid index numbers are 0 to
52 (the index always starts at 0).
# Show the second element:
$a[1]
Windows IP Configuration
It is important to understand just when PowerShell will use arrays. If a command returns just one result, it will
happily return that exact result to you. Only when a command returns more than one result will it wrap them in an
array.
$result = Dir
$result -is [array]
True
$result = Dir C:\autoexec.bat
$result -is [array]
False
Of course, this will make writing scripts difficult because sometimes you cannot predict whether a command will
return one, none, or many results. That's why you can make PowerShell return any result as an array.
Use @() if you'd like to force a command to always return its result in an array. This way you find out the number of
files in a folder:
$result = @(Dir $env:windir -ea 0)
$result.Count
Or in a line:
$result = @(Dir $env:windir -ea 0).Count
Ipconfig will return each line of text as an array element. This is great since all the text lines are individual array
elements, allowing you to process them individually in a pipeline. For example, you can filter out unwanted text
lines:
# Store result of an array and then pass along a pipeline to Select-String:
$result = ipconfig
$result | Where-Object { $_ -like "*Address*"
Connection location IPv6 Address . . . : fe80::6093:8889:257e:8d1%8
IPv4 address . . . . . . . . . . . : 192.168.1.35
Connection location IPv6 Address . : fe80::5efe:192.168.1.35%16
Connection location IPv6 Address . . . : fe80::14ab:a532:a7b9:cd3a%11
As such, the result of ipconfig was passed to Where-Object, which filtered out all text lines that did not contain the
keyword you were seeking. With minimal effort, you can now reduce the results of ipconfig to the information you
deem relevant.
In reality, each element returned by Dir (Get-Childitem) is really an object with a number of individual properties.
Some of these properties surfaced in the previous example as column headers (like Mode, LastWriteTime, Length,
and Name). The majority of properties did not show up, though. To see all object properties, you can pipe them on
to Select-Object and specify an "*" to show all properties. PowerShell will now output them as list rather than table
since the console is too narrow to show them all
# Display all properties of this element:
$result[4] | Format-List *
PSPath : Microsoft.PowerShell.Core\FileSystem::
C:\Users\Tobias Weltner\Desktop
PSParentPath : Microsoft.PowerShell.Core\FileSystem::
C:\Users\Tobias Weltner
PSChildName : Desktop
PSDrive : C
PSProvider : Microsoft.PowerShell.Core\FileSystem
PSIsContainer : True
Mode : d-r-Name : Desktop
Parent : Tobias Weltner
Exists : True
Root : C:\
FullName : C:\Users\Tobias Weltner\Desktop
Extension :
CreationTime : 04/13/2007 01:54:53
CreationTimeUtc : 04/12/2007 23:54:53
LastAccessTime : 10/04/2007 14:21:20
LastAccessTimeUtc : 10/04/2007 12:21:20
LastWriteTime : 10/04/2007 14:21:20
LastWriteTimeUtc : 10/04/2007 12:21:20
Attributes : ReadOnly, Directory
You'll learn more about these types of objects in Chapter 5.
Polymorphic Arrays
Just like variables, individual elements of an array can store any type of value you assign. This way, you can store
whatever you want in an array, even a mixture of different data types. Again, you can separate the elements by using
commas:
$array = "Hello", "World", 1, 2, (Get-Date)
$array
Hello
World
1
2
Tuesday, August 21, 2007 12:12:28
Why is the Get-Date cmdlet enclosed in parentheses? Just try it without parentheses. Arrays can only store data.
Get-Date is a command and no data. Since you want PowerShell to evaluate the command first and then put its
result into the array, you will need to use parentheses. Parentheses will identify a sub-expression and tell PowerShell
to evaluate and process it first.
Every element in an array is addressed using its index number. You will find that negative index numbers count
from last to first. You can also use expressions that calculate the index value:
# Create your own new array:
$array = -5..12
# Access the first element:
$array[0]
-5
# Access the last element (several methods):
$array[-1]
12
$array[$array.Count-1]
12
$array[$array.length-1]
12
# Access a dynamically generated array that is not stored in a variable:
(-5..12)[2]
-3
Remember, the first element in your array will always have the index number 0. The index -1 will always give you
the last element in an array. The example demonstrates that the total number of all elements will be returned in two
properties: Count and Length. Both of these properties will behave identically.
Here is a real-world example using arrays and accessing individual elements. First, assume you have a path and
want to access only the file name. Every string object has a built-in method called Split() that can split the text into
chunks. All you will need to do is submit the split character that is used to separate the chunks:
PS > $path = "c:\folder\subfolder\file.txt"
PS > $array = $path.Split('\')
PS > $array
c:
folder
subfolder
file.txt
As you see, by splitting a path at the backslash, you will get its components. The file name is always the last element
of that array. So to access the filename, you will access the last array element:
PS > $array[-1]
file.txt
Likewise, if you are interested in the file name extension, you can change the split character and use "." instead:
PS > $path.Split('.')[-1]
txt
You will find that array sizes can't be modified so PowerShell will work behind the scenes to create a brand-new,
larger array, copying the contents of the old array into it, and adding the new element. PowerShell will work exactly
the same way when you want to delete elements from an array. Here, too, the original array is copied to a new,
smaller array while disposing of the old array. For example, the next line will remove elements 4 and 5 using the
indexes 3 and 4:
$array = $array[0..2] + $array[5..10]
$array.Count
9
As you can imagine, creating new arrays to add or remove array elements is a slow and expensive approach and is
only useful for occasional array manipulations. A much more efficient way is to convert an array to an ArrayList
object, which is a specialized array. You can use it as a replacement for regular arrays and benefit from the added
functionality, which makes it easy to add, remove, insert or even sort array contents:
PS > $array = 1..10
PS > $superarray = [System.Collections.ArrayList]$array
PS > $superarray.Add(11) | Out-Null
PS > $superarray.RemoveAt(3)
PS > $superarray.Insert(2,100)
PS > $superarray
1
2
100
3
5
6
7
8
9
10
11
PS > $superarray.Sort()
PS > $superarray
1
2
3
5
6
7
8
9
10
11
100
PS > $superarray.Reverse()
PS > $superarray
100
11
10
9
8
7
6
5
3
2
1
The square brackets can return several values at the same time exactly like arrays if you specify several keys and
separate them by a comma. Note that the key names in square brackets must be enclosed in quotation marks (you
don't have to do this if you use dot notation).
BIOSVersion
----------SECCSD - 6040000
PowerShellVersion
----------------2.0
You can just define a hash table with the formatting information and pass it on to Format-Table:
You'll learn more about format cmdlets like Format-Table in the Chapter 5
# Insert two new key-value pairs in the list (two different notations are
possible):
$list.Date = Get-Date
$list["Location"] = "Hanover"
# Check result:
$list
Name
---Name
Location
Date
IP
User
Value
----PC01
Hanover
08/21/2007 13:00:18
10.10.10.10
Tobias Weltner
You can create empty hash tables and then insert keys as needed because it's easy to insert new keys in an existing
hash table:
# Create empty hash table
$list = @{}
# Subsequently insert key-value pairs when required
$list.Name = "PC01"
$list.Location = "Hanover"
(...)
All you need to do is to pass your format definitions to Format-Table to ensure that your listing shows just the name
and date of the last modification in two columns:
# Setting formatting specifications for each column in a hash table:
$column1 = @{expression="Name"; width=30; `
label="filename"; alignment="left"}
$column2 = @{expression="LastWriteTime"; width=40; `
label="last modification"; alignment="right"}
# Output contents of a hash table:
$column1
Name Value
---- ----alignment left
label File name
width 30
expression Name
# Output Dir command result with format table and
# selected formatting:
Dir | Format-Table $column1, $column2
File Name Last Modification
--------- --------------Application Data 10/1/2007 16:09:57
Backup 07/26/2007 11:03:07
Contacts 04/13/2007 15:05:30
Debug 06/28/2007 18:33:29
Desktop 10/4/2007 14:21:20
Documents 10/4/2007 21:23:10
(...)
You'll learn more about format cmdlets like Format-Table in the Chapter 5.
$array1 = 1,2,3
$array2 = $array1
$array2[0] = 99
$array1[0]
99
Although the contents of $array2 were changed in this example, this affects $array1 as well, because they are both
identical. The variables $array1 and $array2 internally reference the same storage area. Therefore, you have to
create a copy if you want to copy arrays or hash tables,:
$array1 = 1,2,3
$array2 = $array1.Clone()
$array2[0] = 99
$array1[0]
1
Whenever you add new elements to an array (or a hash table) or remove existing ones, a copy action takes place
automatically in the background and its results are stored in a new array or hash table. The following example
clearly shows the consequences:
# Create array and store pointer to array in $array2:
$array1 = 1,2,3
$array2 = $array1
# Assign a new element to $array2. A new array is created in the process and stored in $array2:
$array2 += 4
$array2[0]=99
# $array1 continues to point to the old array:
$array1[0]
1
At line:1 char:6
+ $array <<<< += "Hello"
In the example, $array was defined as an array of the Integer type. Now, the array is able to store only whole
numbers. If you try to store values in it that cannot be turned into whole numbers, an error will be reported.
Summary
Arrays and hash tables can store as many separate elements as you like. Arrays assign a sequential index number to
elements that always begin at 0. Hash tables in contrast use a key name. That's why every element in hash tables
consists of a key-value pair.
You create new arrays with @(Element1, Element2, ...). You can also leave out @() for arrays and only use the
comma operator. You create new hash tables with @{key1=value1;key2=value2; ...). @{} must always be specified
for hash tables. Semi-colons by themselves are not sufficient to create a new hash table.
You can address single elements of an array or hash able by using square brackets. Specify either the index number
(for arrays) or the key (for hash tables) of the desired element in the square brackets. Using this approach you can
select and retrieve several elements at the same time.
The PowerShell pipeline chains together a number of commands similar to a production assembly. So, one
command hands over its result to the next, and at the end, you receive the result.
Topics Covered:
The PowerShell pipeline chains together a number of commands similar to a production assembly. So, one
command will hand over its result to the next, and at the end, you will receive the result.
Object-oriented Pipeline
What you see here is a true object-oriented pipeline so the results from a command remain rich objects. Only at the
end of the pipeline will the results be reduced to text or HTML or whatever you choose for output.
Take a look at Sort-Object. It will sort the directory listing by file size. If the pipeline had simply fed plain text into
Sort-Object, you would have had to tell Sort-Object just where the file size information was to be found in the raw
text. You would also have had to tell Sort-Object to sort this information numerically and not alphabetically. Not so
here. All you need to do is tell Sort-Object which objects property you want to sort. The object nature tells SortObject all it needs to know: where the information you want to sort is found and whether it is numeric or letters.
You only have to tell Sort-Object which objects property to use for sorting because PowerShell will send results as
rich .NET objects through the pipeline. Sort-Object does the rest automatically. Simply replace Length with another
objects property, such as Name or LastWriteTime, to sort according to these criteria. Unlike text, information in an
object is clearly structured: this is a crucial PowerShell pipeline advantage.
The cmdlets in Table 5.1 have been specially developed for the pipeline and the tasks frequently performed in it.
They will all be demonstrated in the following pages of this chapter.
Just make sure that the commands you use in a pipeline actually do process information from the pipeline. While it
is technically OK, the following line is really useless because notepad.exe cannot process pipeline results:
Dir $env:windir | Sort-Object | notepad
If you'd like to open pipeline results in an editor, you can put the results in a file first and then open the file with the
editor:
Dir $env:windir | Sort-Object | Out-File result.txt; notepad result.txt
Cmdlet/Function Description
Compare-Object Compares two objects or object collections and marks their differences
ConvertTo-Html Converts objects into HTML code
Export-Clixml
Saves objects to a file (serialization)
Export-Csv
Saves objects in a comma-separated values file
ForEach-Object Returns each pipeline object one after the other
Format-List
Outputs results as a list
Format-Table
Outputs results as a table
Format-Wide
Outputs results in several columns
Get-Unique
Removes duplicates from a list of values
Group-Object
Groups results according to a criterion
Import-Clixml
Imports objects from a file and creates objects out of them (deserialization)
Measure-Object Calculates the statistical frequency distribution of object values or texts
more
Returns text one page at a time
Out-File
Writes results to a file
Out-Host
Outputs results in the console
Out-Host -paging Returns text one page at a time
Out-Null
Deletes results
Out-Printer
Sends results to printer
Out-String
Converts results into plain text
Select-Object
Filters properties of an object and limits number of results as requested
Sort-Object
Sorts results
Tee-Object
Copies the pipeline's contents and saves it to a file or a variable
Where-Object
Filters results according to a criterion
Table 5.1: Typical pipeline cmdlets and functions
Sequential (slow) mode: In sequential mode, pipeline commands are executed one at a time. So the
command's results are passed on to the next one only after the command has completely performed its task.
This mode is slow and hogs memory because results are returned only after all commands finish their work
and the pipeline has to store the entire results of each command. The sequential mode basically corresponds
to the variable concept that first saves the result of a command to a variable before forwarding it to the next
command.
Streaming Mode (quick): The streaming mode immediately processes each command result. Every single
result is passed directly onto the subsequent command. It will rush through the entire pipeline and is
immediately output. This quick mode saves memory because results are output while the pipeline
commands are still performing their tasks, and only one element is travelling the pipeline at a time. The
pipeline doesn't have to store all of the command's results, but only one single result at a time.
If you execute this example, you won't see any signs of life from PowerShell for a long time. If you let the command
run too long, you may even run out of memory.
Here Dir returns all files and directors on your drive C:\. These results are passed by the pipeline to Sort-Object, and
because Sort-Object can only sort the results after all of them are available, it will collect the results as they come in.
Those results eventually block too much memory for your system to handle. The two problem areas in sequential
mode are:
First problem: You won't see any activity as long as data is being collected. The more data that has to be acquired,
the longer the wait time will be for you. In the above example, it can take several minutes.
Second problem: Because enormous amounts of data have to be stored temporarily before Sort-Object can process
them, the memory space requirement is very high. In this case, it's even higher so that the entire Windows system
will respond more and more clumsily until finally you won't be able to control it any longer.
That's not all. In this specific case, confusing error messages may pile up. If you have Dir output a complete
recursive folder listing, it may encounter sub-directories where you have no access rights. While Sort-Object
continues to collect results (so no results appear), error messages are not collected by Sort-Object and appear
immediately. Error messages and results get out of sync and may be misinterpreted.
Whether a command supports streaming is up to the programmer. For Sort-Object, there are technical reasons why
this command must first wait for all results. Otherwise, it wouldn't be able to sort the results. If you use commands
that are not designed for PowerShell then their authors had no way to implement the special demands of PowerShell.
For example, it will work if you use the traditional command more.com to output information one page at a time, but
more.com is also a blocking command that could interrupt pipeline streaming:
# If the preceding command can execute its task quickly,
# you may not notice that it can be a block:
Dir | more.com
Tip: Use Out-Host -Paging instead of more! Out-Host is a true PowerShell cmdlet and will support streaming:
Dir c:\ -recurse | Out-Host -paging
Out-Default will transform the pipeline result into visible text. To do so, it will first call Format-Table (or FormatList when there are more than five properties to output) internally, followed by Out-Host. Out-Host will output the
text in the console. So, this is what happens internally:
Dir | Format-Table | Out-Host
me
meUt e
eUtc
c
----- ----- ----- ----- ----- ----- ---- ---- ---- ---- ------ ---- ---- ---- ---- ---Mi... Mi... Ap... C
Mi... True d... A... T... True C:\
2... 2... 2... 2... 2... ...y
Mi... Mi... Ba... C
Mi... True d... B... T... True C:\
2... 2... 2... 2... 2... ...y
Mi... Mi... Co... C
Mi... True d... C... T... True C:\
1... 1... 1... 1... 1... ...y
Mi... Mi... Debug C
Mi... True d... D... T... True C:\
2... 2... 2... 2... 2... ...y
Mi... Mi... De... C
Mi... True d... D... T... True C:\
1... 3... 3... 3... 3... ...y
2...
C...
2...
C...
1...
C...
2...
C...
1...
You now get so much information that columns shrink to an unreadable format.
For example, if you'd prefer not to reduce visual display because of lack of space, you can use the -Wrap parameter,
like this:
Dir | Format-Table * -wrap
Still, the horizontal table design is unsuitable for more than just a handful of properties. This is why PowerShell will
use Format-List, instead of Format-Table, whenever there are more than five properties to display. You should do
the same:
Dir | Format-List *
You will now see a list of several lines for each object's property. For a folder, it might look like this:
PSPath
Weltner\Music
PSParentPath
Weltner
PSChildName
PSDrive
PSProvider
PSIsContainer
Mode
Name
Parent
Exists
Root
FullName
Extension
CreationTime
CreationTimeUtc
LastAccessTime
LastAccessTimeUtc
LastWriteTime
LastWriteTimeUtc
Attributes
: Microsoft.PowerShell.Core\FileSystem::C:\Users\Tobias
: Microsoft.PowerShell.Core\FileSystem::C:\Users\Tobias
:
:
:
:
:
:
:
:
:
:
:
:
:
:
:
:
:
:
Music
C
Microsoft.PowerShell.Core\FileSystem
True
d-r-Music
Tobias Weltner
True
C:\
C:\Users\Tobias Weltner\Music
13.04.2007 01:54:53
12.04.2007 23:54:53
10.05.2007 21:37:26
10.05.2007 19:37:26
10.05.2007 21:37:26
10.05.2007 19:37:26
ReadOnly, Directory
: Microsoft.PowerShell.Core\FileSystem::C:\Users\Tobias
: Microsoft.PowerShell.Core\FileSystem::C:\Users\Tobias
:
:
:
:
:
:
:
:
:
:
:
:
:
:
:
:
:
:
:
:
views.PS1
C
Microsoft.PowerShell.Core\FileSystem
False
-a--views.PS1
4045
C:\Users\Tobias Weltner
C:\Users\Tobias Weltner
False
True
C:\Users\Tobias Weltner\views.PS1
.PS1
18.09.2007 16:30:13
18.09.2007 14:30:13
18.09.2007 16:30:13
18.09.2007 14:30:13
18.09.2007 16:46:12
18.09.2007 14:46:12
Archive
The property names are located on the left and their content on the right. You now know how to find out which
properties an object contains.
Definition
---------Format-Custom [[-Property]
Format-List [[-Property]
Format-Table [[-Property]
Format-Wide [[-Property]
These formatting cmdlets are not just useful for converting all of an object's properties into text, but you can also
select the properties you want to see.
To accomplish this, you type the property that you want to see and not just an asterisk behind the cmdlet. If you do
not want to explicitly use a table or list format, it is considered best practice to use Select-Object rather than Format* because Select-Object will automatically determine the best formatting and will also return objects that can be
processed by subsequent cmdlets. When you use Format-* cmdlets, objects are converted into formatting
information, which can only be interpreted by Out-* cmdlets which is why Format-* cmdlets must be used only at
the end of your pipeline.
The next instruction will retrieve you a directory listing with only Name and Length. Because sub-directories don't
have a property called Length, you will see that the Length column for the sub-directory is empty:
Dir | Select-Object Name, Length
Name
---Sources
Test
172.16.50.16150.dat
172.16.50.17100.dat
output.htm
output.txt
Length
-----16
16
10834
1338
--
cmdlet.txt
23
Or maybe you'd like your directory listing to show how many days have passed since a file or a folder was last
modified. By using the New-TimeSpan cmdlet, you can calculate how much time has elapsed up to the current date.
To see how this works, you can look at the line below as an example that calculates the time difference between
January 1, 2000, and the current date:/p>
New-TimeSpan "01/01/2000"
Days
: 4100
Hours
: 21
Minutes
: 13
Seconds
: 15
Milliseconds
: 545
Ticks
: 3543163955453834
TotalDays
: 4100,8842077012
TotalHours
: 98421,2209848287
TotalMinutes
: 5905273,25908972
TotalSeconds
: 354316395,545383
TotalMilliseconds : 354316395545,383
Use this script block to output how many days have elapsed from the LastWriteTime property up to the current date
and to read it out in its own column:
{(New-TimeSpan $_.LastWriteTime ).Days}
Dir would then return a sub-directory listing that shows how old the file is in days:
Dir | Select-Object Name, Length, {(New-TimeSpan $_.LastWriteTime ).Days}
Name
Length (New-TimeSpan $_.LastWriteTime (Get-Date)).Days
--------- ----------------------------------------------Application data
61
Backup
55
Contacts
158
Debug
82
Desktop
19
Documents
1
(...)
output.htm
11
output.txt
13
backup.pfx
2
cmdlet.txt
23
Grouping Information
Group-Object works by grouping objects based on one or more properties and then counting the groups. You will
only need to specify the property you want to use as your grouping option. The next line will return a status
overview of services:
Get-Service | Group-Object Status
Count Name
Group
----- -------91 Running
{AeLookupSvc, AgereModemAudio, Appinfo, Ati External Event
Utility...}
67 Stopped
{ALG, AppMgmt, Automatic LiveUpdate - Scheduler, BthServ...}
The number of groups will depend only on how many different values are found in the property specified in the
grouping operation. The results' object contains the properties Count, Name, and Group. Services are grouped
according to the desired criteria in the Group property. The following will show you how to obtain a list of all
currently running services:
$result = Get-Service | Group-Object Status
$result[0].Group
Group
----{Application data, Backup, Contacts,
{export.xml, now.xml} in the column Count is
The script block is not limited to returning True or False. The next example will use a script block that returns a file
name's first letter. The result: Group-Object will group the sub-directory contents by first letters:
Dir | Group-Object {$_.name.SubString(0,1).toUpper()}
Count Name
Group
----- -------4 A
{Application data, alias1, output.htm,
output.txt}
2 B
{Backup, backup.pfx}
2 C
{Contacts, cmdlet.txt}
5 D
{Debug, Desktop, Documents, Downloads...}
5 F
{Favorites, filter.ps1, findview.PS1,
findview2.PS1...}
3 L
{Links, layout.lxy, liste.txt}
3 M
{MSI, Music, meinskript.ps1}
3 P
{Pictures, p1.nrproj, ping.bat}
7 S
{Saved Games, Searches, Sources,
SyntaxEditor...}
15 T
{Test, test.bat, test.csv, test.ps1...}
2 V
{Videos, views.PS1}
1 [
{[test]}
1 1
{1}
4 E
{result.csv, result.txt, error.txt,
export.xml}
4 H
{mainscript.ps1, help.txt, help2.txt,
history.csv}
1 I
{info.txt}
2 N
{netto.ps1, now.xml}
3 R
{countfunctions.ps1, report.htm, root.cer}
2 U
{unsigned.ps1, .ps1}
This way, you can even create listings that are divided into sections:
Dir | Group-Object {$_.name.SubString(0,1).toUpper()} | ForEach-Object {
($_.Name)*7;
"======="; $_.Group}
(...)
BBBBBBB
=======
d----a--CCCCCCC
=======
d-r--a--DDDDDDD
=======
d---d-r-d-r-d-r--a--(...)
26.07.2007
17.09.2007
11:03
16:05
Backup
1732 backup.pfx
13.04.2007
13.08.2007
15:05
13:41
Contacts
23586 cmdlet.txt
28.06.2007
30.08.2007
17.09.2007
24.09.2007
26.04.2007
18:33
15:56
13:29
11:22
11:43
Debug
Desktop
Documents
Downloads
1046 drive.vbs
You can use the parameter -NoElement if you don't need the grouped objects and only want to know which groups
exist. This will save a lot of memory:
Get-Process | Group-Object -property Company -noelement
Count Name
----- ---50
1 AuthenTec, Inc.
2 LG Electronics Inc.
1 Symantec Corporation
2 ATI Technologies Inc.
30 Microsoft Corporation
1 Adobe Systems, Inc.
1 BIT LEADER
1 LG Electronics
1 Intel Corporation
2 Apple Inc.
1 BlazeVideo Company
1 ShellTools LLC
2 Infineon Technologies AG
1 Just Great Software
1 Realtek Semiconductor
1 Synaptics, Inc.
Running
Running
Running
Running
Running
(...)
Where-Object takes a script block and evaluates it for every pipeline object. The current object that is travelling the
pipeline is found in $_. So Where-Object really works like a condition (see Chapter 7): if the expression results in
$true, the object will be let through.
Here is another example of what a pipeline filter could look like:
Get-WmiObject Win32_Service | Where-Object {($_.Started -eq
($_.StartMode -eq
"Auto")} | Format-Table
ExitCode Name
ProcessId
State
Status
-------- --------------------0 Automatic Li...
0
Stopped
OK
0 ehstart
0
Stopped
OK
0 LiveUpdate Notic...
0
Stopped
OK
0 WinDefend
0
Stopped
OK
$false) -and
StartMode
--------Auto
Auto
Auto
Auto
If you aren't logged on with administrator privileges, you may not retrieve the information from some processes.
However, you can avoid exceptions by adding -ErrorAction SilentlyContinue (shortcut: -ea 0):
Get-Process | Sort-Object StartTime -ea 0 | Select-Object -last 5 |
Select-Object ProcessName, StartTime
Using the cmdlets Measure-Object and Compare-Object, you can measure and evaluate PowerShell command
results. For example, Measure-Object will allow you to determine how often particular object properties are
distributed. Compare-Object will enable you to compare before-and-after snapshots.
Statistical Calculations
Using the Measure-Object cmdlet, you can get statistic information. For example, if you want to check file sizes, let
Dir give you a directory listing and then examine the Length property:
Dir $env:windir | Measure-Object Length -average -maximum -minimum -sum
Count
: 50
Average : 36771,76
Sum
: 1838588
Maximum : 794050
Minimum : 0
Property : Length
Measure-Object also accepts text files and discovers the frequency of characters, words, and lines in them:
Get-Content $env:windir\windowsupdate.log | Measure-Object -character -line word
Out-* cmdlets turn results into plain text so you are reducing the richness of your results (Out-GridView is the only
exception to the rule which displays the results in an extra window as a mini-spreadsheet).
Export it instead and use one of the xport-* cmdlets to preserve the richness of your results. For example, to open
results in Microsoft Excel, do this:
Get-Process | Export-CSV -UseCulture -NoTypeInformation -Encoding UTF8
$env:temp:\report.csv
Invoke-Item $env:temp\report.csv
Suppressing Results
You can send the output to Out-Null if you want to suppress command output:
# This command not only creates a new directory but also returns the new
directory:
md testdirectory
Directory: Microsoft.PowerShell.Core\FileSystem::C:\Users\Tobias Weltner
Mode
LastWriteTime
Length Name
--------------------- ---d---19.09.2007
14:31
testdirectory
rm testdirectory
# Here the command output is sent to "nothing":
md testdirectory | Out-Null
rm testdirectory
# That matches the following redirection:
md testdirectory > $null
rm testdirectory
HTML Outputs
If youd like, PowerShell can also pack its results into (rudimentary) HTML files. Converting objects into HTML
formats is done by ConvertTo-Html:
Get-Process | ConvertTo-Html | Out-File output.hta
.\output.hta
Get-Process | Select-Object Name, Description | ConvertTo-Html -title
"Process Report" |
Out-File output.hta
.\output.hta
In this chapter, you will learn what objects are and how to get your hands on PowerShell objects before they get
converted to simple text.
Topics Covered:
Standard Methods
Table 6.2: Standard methods of a .NET object
o Calling a Method
o Call Methods with Arguments
Which Arguments are Required?
o Several Method "Signatures"
Playing with PromptForChoice
Working with Real-Life Objects
o Storing Results in Variables
Using Object Properties
PowerShell-Specific Properties
Table 6.3: Different property types
Using Object Methods
Different Method Types
Table 6.4: Different types of methods
Using Static Methods
o Table 6.5: Mathematical functions from the [Math] library
o Finding Interesting .NET Types
Converting Object Types
Using Static Type Members
Using Dynamic Object Instance Members
Creating New Objects
o Creating New Objects with New-Object
Using Constructors
o New Objects by Conversion
o Loading Additional Assemblies: Improved Internet Download
o Using COM Objects
Which COM Objects Are Available?
How Do You Use COM Objects?
Summary
Properties: A pocketknife has particular properties, such as its color, manufacturer, size, or number of
blades. The object is red, weights 55 grams, has three blades, and is made by the firm Idera. So properties<
describe what an object is.
Methods: n addition, you can do things with this object, such as cut, turn screws, or pull corks out of wine
bottles. The object can cut, screw, and remove corks. Everything that an object can is called its methods.
In the computing world, an object is very similar: its nature is described by properties, and the actions it can perform
are called its methods. Properties and methods are called members.
Adding Properties
Next, let's start describing what our object is. To do that, you can add properties to the object.
# Adding a new property:
Add-Member -MemberType NoteProperty -Name Color-Value Red -InputObject
$pocketknife
You can uUse the Add-Member cmdlet to add properties. Here, you added the property color with the value red to
the object $pocketknife. If you call for the object now, it suddenly has a property telling the world that its color is
red:
$pocketknife
Color
----Red
You can then add more properties to describe the object even better. This time, we use positional parameters to
shorten the code necessary to add members to the object:
$pocketknife | Add-Member NoteProperty Weight 55
$pocketknife | Add-Member NoteProperty Manufacturer Idera
$pocketknife | Add-Member NoteProperty Blades 3
By now, you've described the object in $pocketknife with a total of four properties. If you output the object in
$pocketknife in the PowerShell console, PowerShell will automatically convert the object into readable text:
# Show all properties of the object all at once:
$pocketknife
Color
Weight
Blades
----------------Red
55
3
Manufacturer
---------Idera
You will now get a quick overview of its properties when you output the object to the console. You can access the
value of a specific property by either using Select-Object with the parameter -expandProperty, or add a dot, and then
the property name:
# Display a particular property:
$pocketknife | Select-Object -expandProperty Manufacturer
$pocketknife.manufacturer
Adding Methods
With every new property you added to your object, $pocketknife has been gradually taking shape, but it still really
can't do anything. Properties only describe what an object is, not what it can do.
The actions your object can do are called its methods. So let's teach your object a few useful methods:
# Adding new methods:
$pocketknife | Add-Member ScriptMethod cut { "I'm whittling now" }
$pocketknife | Add-Member ScriptMethod screw { "Phew...it's in!" }
$pocketknife | Add-Member ScriptMethod corkscrew { "Pop! Cheers!" }
Again, you used the Add-Member cmdlet, but this time you added a method instead of a property (in this case, a
ScriptMethod). The value is a scriptblock marked by brackets, which contains the PowerShell instructions you want
the method to perform. If you output your object, it will still look the same because PowerShell only visualizes
object properties, not methods:
$pocketknife
Color
Blades
----------Red
3
Weight Manufacturer
------- ---------55 Idera
You can add a dot and then the method name followed by two parentheses to use any of the three newly added
methods. They are part of the method name, so be sure to not put a space between the method name and the opening
parenthesis. Parentheses formally distinguishes properties from methods.
For example, if you'd like to remove a cork with your virtual pocketknife, you can use this code:
$pocketknife.corkscrew()
Pop! Cheers!
Your object really does carry out the exact script commands you assigned to the corkscrew() method. So, methods
perform actions, while properties merely provide information. Always remember to add parentheses to method
names. If you forget them, something interesting like this will happen:
# If you don't use parentheses, you'll retrieve information on a method:
$pocketknife.corkscrew
Script
: "Pop! Cheers!"
OverloadDefinitions : {System.Object corkscrew();}
MemberType
: ScriptMethod
TypeNameOfValue
: System.Object
Value
: System.Object corkscrew();
Name
: corkscrew
IsInstance
: True
You just received a method description. What's interesting about this is mainly the OverloadDefinitions property. As
you'll see later, it reveals the exact way to use a command for any method. In fact, the OverloadDefinitions
information is in an additional object. For PowerShell, absolutely everything is an object so you can store the object
in a variable and then specifically ask the OverloadDefinitions property for information:
# Information about a method is returned in an object of its own:
$info = $pocketknife.corkscrew
$info.OverloadDefinitions
System.Object corkscrew();
The "virtual pocketknife" example reveals that objects are containers that contain data (properties) and actions
(methods).
Our virtual pocketknife was a somewhat artificial object with no real use. Next, let's take a look at a more interesting
object: PowerShell! There is a variable called $host which represents your PowerShell host.
The object stored in the variable $host apparently contains seven properties. The properties names are listed in the
first column. So, if you want to find out which PowerShell version you're using, you could access and return the
Version property:
$Host.Version
Major Minor Build
----- ----- ----1
0
0
Revision
-------0
It worksyou get back the PowerShell host version. The version isn't displayed as a single number. Instead,
PowerShell displays four columns: Major, Minor, Build, and Revision. Whenever you see columns, you know these
are object properties that PowerShell has just converted into text. So, the version in itself is again a special object
designed to store version numbers. Let's check out the data type that the Version property uses:
$version = $Host.Version
$version.GetType().FullName
System.Version
The version is not stored as a String object but as a System.Version object. This object type is perfect for storing
versions, allowing you to easily read all details about any given version:
$Host.Version.Major
1
$Host.Version.Build
0
Knowing an object type is very useful because once you know there is a type called System.Version, you can use it
for your own purposes as well. Try to convert a simple string of your choice into a rich version object! To do that,
simply make sure the string consists of four numbers separated by dots (the typical format for versions), then make
PowerShell convert the string into a System.Version type. You can convert things by adding the target type in
square brackets in front of the string:
[System.Version]'12.55.3.28334'
Major Minor Build Revision
----- ----- ----- -------12
55
3
28334
The CurrentCulture property is just another example of the same concept. Read this property to find out its type:
$Host.CurrentCulture
LCID
Name
------1033
en-US
DisplayName
----------English (United States)
$Host.CurrentCulture.GetType().FullName
System.Globalization.CultureInfo
Country properties are again stored in a highly specialized type that describes a culture with the properties LCID,
Name, and DisplayName. If you want to know which international version of PowerShell you are using, you can
read the DisplayName property:
$Host.CurrentCulture.DisplayName
English (United States)
$Host.CurrentCulture.DisplayName.GetType().FullName
System.String
Likewise, you can convert any suitable string into a CultureInfo-object. Try this if you wanted to find out details
about the 'de-DE' locale:
[System.Globalization.CultureInfo]'de-DE'
LCID
Name
DisplayName
----------------1031
de-DE
German (Germany)
You can also convert the LCID into a CultureInfo object by converting a suitable number:
[System.Globalization.CultureInfo]1033
LCID
---1033
Name
---en-US
DisplayName
----------English (United States)
This is because both these properties again contain an object. If you'd like to find out what is actually stored in the
UI property, you can read the property:
$Host.UI
RawUI
----System.Management.Automation.Internal.Host.InternalHostRawUserInterface
You see that the property UI contains only a single property called RawUI, in which yet another object is stored.
Let's see what sort of object is stored in the RawUI property:
$Host.ui.rawui
ForegroundColor
BackgroundColor
CursorPosition
WindowPosition
CursorSize
BufferSize
WindowSize
MaxWindowSize
MaxPhysicalWindowSize
KeyAvailable
WindowTitle
:
:
:
:
:
:
:
:
:
:
:
DarkYellow
DarkMagenta
0,136
0,87
25
120,3000
120,50
120,62
140,62
False
PowerShell
"RawUI" stands for "Raw User Interface" and exposes the raw user interface settings your PowerShell console uses.
You can read all of these properties, but can you also change them?
Can you actually change properties, too? And if you can, what happens next?
Properties need to accurately describe an object. So, if you modify a property, the underlying object has to also be
modified to reflect that change. If this is not possible, the property cannot be changed and is called "read-only."
Console background and foreground colors are a great example of properties you can easily change. If you do, the
console will change colors accordingly. Your property changes are reflected by the object, and the changed
properties still accurately describe the object.
$Host.ui.rawui.BackgroundColor = "Green"
$Host.ui.rawui.ForegroundColor = "White"
Whether the console receives key press input or not, depends on whether you pressed a key or not. You cannot
control that by changing a property, so this property refuses to be changed. You can only read it.
Property
Description
Text color. Optional values are Black, DarkBlue, DarkGreen, DarkCyan, DarkRed,
ForegroundColor
DarkMagenta, DarkYellow, Gray, DarkGray, Blue, Green, Cyan, Red, Magenta, Yellow,
and White.
Background color. Optional values are Black, DarkBlue, DarkGreen, DarkCyan,
BackgroundColor
DarkRed, DarkMagenta, DarkYellow, Gray, DarkGray, Blue, Green, Cyan, Red,
Magenta, Yellow, and White.
CursorPosition
Current position of the cursor
WindowPosition
Current position of the window
CursorSize
Size of the cursor
BufferSize
Size of the screen buffer
WindowSize
Size of the visible window
MaxWindowSize
Maximally permissible window size
MaxPhysicalWindowSize Maximum possible window size
KeyAvailable
Makes key press input available
WindowTitle
Text in the window title bar
Table 6.1: Properties of the RawUI object
Property Types
Some properties accept numeric values. For example, the size of a blinking cursor is specified as a number from 0 to
100 and corresponds to the fill percentage. The next line sets a cursor size of 75%. Values outside the 0-100 numeric
range will generate an error:
Other properties expect color settings. However, you cannot specify any color that comes to mind. Instead,
PowerShell expects a "valid" color and if your color is unknown, you will receive an error message listing the colors
you can use:
# Colors are specified as text (in quotation marks):
$Host.ui.rawui.ForegroundColor = "yellow"
# Not all colors are allowed:
$Host.ui.rawui.ForegroundColor = "pink"
Exception setting "ForegroundColor": "Cannot convert value "pink" to type
"System.ConsoleColor" due to invalid enumeration values. Specify one of
the
following enumeration values and try again. The possible enumeration
values are
"Black, DarkBlue, DarkGreen, DarkCyan, DarkRed, DarkMagenta, DarkYellow,
Gray,
DarkGray, Blue, Green, Cyan, Red, Magenta, Yellow, White"."
At line:1 char:16
+ $Host.ui.rawui.F <<<< oregroundColor = "pink"
If you assign an invalid value to the property ForegroundColor, the error message will list the possible values. If
you assign an invalid value to the property CursorSize, you get no hint. Why?
Every property expects a certain object type. Some object types are more specific than others. You can use GetMember to find out which object types a given property will expect:
$Host.ui.RawUI | Get-Member -MemberType Property
TypeName:
System.Management.Automation.Internal.Host.InternalHostRawUserInterface
Name
MemberType Definition
------------- ---------BackgroundColor
Property
System.ConsoleColor BackgroundColor
{get;set;}
BufferSize
Property
System.Management.Automation.Host.Size
BufferSize {get;set;}
CursorPosition
Property
System.Management.Automation.Host.Coordinates CursorPosition {get;set;}
CursorSize
Property
System.Int32 CursorSize {get;set;}
ForegroundColor
Property
System.ConsoleColor ForegroundColor
{get;set;}
KeyAvailable
Property
System.Boolean KeyAvailable {get;}
MaxPhysicalWindowSize Property
System.Management.Automation.Host.Size
MaxPhysicalWindowSize {get;}
MaxWindowSize
Property
System.Management.Automation.Host.Size
MaxWindowSize {get;}
WindowPosition
Property
System.Management.Automation.Host.Coordinates WindowPosition {get;set;}
WindowSize
Property
System.Management.Automation.Host.Size
WindowSize {get;set;}
WindowTitle
Property
System.String WindowTitle {get;set;}
As you can see, ForegroundColor expects a System.ConsoleColor type. This type is a highly specialized type: a list
of possible values, a so-called enumeration:
[system.ConsoleColor].IsEnum
True
Whenever a type is an enumeration, you can use a special .NET method called GetNames() to list the possible values
defined in that enumeration:
[System.Enum]::GetNames([System.ConsoleColor])
Black
DarkBlue
DarkGreen
DarkCyan
DarkRed
DarkMagenta
DarkYellow
Gray
DarkGray
Blue
Green
Cyan
Red
Magenta
Yellow
White
If you do not specify anything contained in the enumeration, the error message will simply return the enumerations
contents.
CursorSize stores its data in a System.Int32 object, which is simply a 32-bit number. So, if you try to set the cursor
size to 1,000, you are actually not violating the object boundaries because the value of 1,000 can be stored in a
System.Int32 object. You get an error message anyway because of the validation code that the CursorSize property
executes internally. So, whether you get detailed error information will really depend on the propertys definition. In
the case of CursorSize, you will receive only an indication that your value is invalid, but not why.
Sometimes, a property expects a value to be wrapped in a specific object. For example, if you'd like to change the
PowerShell window size, you can use the WindowSize property. As it turns out, the property expects a new window
size wrapped in an object of type System.Management.Automation.Host.Size. Where can you get an object like that?
$Host.ui.rawui.WindowSize = 100,100
Exception setting "WindowSize": "Cannot convert "System.Object[]"
to "System.Management.Automation.Host.Size"."
At line:1 char:16
+ $Host.ui.rawui.W <<<< indowSize = 100,100
There are a number of ways to provide specialized objects for properties. The easiest approach: read the existing
value of a property (which will get you the object type you need), change the result, and then write back the
changes. For example, here's how you would change the PowerShell window size to 80 x 30 characters:
$value = $Host.ui.rawui.WindowSize
$value
Width
Height
---------110
64
$value.Width = 80
$value.Height = 30
$Host.ui.rawui.WindowSize = $value
Or, you can freshly create the object you need by using New-Object:
$value = New-Object System.Management.Automation.Host.Size(80,30)
$Host.ui.rawui.WindowSize = $value
Or in a line:
$host.ui.rawui.WindowSize = New-Object
System.Management.Automation.Host.Size(80,30)
Version
Property
In the column Name, you will now see all supported properties in $host. In the column Definition, the property
object type is listed first. For example, you can see that the Name property stores a text as System.String type. The
Version property uses the System.Version type.
At the end of each definition, curly brackets will report whether the property is read-only ({get;}) or can also be
modified ({get;set;}). You can see at a glance that all properties of the $host object are only readable. Now, take a
look at the $host.ui.rawui object:
$Host.ui.rawui | Get-Member -membertype property
BackgroundColor
Property
System.ConsoleColor BackgroundColor
{get;set;}
BufferSize
Property
System.Management.Automation.Host.Size
BufferSize {get;set;}
CursorPosition
Property
System.Management.Automation.Host.Coordinates CursorPosition {get;set;}
CursorSize
Property
System.Int32 CursorSize {get;set;}
ForegroundColor
Property
System.ConsoleColor ForegroundColor
{get;set;}
KeyAvailable
Property
System.Boolean KeyAvailable {get;}
MaxPhysicalWindowSize Property
System.Management.Automation.Host.Size
MaxPhysicalWindowSize {get;}
MaxWindowSize
Property
System.Management.Automation.Host.Size
MaxWindowSize {get;}
WindowPosition
Property
System.Management.Automation.Host.Coordinates WindowPosition {get;set;}
WindowSize
Property
System.Management.Automation.Host.Size
WindowSize {get;set;}
WindowTitle
Property
System.String WindowTitle {get;set;}
This result is more differentiated. It shows you that some properties could be changed, while others could not.
There are different "sorts" of properties. Most properties are of the Property type, but PowerShell can add additional
properties like ScriptProperty. So if you really want to list all properties, you can use the -MemberType parameter
and assign it a value of *Property. The wildcard in front of "property" will also select all specialized properties like
"ScriptProperty."
MemberType Definition
---------- ---------Method
System.Void EnterNestedPrompt()
Equals
ExitNestedPrompt
GetHashCode
GetType
NotifyBeginApplication
NotifyEndApplication
PopRunspace
PushRunspace
SetShouldExit
ToString
Method
Method
Method
Method
Method
Method
Method
Method
Method
Method
Minor
----0
Build
-----1
Revision
--------1
----2
----0
-----1
--------1
The same is true for Set_ methods: they change a property value and exist for properties that are read/writeable.
Note in this example: all properties of the $host object can only be read so there are no Set_ methods. There can be
more internal methods like this, such as Add_ and Remove_ methods. Generally speaking, when a method name
contains an underscore, it is most likely an internal method.
Standard Methods
In addition, nearly every object contains a number of "inherited" methods that are also not specific to the object but
perform general tasks for every object:
Method
Equals
GetHashCode
GetType
ToString
Description
Verifies whether the object is identical to a comparison object
Retrieves an object's digital "fingerprint"
Retrieves the underlying object type
Converts the object into readable text
Calling a Method
Before you invoke a method: make sure you know what the method will do. Methods are commands that do
something, which could be dangerous. You can add a dot to the object and then the method name to call a method.
Add an opened and closed parenthesis, like this:
$host.EnterNestedPrompt()
The PowerShell prompt changes to ">>" (unless you changed your default prompt function). You have used
EnterNestedPrompt() to open a nested prompt. Nested prompts are not especially useful in a normal console, so be
sure to exit it again using the exit command or call $host.ExitNestedPrompt().
Nested prompts can be useful in functions or scripts because they work like breakpoints. They can temporarily stop
a function or script so you can verify variable contents or make code changes, after which you continue the code by
entering exit. You'll learn more about this in Chapter 11.
MemberType
---------Method
Method
Method
Definition
---------System.Boolean Equals(Object obj)
System.Int32 GetHashCode()
System.Type GetType()
get_RawUI
Method
System.Management.Automation.Host.PSHostRawUserInterface get_RawUI()
Prompt
Method
System.Collections.Generic.Dictionary`2[[System.String, mscorlib, Versio...
PromptForChoice
Method
System.Int32 PromptForChoice(String
caption, String message, Collection`...
PromptForCredential
Method
System.Management.Automation.PSCredential
PromptForCredential(String cap...
ReadLine
Method
System.String ReadLine()
ReadLineAsSecureString Method
System.Security.SecureString
ReadLineAsSecureString()
ToString
Method
System.String ToString()
Write
Method
System.Void Write(String value),
System.Void Write(ConsoleColor foregrou...
WriteDebugLine
Method
System.Void WriteDebugLine(String message)
WriteErrorLine
Method
System.Void WriteErrorLine(String value)
WriteLine
Method
System.Void WriteLine(), System.Void
WriteLine(String value), System.Voi...
WriteProgress
Method
System.Void WriteProgress(Int64 sourceId,
ProgressRecord record)
WriteVerboseLine
Method
System.Void WriteVerboseLine(String
message)
WriteWarningLine
Method
System.Void WriteWarningLine(String
message)
Most methods require additional arguments from you, which are listed in the Definition column.
The Definition property tells you how to call the method. Every definition will begin with the object type that a
method returns. In this example, it is System.Void, a special object type because it represents "nothing": the method
doesn't return anything at all. A method "returning" System.Void is really a procedure, not a function.
Next, a methods name follows, which is then followed by required arguments. WriteDebugLine needs exactly one
argument called message, which is of String type. Here is how you call WriteDebugLine():
$Host.ui.WriteDebugLine("Hello!")
Hello!
The definition is hard to read at first. You can make it more readable by using Replace() to add line breaks.
Remember the "backtick" character ("`"). It introduces special characters; "`n" stands for a line break.
$info.Definition.Replace("), ", ")`n")
System.Void WriteLine()
System.Void WriteLine(String value)
System.Void WriteLine(ConsoleColor foregroundColor, ConsoleColor
backgroundColor, String value)
This definition tells you: You do not necessarily need to supply arguments:
$host.ui.WriteLine()
The result is an empty line.
To output text, you can specify one argument only, the text itself:
$Host.ui.WriteLine("Hello world!")
Hello world!
The third variant adds support for foreground and background colors:
$host.ui.WriteLine("Red", "White", "Alarm!")
WriteLine() actually is the low-level function of the Write-Host cmdlet:
Write-Host
Write-Host "Hello World!"
Write-Host -ForegroundColor Red -BackgroundColor White Alarm!
So far, most methods you examined have turned out to be low-level commands for cmdlets. This is also true for the
following methods: Write() (corresponds to Write-Host -nonewline) or ReadLine()/ReadLineAsSecureString() (readhost -asSecureString) or PromptForCredential() (get-credential).
A new functionality is exposed by the method PromptForChoice(). Let's first examine which arguments this method
expects:
$info = $Host.UI | Get-Member PromptForChoice
$info.Definition
System.Int32 PromptForChoice(String caption, String message, Collection`1
choices,
Int32 defaultChoice)
You can get the same information if you call the method without parentheses:
You can get the same information if you call the method without parentheses:
$Host.ui.PromptForChoice
MemberType
: Method
OverloadDefinitions : {System.Int32 PromptForChoice(String caption, String
message,
Collection`1 choices, Int 32 defaultChoice)}
TypeNameOfValue
: System.Management.Automation.PSMethod
Value
: System.Int32 PromptForChoice(String caption, String
message,
Collection`1 choices, Int32 defaultChoice)
Name
: PromptForChoice
IsInstance
: True
The definition reveals that this method returns a numeric value (System.Int32). It requires a heading and a message
respectively as text (String). The third argument is a bit strange: Collection`1 choices. The fourth argument is a
number (Int32), the standard selection. You may have noticed by now the limitations of PowerShell's built-in
description.
This is how you can use PromptForChoice() to create a simple menu:
$yes = ([System.Management.Automation.Host.ChoiceDescription]"&yes")
$no = ([System.Management.Automation.Host.ChoiceDescription]"&no")
$selection =
[System.Management.Automation.Host.ChoiceDescription[]]($yes,$no)
$answer = $Host.ui.PromptForChoice('Reboot', 'May the system now be
rebooted?',$selection,1)
$selection[$answer]
if ($selection -eq 0) {
"Reboot"
} else {
"OK, then not"
}
Every PowerShell command will return objects. However, it is not that easy to get your hands on objects because
PowerShell converts them to text whenever you output them to the console.
To get to the real objects, you can directly access them inside of a variable. Dir has stored its result in $listing. It is
wrapped in an array since the listing consists of more than one entry. Access an array element to get your hands on a
real object:
# Access first element in listing
$object = $listing[0]
# Object is converted into text when you output it in the console
$object
Directory: Microsoft.PowerShell.Core\FileSystem::C:\Users\Tobias Weltner
Mode
LastWriteTime
Length Name
--------------------- ---d---20.07.2007
11:37
Application data
The object picked here happens to match the folder Application Data; so it represents a directory. You can do this if
you prefer to directly pick a particular directory or file:
# Address a particular file:
$object = Get-Item $env:windir\explorer.exe
# Address a folder:
$object = Get-Item $env:windir
Properties marked with {get;set;} in the column Definition are readable and writeable. You can actually change their
value, too, by simply assigning a new value (provided you have sufficient privileges):
# Determine last access date:
$object.LastAccessTime
Friday, July 20, 2007 11:37:39
# Change Date:
$object.LastAccessTime = Get-Date
# Change was accepted:
$object.LastAccessTime
Monday, October 1, 2007 15:31:41
PowerShell-Specific Properties
PowerShell can add additional properties to an object. Whenever that occurs, Get-Member will label the property
accordingly in the MemberType column. Native properties are just called "Property." Properties that are added by
PowerShell use a prefix, such as "ScriptProperty" or "NoteProperty."
A NoteProperty like PSChildName contains static data. PowerShell will add it to tag additional information to an
object. A ScriptProperty like Mode executes PowerShell script code that calculates the propertys value.
MemberType
Description
AliasProperty
Alternative name for a property that already exists
CodeProperty
Static .NET method returns property contents
Property
Genuine property
NoteProperty
Subsequently added property with set data value
ScriptProperty
Subsequently added property whose value is calculated by a script
ParameterizedProperty Property requiring additional arguments
Table 6.3: Different property types
get_LastAccessTimeUtc
Method
System.DateTime get_LastAccessTimeUtc()
get_LastWriteTime
Method
System.DateTime get_LastWriteTime()
get_LastWriteTimeUtc
Method
System.DateTime get_LastWriteTimeUtc()
get_Name
Method
System.String get_Name()
get_Parent
Method
System.IO.DirectoryInfo get_Parent()
get_Root
Method
System.IO.DirectoryInfo get_Root()
InitializeLifetimeService Method
System.Object
InitializeLifetimeService()
MoveTo
Method
System.Void MoveTo(String destDirName)
Refresh
Method
System.Void Refresh()
SetAccessControl
Method
System.Void
SetAccessControl(DirectorySecurity DirectorySecurity)
set_Attributes
Method
System.Void
set_Attributes(FileAttributes value)
set_CreationTime
Method
System.Void set_CreationTime(DateTime
value)
set_CreationTimeUtc
Method
System.Void set_CreationTimeUtc(DateTime
value)
set_LastAccessTime
Method
System.Void set_LastAccessTime(DateTime
value)
set_LastAccessTimeUtc
Method
System.Void
set_LastAccessTimeUtc(DateTime value)
set_LastWriteTime
Method
System.Void set_LastWriteTime(DateTime
value)
set_LastWriteTimeUtc
Method
System.Void
set_LastWriteTimeUtc(DateTime value)
ToString
Method
System.String ToString()
You can apply methods just like you did in the previous examples. For example, you can use the
CreateSubDirectory method if you'd like to create a new sub-directory. First, you should find out which arguments
this method requires and what it returns:
$info = $object | Get-Member CreateSubDirectory
$info.Definition.Replace("), ", ")`n")
System.IO.DirectoryInfo CreateSubDirectory(String path)
System.IO.DirectoryInfo CreateSubDirectory(String path, DirectorySecurity
DirectorySecurity)
You can see that the method has two signatures. Try using the first to create a sub-directory and the second to add
access permissions.
The next line creates a sub-directory called "My New Directory" without any special access privileges:
$object.CreateSubDirectory("My New Directory")
Mode
LastWriteTime
Length Name
--------------------- ---d---01.10.2007
15:49
My New Directory
Because the method returns a DirectoryInfo object as a result and you haven't caught and stored this object in a
variable, the pipeline will convert it into text and output it. You could just as well have stored the result of the
method in a variable:
Description
Method mapped to a static .NET method
Genuine method
Method invokes PowerShell code
Every type can have its own set of private members called "static" members. You can simply specify a type in
square brackets, pipe it to Get-Member, and then use the -static parameter to see the static members of a type.
[System.DateTime] | Get-Member -static -memberType *method
TypeName: System.DateTime
Name
MemberType
------------Compare
Method
DateTime t2)
DaysInMonth
Method
Int32 month)
Equals
Method
DateTime t2), static Sys...
FromBinary
Method
dateData)
FromFileTime
Method
fileTime)
FromFileTimeUtc
Method
fileTime)
FromOADate
Method
get_Now
Method
get_Today
Method
Definition
---------static System.Int32 Compare(DateTime t1,
static System.Int32 DaysInMonth(Int32 year,
static System.Boolean Equals(DateTime t1,
static System.DateTime FromBinary(Int64
static System.DateTime FromFileTime(Int64
static System.DateTime FromFileTimeUtc(Int64
static System.DateTime FromOADate(Double d)
static System.DateTime get_Now()
static System.DateTime get_Today()
get_UtcNow
Method
static System.DateTime get_UtcNow()
IsLeapYear
Method
static System.Boolean IsLeapYear(Int32 year)
op_Addition
Method
static System.DateTime op_Addition(DateTime
d, TimeSpan t)
op_Equality
Method
static System.Boolean op_Equality(DateTime
d1, DateTime d2)
op_GreaterThan
Method
static System.Boolean
op_GreaterThan(DateTime t1, DateTime t2)
op_GreaterThanOrEqual Method
static System.Boolean
op_GreaterThanOrEqual(DateTime t1, DateTime t2)
op_Inequality
Method
static System.Boolean op_Inequality(DateTime
d1, DateTime d2)
op_LessThan
Method
static System.Boolean op_LessThan(DateTime
t1, DateTime t2)
op_LessThanOrEqual
Method
static System.Boolean
op_LessThanOrEqual(DateTime t1, DateTime t2)
op_Subtraction
Method
static System.DateTime
op_Subtraction(DateTime d, TimeSpan t), sta...
Parse
Method
static System.DateTime Parse(String s),
static System.DateTime Par...
ParseExact
Method
static System.DateTime ParseExact(String s,
String format, IFormat...
ReferenceEquals
Method
static System.Boolean ReferenceEquals(Object
objA, Object objB)
SpecifyKind
Method
static System.DateTime SpecifyKind(DateTime
value, DateTimeKind kind)
TryParse
Method
static System.Boolean TryParse(String s,
DateTime& result), static...
TryParseExact
Method
static System.Boolean TryParseExact(String
s, String format, IForm...
There are a lot of method names starting with "op_," with "op" standing for "operator." These are methods that are
called internally whenever you use this data type with an operator. op_GreaterThanOrEqual is the method that does
the internal work when you use the PowerShell comparison operator "-ge" with date values.
The System.DateTime class supplies you with a bunch of important date and time methods. For example, you should
use Parse() to convert a date string into a real DateTime object and the current locale:
[System.DateTime]::Parse("March 12, 1999")
Friday, March 12, 1999 00:00:00
You could easily find out whether a certain year is a leap year:
[System.DateTime]::isLeapYear(2010)
False
for ($x=2000; $x -lt 2010; $x++) { if( [System.DateTime]::isLeapYear($x) ) {
"$x is a leap year!" } }
2000 is a leap year!
2004 is a leap year!
2008 is a leap year!
Or you'd like to tell your children with absolute precision how much time will elapse before they get their Christmas
gifts:
[DateTime]"12/24/2007 18:00" - [DateTime]::now
Days
: 74
Hours
: 6
Minutes
: 28
Seconds
: 49
Milliseconds
: 215
Ticks
: 64169292156000
TotalDays
: 74.2700140694444
TotalHours
: 1782,48033766667
TotalMinutes
: 106948,82026
TotalSeconds
: 6416929,2156
TotalMilliseconds : 6416929215,6
Two dates are being subtracted from each other here so you now know what happened during this operation:
The first time indication is actually text. For it to become a DateTime object, you must specify the desired
object type in square brackets. Important: Converting a String to a DateTime this way always uses the U.S.
locale. To convert a String to a DateTime using your current locale, you can use the Parse() method as
shown a couple of moments ago!
The second time comes from the Now static property, which returns the current time as DateTime object.
This is the same as calling the Get-Date cmdlet (which you'd then need to put in parenthesis because you
wouldn't want to subtract the Get-Date cmdlet, but rather the result of the Get-Date cmdlet).
The two timestamps are subtracted from each other using the subtraction operator ("-"). This was possible
because the DateTime class defined the op_Subtraction() static method, which is needed for this operator.
Of course, you could have called the static method yourself and received the same result:
[DateTime]::op_Subtraction("12/24/2007 18:00", [DateTime]::Now)
Now it's your turn. In the System.Math class, you'll find a lot of useful mathematical methods. Try to put some of
these methods to work.
Function
Abs
Acos
Asin
Atan
Atan2
BigMul
Ceiling
Cos
Cosh
DivRem
Description
Returns the absolute value of a specified number (without signs).
Returns the angle whose cosine is the specified number.
Returns the angle whose sine is the specified number.
Returns the angle whose tangent is the specified number.
Returns the angle whose tangent is the quotient of two specified
numbers.
Calculates the complete product of two 32-bit numbers.
Returns the smallest integer greater than or equal to the specified
number.
Returns the cosine of the specified angle.
Returns the hyperbolic cosine of the specified angle.
Calculates the quotient of two numbers and returns the remainder
in an output parameter.
Example
[Math]::Abs(-5)
[Math]::Acos(0.6)
[Math]::Asin(0.6)
[Math]::Atan(90)
[Math]::Atan2(90, 15)
[Math]::BigMul(1gb, 6)
[Math]::Ceiling(5.7)
[Math]::Cos(90)
[Math]::Cosh(90)
$a = 0
[Math]::DivRem(10,3,[ref]$a)
$a
Exp
[Math]::Exp(12)
[Math]::Floor(5.7)
[Math]::IEEERemainder(5,2)
[Math]::Log(1)
[Math]::Log10(6)
[Math]::Max(-5, 12)
[Math]::Min(-5, 12)
[Math]::Pow(6,2)
[Math]::Round(5.51)
[Math]::Sign(-12)
[Math]::Sin(90)
[Math]::Sinh(90)
[Math]::Sqrt(64)
[Math]::Tan(45)
[Math]::Tanh(45)
[Math]::Truncate(5.67)
:
:
:
:
:
:
:
127.0.0.1
16777343
InterNetwork
False
False
False
HostName
-------PCNEU01
Aliases
------{}
AddressList
----------{127.0.0.1}
You can use a different constructor to create a specific date. There is one that takes three numbers for year, month,
and day:
New-Object System.DateTime(2000,5,1)
Monday, May 01, 2000 12:00:00 AM
If you simply add a number, yet another constructor is used which interprets the number as ticks, the smallest time
unit a computer can process:
New-Object System.DateTime(568687676789080999)
Monday, February 07, 1803 7:54:38 AM
Using Constructors
When you create a new object using New-Object, you can submit additional arguments by adding argument values
as a comma separated list enclosed in parentheses. New-Object is in fact calling a method called ctor, which is the
type constructor. Like any other method, it can support different argument signatures.
Let's check out how you can discover the different constructors, which a type will support. The next line creates a
new instance of a System.String and uses a constructor that accepts a character and a number:
New-Object System.String(".", 100)
.............................................................................
.......................
To list the available constructors for a type, you can use the GetConstructors() method available in each type. For
example, you can find out which constructors are offered by the System.String type to produce System.String
objects:
[System.String].GetConstructors() | ForEach-Object { $_.toString() }
Void .ctor(Char*)
Void .ctor(Char*, Int32, Int32)
Void .ctor(SByte*)
Void .ctor(SByte*, Int32, Int32)
Void .ctor(SByte*, Int32, Int32, System.Text.Encoding)
Void .ctor(Char[], Int32, Int32)
Void .ctor(Char[])
Void .ctor(Char, Int32)
In fact, there are eight different signatures to create a new object of the System.String type. You just used the last
variant: the first argument is the character, and the second a number that specifies how often the character will be
repeated. PowerShell will use the next to last constructor so if you specify text in quotation marks, it will interpret
text in quotation marks as a field with nothing but characters (Char[]).
Objects can often be created without New-Object by using type casting instead. You've already seen how it's done
for variables in Chapter 3:
# PowerShell normally wraps text as a System.String:
$date = "November 1, 2007"
$date.GetType().FullName
System.String
$date
November 1, 2007
# Use strong typing to set the object type of $date:
[System.DateTime]$date = "November 1, 2007"
$date.GetType().FullName
System.DateTime
$date
Thursday, November 1, 2007 00:00:00
So, if you enclose the desired .NET type in square brackets and put it in front of a variable name, PowerShell will
require you to use precisely the specified object type for this variable. If you assign a value to the variable,
PowerShell will automatically convert it to that type. That process is sometimes called "implicit type conversion."
Explicit type conversion works a little different. Here, the desired type is put in square brackets again, but placed on
the right side of the assignment operator:
$value = [DateTime]"November 1, 2007"
$value
Thursday, November 1, 2007 00:00:00
PowerShell would first convert the text into a date because of the type specification and then assign it to the variable
$value, which itself remains a regular variable without type specification. Because $value is not limited to DateTime
types, you can assign other data types to the variable later on.
$value = "McGuffin"
Using the type casting, you can also create entirely new objects without New-Object. First, create an object using
New-Object:
New-Object system.diagnostics.eventlog("System")
Max(K) Retain OverflowAction
Entries Name
------ ------ -------------------- ---20,480
0 OverwriteAsNeeded
64,230 System
In the second example, the string System is converted into the System.Diagnostics.Eventlog type: The result is an
EventLog object representing the System event log.
So, when can you use New-Object and when type conversion? It is largely a matter of taste, but whenever a type has
more than one constructor and you want to select the constructor, you should use New-Object and specify the
arguments for the constructor of your choice. Type conversion will automatically choose one constructor, and you
have no control over which constructor is picked.
# Using New-Object, you can select the constructor you wish of the type
yourself:
New-Object System.String(".", 100)
.............................................................................
.......................
# When casting types, PowerShell selects the constructor automatically
# For the System.String type, a constructor will be chosen that requires no
arguments
# Your arguments will then be interpreted as a PowerShell subexpression in
which
# a field will be created
# PowerShell will change this field into a System.String type
# PowerShell changes fields into text by separating elements from each other
with whitespace:
[system.string](".",100)
. 100
# If your arguments are not in round brackets, they will be interpreted as a
Field
# and the first field element # Cast in the System.String type:
[system.string]".", 100
.
100
Type conversion can also include type arrays (identified by "[]") and can be a multi-step process where you convert
from one type over another type to a final type. This is how you would convert string text into a character array:
[char[]]"Hello!"
H
e
l
l
o
!
You could then convert each character into integers to get the character codes:
[Int[]][Char[]]"Hello World!"
72
97
108
108
111
32
87
101
108
116
33
Conversely, you could make a numeric list out of a numeric array and turn that into a string:
[string][char[]](65..90)
A B C D E F G H I J K L M N O P Q R S T U V W X Y Z
$OFS = ","
[string][char[]](65..90)
A,B,C,D,E,F,G,H,I,J,K,L,M,N,O,P,Q,R,S,T,U,V,W,X,Y,Z
Just remember: if arrays are converted into a string, PowerShell uses the separator in the $ofs automatic variable as a
separator between the array elements.
Once you do that, you have access to a whole bunch of new types:
[Microsoft.VisualBasic.Interaction] | Get-Member -static
TypeName: Microsoft.VisualBasic.Interaction
Name
---AppActivate
Beep
CallByName
Choose
Command
CreateObject
DeleteSetting
Environ
Equals
GetAllSettings
GetObject
GetSetting
IIf
InputBox
MsgBox
Partition
ReferenceEquals
SaveSetting
Shell
switch
MemberType
---------Method
Method
Method
Method
Method
Method
Method
Method
Method
Method
Method
Method
Method
Method
Method
Method
Method
Method
Method
Method
Definition
---------static System.Void AppActivate(Int32 Proces...
static System.Void Beep()
static System.Object CallByName(Object Obje...
static System.Object Choose(Double Index, P...
static System.String Command()
static System.Object CreateObject(String Pr...
static System.Void DeleteSetting(String App...
static System.String Environ(Int32 Expressi...
static System.Boolean Equals(Object objA, O...
static System.String[,] GetAllSettings(Stri...
static System.Object GetObject(String PathN...
static System.String GetSetting(String AppN...
static System.Object IIf(Boolean Expression...
static System.String InputBox(String Prompt...
static Microsoft.VisualBasic.MsgBoxResult M...
static System.String Partition(Int64 Number...
static System.Boolean ReferenceEquals(Objec...
static System.Void SaveSetting(String AppNa...
static System.Int32 Shell(String PathName, ...
static System.Object switch(Params Object[]...
Or, you can use a much-improved download method, which shows a progress bar while downloading files from the
Internet:
# Reload required assembly:
[void][reflection.assembly]::LoadWithPartialName("Microsoft.VisualBasic")
# Download address of a file:
$address = "http://www.idera.com/powershellplus"
# This is where the file should be saved:
$target = "$home\psplus.zip"
# Download will be carried out:
$object = New-Object Microsoft.VisualBasic.Devices.Network
$object.DownloadFile($address, $target, "", "", $true, 500, $true,
"DoNothing")
MemberType Definition
---------- ---------Method
bool AppActivate (Variant, Variant)
CreateShortcut
Exec
ExpandEnvironmentStrings
LogEvent
Popup
Variant)
RegDelete
RegRead
RegWrite
Run
SendKeys
Method
Method
Method
Method
Method
Method
Method
Method
Method
Method
The information required to understand how to use a method may be inadequate. Only the expected object types are
given, but not why the arguments exist. The Internet can help you if you want to know more about a COM
command. Go to a search site of your choice and enter two keywords: the ProgID of the COM components (in this
case, it will be WScript.Shell) and the name of the method that you want to use.
Some of the commonly used COM objects are WScript.Shell, WScript.Network, Scripting.FileSystemObject,
InternetExplorer.Application, Word.Application, and Shell.Application. Lets create a shortcut to powershell.exe
using WScript.Shell Com object and its method CreateShorcut():
# Create an object:
$wshell = New-Object -comObject WScript.Shell
# Assign a path to Desktop to the variable $path
$path = [system.Environment]::GetFolderPath('Desktop')
# Create a link object $link = $wshell.CreateShortcut("$path\PowerShell.lnk")
# $link is an object and has the properties and methods
$link | Get-Member
TypeName: System.__ComObject#{f935dc23-1cf0-11d0-adb9-00c04fd58a0b}
Name
---Load
Save
Arguments
Description
FullName
Hotkey
IconLocation
RelativePath
TargetPath
WindowStyle
WorkingDirectory
MemberType
---------Method
Method
Property
Property
Property
Property
Property
Property
Property
Property
Property
Definition
---------void Load (string)
void Save ()
string Arguments () {get} {set}
string Description () {get} {set}
string FullName () {get}
string Hotkey () {get} {set}
string IconLocation () {get} {set}
{get} {set}
string TargetPath () {get} {set}
int WindowStyle () {get} {set}
string WorkingDirectory () {get} {set}
Summary
Everything in PowerShell is represented by objects that have exactly two aspects: properties and methods, which
both form the members of the object. While properties store data, methods are executable commands.
Objects are the result of all PowerShell commands and are not converted to readable text until you output the objects
to the console. However, if you save a commands result in a variable, you will get a handle on the original objects
and can evaluate their properties or call for their commands. If you would like to see all of an objects properties,
then you can pass the object to Format-List and type an asterisk after it. This allows allnot only the most
importantproperties to be output as text.
The Get-Member cmdlet retrieves even more data, enabling you to output detailed information on the properties and
methods of any object.
All the objects that you will work with in PowerShell originate from .NET framework, which PowerShell is layered.
Aside from the objects that PowerShell commands provide to you as results, you can also invoke objects directly
from the .NET framework and gain access to a powerful arsenal of new commands. Along with the dynamic
methods furnished by objects, there are also static methods, which are provided directly by the class from which
objects are also derived.
If you cannot perform a task with the cmdlets, regular console commands, or methods of the .NET framework, you
can resort to the unmanaged world outside the .NET framework. You can directly access the low-level API
functions, the foundation of the .NET framework, or use COM components.
Conditions are what you need to make scripts clever. Conditions can evaluate a situation and then take appropriate
action. There are a number of condition constructs in the PowerShell language which that we will look at in this
chapter.
In the second part, you'll employ conditions to execute PowerShell instructions only if a particular condition is
actually met.
Topics Covered:
Creating Conditions
o Table 7.1: Comparison operators
o Carrying Out a Comparison
o "Reversing" Comparisons
o Combining Comparisons
Table 7.2: Logical operators
o Comparisons with Arrays and Collections
Verifying Whether an Array Contains a Particular Element
Where-Object
o Filtering Results in the Pipeline
o Putting a Condition
If-ElseIf-Else
Switch
o Testing Range of Values
o No Applicable Condition
o Several Applicable Conditions
o Using String Comparisons
Case Sensitivity
Wildcard Characters
Regular Expressions
Creating Conditions
A condition is really just a question that can be answered with yes (true) or no (false). The following PowerShell
comparison operators allow you to compare values,
Operator
-eq, -ceq, -ieq
-ne, -cne, -ine
-gt, -cgt, -igt
-ge, -cge, -ige
-lt, -clt, -ilt
-le, -cle, -ile
-contains,
-ccontains,
-icontains
-notcontains,
-cnotcontains,
-inotcontains
Conventional
=
<>
>
>=
<
<=
Description
equals
not equal
greater than
greater than or equal to
less than
less than or equal to
Example
10 -eq 15
10 -ne 15
10 -gt 15
10 -ge 15
10 -lt 15
10 -le 15
Result
$false
$true
$false
$false
$true
$true
contains
1,2,3 -contains 1
$true
As long as you compare only numbers or only strings, comparisons are straight-forward:
123 -lt 123.5
True
However, you can also compare different data types. However, these results are not always as straight-forward as
the previous one:
12 -eq "Hello"
False
12 -eq "000012"
True
"12" -eq 12
True
"12" -eq 012
True
"012" -eq 012
False
123 lt 123.4
True
123 lt "123.4"
False
123 lt "123.5"
True
Are the results surprising? When you compare different data types, PowerShell will try to convert the data types into
one common data type. It will always look at the data type to the left of the comparison operator and then try and
convert the value to the right to this data type.
"Reversing" Comparisons
With the logical operator -not you can reverse comparison results. It will expect an expression on the right side that
is either true or false. Instead of -not, you can also use "!":
$a = 10
$a -gt 5
True
-not ($a -gt 5)
False
# Shorthand: instead of -not "!" can also be used:
!($a -gt 5)
False
You should make good use of parentheses if you're working with logical operators like not. Logical operators are
always interested in the result of a comparison, but not in the comparison itself. That's why the comparison should
always be in parentheses.
Combining Comparisons
You can combine several comparisons with logical operators because every comparison returns either True or False.
The following conditional statement would evaluate to true only if both comparisons evaluate to true:
( ($age -ge 18) -and ($sex -eq "m") )
You should put separate comparisons in parentheses because you only want to link the results of these comparisons
and certainly not the comparisons themselves.
Operator Description
-and
-or
-xor
-not
Left Value
True
False
Both conditions must be met
False
True
True
False
At least one of the two conditions must be met
False
True
True
False
One or the other condition must be met, but not both
False
True
Reverses the result
Right Value
False
True
False
True
False
True
False
True
True
False
True
False
True
(not applicable)
False
Result
False
False
False
True
True
True
False
True
False
False
True
True
False
True
If you'd like to see only the elements of an array that don't match the comparison value, you can use -ne (not equal)
operator:
1,2,3,4,3,2,1 -ne 3
1
2
4
2
1
1,2,3 eq 5
# -contains answers the question of whether the sought element is included in
the array:
1,2,3 -contains 5
False
1,2,3 -notcontains 5
True
Where-Object
In the pipeline, the results of a command are handed over to the next one and the Where-Object cmdlet will work
like a filter, allowing only those objects to pass the pipeline that meet a certain condition. To make this work, you
can specify your condition to Where-Object.
MaxWorkingSet
MinWorkingSet
Modules
NonpagedSystemMemorySize
NonpagedSystemMemorySize64
PagedMemorySize64
PagedSystemMemorySize
PagedSystemMemorySize64
PeakPagedMemorySize
PeakPagedMemorySize64
PeakWorkingSet
PeakWorkingSet64
PeakVirtualMemorySize
PeakVirtualMemorySize64
PriorityBoostEnabled
PrivateMemorySize64
PrivilegedProcessorTime
ProcessName
ProcessorAffinity
Responding
SessionId
StartInfo
StartTime
SynchronizingObject
Threads
UserProcessorTime
VirtualMemorySize64
EnableRaisingEvents
StandardInput
StandardOutput
StandardError
WorkingSet64
Site
Container
:
:
:
:
:
:
:
:
:
:
:
:
:
:
:
:
:
:
:
:
:
:
:
:
:
:
:
:
:
:
:
:
:
:
1768
1768
716800
24860
24860
716800
716800
2387968
2387968
21884928
21884928
716800
agrsmsvc
True
0
System.Diagnostics.ProcessStartInfo
{1964, 1000}
21884928
False
57344
Here are two things to note: if the call does not return anything at all, then there are probably no Notepad processes
running. Before you make the effort and use Where-Object to filter results, you should make sure the initial cmdlet
has no parameter to filter the information you want right away. For example, Get-Process already supports a
parameter called -name, which will return only the processes you specify:
Get-Process -name notepad
Handles NPM(K)
PM(K)
WS(K) VM(M)
CPU(s)
Id ProcessName
------68
68
-----4
4
----1636
1632
----- ----8744
62
8764
62
-----0,14
0,05
-- ----------7732 notepad
7812 notepad
The only difference with the latter approach: if no Notepad process is running, Get-Process throws an exception,
telling you that there is no such process. If you don't like that, you can always add the parameter -ErrorAction
SilentlyContinue, which will work for all cmdlets and hide all error messages.
When you revisit your Where-Object line, you'll see that your condition is specified in curly brackets after the
cmdlet. The $_ variable contains the current pipeline object. While sometimes the initial cmdlet is able to do the
filtering all by itself (like in the previous example using -name), Where-Object is much more flexible because it can
filter on any piece of information found in an object.
You can use the next one-liner to retrieve all processes whose company name begins with "Micro" and output name,
description, and company name:
Get-Process | Where-Object { $_.company -like 'micro*' } | Select-Object
name, description, company
Name
Description
Company
-------------------conime
Console IME
Microsoft Corporation
dwm
Desktopwindow-Manager
Microsoft Corporation
ehmsas
Media Center Media Status Aggr...
Microsoft Corporation
ehtray
Media Center Tray Applet
Microsoft Corporation
EXCEL
Microsoft Office Excel
Microsoft Corporation
explorer
Windows-Explorer
Microsoft Corporation
GrooveMonitor
GrooveMonitor Utility
Microsoft Corporation
ieuser
Internet Explorer
Microsoft Corporation
iexplore
Internet Explorer
Microsoft Corporation
msnmsgr
Messenger
Microsoft Corporation
notepad
Editor
Microsoft Corporation
notepad
Editor
Microsoft Corporation
sidebar
Windows-Sidebar
Microsoft Corporation
taskeng
Task Scheduler Engine
Microsoft Corporation
WINWORD
Microsoft Office Word
Microsoft Corporation
wmpnscfg
Windows Media Player Network S...
Microsoft Corporation
wpcumi
Windows Parental Control Notif...
Microsoft Corporation
Since you will often need conditions in a pipeline, there is an alias for Where-Object: "?". So, instead of WhereObject, you can also use "?'". However, it does make your code a bit unreadable:
# The two following instructions return the same result: all running services
Get-Service | Foreach-Object {$_.Status -eq 'Running' }
Get-Service | ? {$_.Status -eq 'Running' }
If-ElseIf-Else
Where-object works great in the pipeline, but it is inappropriate if you want to make longer code segments
dependent on meeting a condition. Here, the If..ElseIf..Else statement works much better. In the simplest case, the
statement will look like this:
If (condition) {# If the condition applies, this code will be executed}
The condition must be enclosed in parentheses and follow the keyword If. If the condition is met, the code in the
curly brackets after it will be executed, otherwise, it will not. Try it out:
If ($a -gt 10) { "$a is larger than 10" }
It's likely, though, that you won't (yet) see a result. The condition was not met, and so the code in the curly brackets
wasn't executed. To get an answer, you can make sure that the condition is met:
$a = 11
if ($a -gt 10) { "$a is larger than 10" }
11 is larger than 10
Now, the comparison is true, and the If statement ensures that the code in the curly brackets will return a result. As it
is, that clearly shows that the simplest If statement usually doesn't suffice in itself, because you would like to always
get a result, even when the condition isn't met. You can expand the If statement with Else to accomplish that:
if ($a -gt 10)
{
"$a is larger than 10"
}
else
{
"$a is less than or equal to 10"
}
Now, the code in the curly brackets after If is executed if the condition is met. However, if the preceding condition
isnt true, the code in the curly brackets after Else will be executed. If you have several conditions, you may insert as
many ElseIf blocks between If and Else as you like:
if ($a -gt 10)
{
"$a is larger than 10"
}
elseif ($a -eq 10)
{
The If statement here will always execute the code in the curly brackets after the condition that is met. The code
after Else will be executed when none of the preceding conditions are true. What happens if several conditions are
true? Then the code after the first applicable condition will be executed and all other applicable conditions will be
ignored.
if ($a -gt 10)
{
"$a is larger than 10"
}
elseif ($a -eq 10)
{
"$a is exactly 10"
}
elseif ($a ge 10)
{
"$a is larger than or equal to 10"
}
else
{
"$a is smaller than 10"
}
The fact is that the If statement doesn't care at all about the condition that you state. All that the If statement
evaluates is $true or $false. If condition evaluates $true, the code in the curly brackets after it will be executed,
otherwise, it will not. Conditions are only a way to return one of the requested values $true or $false. But the value
could come from another function or from a variable:
# Returns True from 14:00 on, otherwise False:
function isAfternoon { (get-date).Hour -gt 13 }
isAfternoon
True
# Result of the function determines which code the If statement executes:
if (isAfternoon) { "Time for break!" } else { "Its still early." }
Time for break!
This example shows that the condition after If must always be in parentheses, but it can also come from any source
as long as it is $true or $false. In addition, you can also write the If statement in a single line. If you'd like to execute
more than one command in the curly brackets without having to use new lines, then you should separate the
commands with a semi-colon ";".
Switch
If you'd like to test a value against many comparison values, the If statement can quickly become unreadable. The
Switch code is much cleaner:
# Test a value
$value = 1
if ($value -eq
{
" Number 1"
}
elseif ($value
{
" Number 2"
}
elseif ($value
{
" Number 3"
}
Number 1
# Test
$value
switch
{
1 {
2 {
3 {
}
Number
-eq 2)
-eq 3)
This is how you can use the Switch statement: the value to switch on is in the parentheses after the Switch keyword.
That value is matched with each of the conditions on a case-by-case basis. If a match is found, the action associated
with that condition is then performed. You can use the default comparison operator, the eq operator, to verify
equality.
Number from 7 to 10
The code block {$_ -le 5} includes all numbers less than or equal to 5.
The code block {(($_ -gt 6) -and ($_ -le 10))} combines two conditions and results in true if the number is
either larger than 6 or less than-equal to 10. Consequently, you can combine any PowerShell statements in
the code block and also use the logical operators listed in Table 7.2.
Here, you can use the initial value stored in $_ for your conditions, but because $_ is generally available anywhere
in the Switch block, you could just as well have put it to work in the result code:
$value = 8
switch ($value)
{
# The initial value (here it is in $value) is available in the variable $_:
{$_ -le 5} { "$_ is a number from 1 to 5" }
6 { "Number 6" }
{(($_ -gt 6) -and ($_ -le 10))} { "$_ is a number from 7 to 10" }
}
8 is a number from 7 to 10
No Applicable Condition
In contrast to If, the Switch clause will execute all code for all conditions that are met. So, if there are two conditions
that are both met, Switch will execute them both whereas If had only executed the first matching condition code. To
change the Switch default behavior and make it execute only the first matching code, you should use the statement
continue inside of a code block.
If no condition is met, the If clause will provide the Else statement, which serves as a catch-all. Likewise, Switch has
a similar catch-all called default:
$value = 50
switch ($value)
{
{$_ -le 5} { "$_is a number from 1 to 5" }
6 { "Number 6" }
{(($_ -gt 6) -and ($_ -le 10))} { "$_ is a number from 7 to 10" }
# The code after the next statement will be executed if no other condition
has been met:
default {"$_ is a number outside the range from 1 to 10" }
}
50 is a number outside the range from 1 to 10
Consequently, all applicable conditions will ensure that the following code is executed. So in some circumstances,
you may get more than one result.
Try out that example, but assign 50.0 to $value. In this case, you'll get just two results instead of three. Do you know
why? That's right: the third condition is no longer fulfilled because the number in $value is no longer an integer
number. However, the other two conditions continue to remain fulfilled.
If you'd like to receive only one result, you can add the continue or break statement to the code.
$value = 50
switch ($value)
{
50 { "the number 50"; break }
{$_ -gt 10} {"larger than 10"; break}
{$_ -is [int]} {"Integer number"; break}
}
The number 50
The keyword break tells PowerShell to leave the Switch construct. In conditions, break and continue are
interchangeable. In loops, they work differently. While breaks exits a loop immediately, continue would only exit
the current iteration.
Case Sensitivity
Since the eq comparison operator doesn't distinguish between lower and upper case, case sensitivity doesn't play a
role in comparisons. If you want to distinguish between them, you can use the case option. Working behind the
scenes, it will replace the eq comparison operator with ceq, after which case sensitivity will suddenly become
crucial:
$action = "sAVe"
switch -case ($action)
{
"save" { "I save..." }
"open" { "I open..." }
"print" { "I print..." }
Default { "Unknown command" }
}
Unknown command
Wildcard Characters
In fact, you can also exchange a standard comparison operator for like and match operators and then carry out
wildcard comparisons. Using the wildcard option, you can activate the -like operator, which is conversant, among
others, with the "*" wildcard character:
$text = "IP address: 10.10.10.10"
switch -wildcard ($text)
{
"IP*" { "The text begins with IP: $_" }
"*.*.*.*" { "The text contains an IP address string pattern: $_" }
"*dress*" { "The text contains the string 'dress' in arbitrary locations:
$_" }
}
The text begins with IP: IP address: 10.10.10.10
The text contains an IP address string pattern: IP address: 10.10.10.10
The text contains the string 'dress' in arbitrary locations: IP address:
10.10.10.10
Regular Expressions
Simple wildcard characters ca not always be used for recognizing patterns. Regular expressions are much more
efficient. But they assume much more basic knowledge, which is why you should take a peek ahead at Chapter 13,
discussion of regular expression in greater detail.
With the -regex option, you can ensure that Switch uses the match comparison operator instead of eq, and thus
employs regular expressions. Using regular expressions, you can identify a pattern much more precisely than by
using simple wildcard characters. But that's not all!. As in the case with the match operator, you will usually get
back the text that matches the pattern in the $matches variable. This way, you can even parse information out of the
text:
$text = "IP address: 10.10.10.10"
switch -regex ($text)
{
"^IP" { "The text begins with IP: $($matches[0])" }
"\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}" { "The text contains an IP address
string pattern: $($matches[0])" }
"\b.*?dress.*?\b" { " The text contains the string 'dress' in arbitrary
locations: $($matches[0])" }
}
The text begins with IP: IP
The text contains an IP address string pattern: 10.10.10.10
The text contains the string 'dress' in arbitrary locations: IP address
The result of the match comparison with the regular expression is returned in $matches, a hash table with each
result, because regular expressions can, depending on their form, return several results. In this example, only the
first result you got by using $matches[0] should interest you.. The entire expression is embedded in $(...) to ensure
that this result appears in the output text.
There you have it: Switch will accept not only single values, but also entire arrays and collections. As such, Switch
would be an ideal candidate for evaluating results on the PowerShell pipeline because the pipeline character ("|") is
used to forward results as arrays or collections from one command to the next.
The next line queries Get-Process for all running processes and then pipes the result to a script block (& {...}). In the
script block, Switch will evaluate the result of the pipeline, which is available in $input. If the WS property of a
process is larger than one megabyte, this process is output. Switch will then filter all of the processes whose WS
property is less than or equal to one megabyte:
Get-Process | & { Switch($input) { {$_.WS -gt 1MB} { $_ }}}
However, this line is extremely hard to read and seems complicated. You can formulate the condition in a much
clearer way by using Where-Object:
Get-Process | Where-Object { $_.WS -gt 1MB }
This variant also works more quickly because Switch had to wait until the pipeline has collected the entire results of
the preceding command in $input. In Where-Object, it processes the results of the preceding command precisely
when the results are ready. This difference is especially striking for elaborate commands:
# Switch returns all files beginning with "a":
Dir | & { switch($Input) { {$_.name.StartsWith("a")} { $_ } }}
# But it doesn't do so until Dir has retrieved all data, and that can take a
long time:
Dir -Recurse | & { switch($Input) { {$_.name.StartsWith("a")} { $_ } }}
# Where-Object processes the incoming results immediately:
Dir -recurse | Where-Object { $_.name.StartsWith("a") }
# The alias of Where-Object ("?") works exactly the same way:
Dir -recurse | ? { $_.name.StartsWith("a") }
Summary
Intelligent decisions are based on conditions, which in their simplest form can be reduced to plain Yes or No
answers. Using the comparison operators listed in Table 7.1, you can formulate such conditions and even combine
these with the logical operators listed in Table 7.2 to form complex queries.
The simple Yes/No answers of your conditions will determine whether particular PowerShell instructions can carried
out or not. In their simplest form, you can use the Where-Object cmdlet in the pipeline. It functions there like a
filter, allowing only those results through the pipeline that correspond to your condition.
If you would like more control, or would like to execute larger code segments independently of conditions, you can
use the If statement, which evaluates as many different conditions as you wish and, depending on the result, will
then execute the allocated code. This is the typical "If-Then" scenario: if certain conditions are met, then certain
code segments will be executed.
An alternative to the If statement is the Switch statement. Using it, you can compare a fixed initial value with various
possibilities. Switch is the right choice when you want to check a particular variable against many different possible
values.
Loops repeat PowerShell code and are the heart of automation. In this chapter, you will learn the PowerShell loop
constructs.
Topics Covered:
ForEach-Object
o Invoking Methods
Foreach
Do and While
o Continuation and Abort Conditions
o Using Variables as Continuation Criteria
o Endless Loops without Continuation Criteria
For
o For Loops: Just Special Types of the While Loop
o Unusual Uses for the For Loop
Switch
Exiting Loops Early
o Continue: Skipping Loop Cycles
o Nested Loops and Labels
Summary
ForEach-Object
Many PowerShell cmdlets return more than one result object. You can use a Pipeline loop: foreach-object to process
them all one after another.. In fact, you can easily use this loop to repeat the code multiple times. The next line will
launch 10 instances of the Notepad editor:
1..10 | Foreach-Object { notepad }
Foreach-Object is simply a cmdlet, and the script block following it really is an argument assigned to ForeachObject:
1..10 | Foreach-Object -process { notepad }
Inside of the script block, you can execute any code. You can also execute multiple lines of code. You can use a
semicolon to separate statements from each other in one line:
1..10 | Foreach-Object { notepad; "Launching Notepad!" }
In PowerShell editor, you can use multiple lines:
1..10 | Foreach-Object { notepad "Launching Notepad!" }
The element processed by the script block is available in the special variable $_:
1..10 | Foreach-Object { "Executing $_. Time" }
Most of the time, you will not feed numbers into Foreach-Object, but instead the results of another cmdlet. Have a
look:
Get-Process | Foreach-Object { 'Process {0} consumes {1} seconds CPU time' -f $_.Name, $_.CPU }
Invoking Methods
Because ForEach-Object will give you access to each object in a pipeline, you can invoke methods of these objects.
In Chapter 7, you learned how to take advantage of this to close all instances of the Notepad. This will give you
much more control. You could use Stop-Process to stop a process. But if you want to close programs gracefully, you
should provide the user with the opportunity to save unsaved work by also invoking the method
CloseMainWindow(). The next line closes all instances of Notepad windows. If there is unsaved data, a dialog
appears asking the user to save it first:
Get-Process notepad | ForEach-Object { $_.CloseMainWindow() }
You can also solve more advanced problems. If you want to close only those instances of Notepad that were running
for more than 10 minutes, you can take advantage of the property StartTime. All you needed to do is calculate the
cut-off date using New-Timespan. Let's first get a listing that tells you how many minutes an instance of Notepad has
been running:
Get-Process notepad | ForEach-Object {
$info = $_ | Select-Object Name, StartTime, CPU, Minutes
$info.Minutes = New-Timespan $_.StartTime | Select-Object -expandproperty
TotalMinutes
$info
}
Check out a little trick. In the above code, the script block creates a copy of the incoming object using Select-Object,
which selects the columns you want to view. We specified an additional property called Minutes to display the
running minutes, which are not part of the original object. Select-Object will happily add that new property to the
object. Next, we can fill in the information into the Minutes property. This is done using New-Timespan, which
calculates the time difference between now and the time found in StartTime. Don't forget to output the $info object
at the end or the script block will have no result.
To kill only those instances of Notepad that were running for more than 10 minutes, you will need a condition:
Get-Process Notepad | Foreach-Object {
$cutoff = ( (Get-Date) - (New-Timespan -minutes 10) )
if ($_.StartTime -lt $cutoff) { $_ }
}
This code would only return Notepad processes running for more than 10 minutes and you could pipe the result into
Stop-Process to kill those.
What you see here is a Foreach-Object loop with an If condition. This is exactly what Where-Object does so if you
need loops with conditions to filter out unwanted objects, you can simplify:
Get-Process Notepad | Where-Object {
$cutoff = ( (Get-Date) - (New-Timespan -minutes 10) )
$_.StartTime -lt $cutoff
}
Foreach
There is another looping construct called Foreach. Don't confuse this with the Foreach alias, which represents
Foreach-Object. So, if you see a Foreach statement inside a pipeline, this really is a Foreach-Object cmdlet. The
true Foreach loop is never used inside the pipeline. Instead, it can only live inside a code block.
While Foreach-Object obtains its entries from the pipeline, the Foreach statement iterates over a collection of
objects:
# ForEach-Object lists each element in a pipeline:
Dir C:\ | ForEach-Object { $_.name }
# Foreach loop lists each element in a colection:
foreach ($element in Dir C:\) { $element.name }
The true Foreach statement does not use the pipeline architecture. This is the most important difference because it
has very practical consequences. The pipeline has a very low memory footprint because there is always only one
object travelling the pipeline. In addition, the pipeline processes objects in real time. That's why it is safe to process
even large sets of objects. The following line iterates through all files and folders on drive c:\. Note how results are
returned immediately:
Do and While
Do and While generate endless loops. Endless loops are a good idea if you don't know exactly how many times the
loop should iterate. You must set additional abort conditions to prevent an endless loop to really run endlessly. The
loop will end when the conditions are met.
This loop asks the user for his home page Web address. While is the criteria that has to be met at the end of the loop
so that the loop can be iterated once again. In the example, -like is used to verify whether the input matches the
www.*.* pattern. While that's only an approximate verification, it usually suffices. You could also use regular
expressions to refine your verification. Both procedures will be explained in detail in Chapter 13.
This loop is supposed to re-iterate only if the input is false. That's why "!" is used to simply invert the result of the
condition. The loop will then be iterated until the input does not match a Web address.
In this type of endless loop, verification of the loop criteria doesn't take place until the end. The loop will go through
its iteration at least once because you have to query the user at least once before you can check the criteria.
There are also cases in which the criteria needs to be verified at the beginning and not at the end of the loop. An
example would be a text file that you want to read one line at a time. The file could be empty and the loop should
check before its first iteration whether there's anything at all to read. To accomplish this, just put the While statement
and its criteria at the beginning of the loop (and leave out Do, which is no longer of any use):
# Open a file for reading:
$file = [system.io.file]::OpenText("C:\autoexec.bat")
# Continue loop until the end of the file has been reached:
while (!($file.EndOfStream)) {
# Read and output current line from the file:
$file.ReadLine()
}
# Close file again:
$file.close
For
You can use the For loop if you know exactly how often you want to iterate a particular code segment. For loops are
counting loops. You can specify the number at which the loop begins and at which number it will end to define the
number of iterations, as well as which increments will be used for counting. The following loop will output a sound
at various 100ms frequencies (provided you have a soundcard and the speaker is turned on):
# Output frequencies from 1000Hz to 4000Hz in 300Hz increments
for ($frequency=1000; $frequency le 4000; $frequency +=300) {
[System.Console]::Beep($frequency,100)
}
These three expressions can be used to initialize a control variable, to verify whether a final value is achieved, and to
change a control variable with a particular increment at every iteration of the loop. Of course, it is entirely up to you
whether you want to use the For loop solely for this purpose.
A For loop can become a While loop if you ignore the first and the second expression and only use the second
expression, the continuation criteria:
# First expression: simple While loop:
$i = 0
while ($i lt 5) {
$i++
$i
}
1
2
3
4
5
# Second expression: the For loop behaves like the While loop:
$i = 0
for (;$i -lt 5;) {
$i++
$i
}
1
2
3
4
5
In the first expression, the $input variable is set to an empty string. The second expression checks whether a valid
Web address is in $input. If it is, it will use "!" to invert the result so that it is $true if an invalid Web address is in
$input. In this case, the loop is iterated. In the third expression, the user is queried for a Web address. Nothing more
needs to be in the loop. In the example, an explanatory text is output.
In addition, the line-by-line reading of a text file can be implemented by a For loop with less code:
for ($file = [system.io.file]::OpenText("C:\autoexec.bat");
!($file.EndOfStream); `
$line = $file.ReadLine())
{
# Output read line:
$line
}
$file.close()
REM Dummy file for NTVDM
In this example, the first expression of the loop opened the file so it could be read. In the second expression, a check
is made whether the end of the file has been reached. The "!" operator inverts the result again. It will return $true if
the end of the file hasn't been reached yet so that the loop will iterate in this case. The third expression reads a line
from the file. The read line is then output in the loop.
The third expression of the For loop is executed before every loop cycle. In the example, the current line from the
text file is read. This third expression is always executed invisibly, which means you can't use it to output any text.
So, the contents of the line are output within the loop.
Switch
Switch is not only a condition, but also functions like a loop. That makes Switch one of the most powerful statements
in PowerShell. Switch works almost exactly like the Foreach loop. Moreover, it can evaluate conditions. For a quick
demonstration, take a look at the following simple Foreach loop:
$array = 1..5
The control variable that returns the current element of the array for every loop cycle cannot be named for Switch, as
it can for Foreach, but is always called $_. The external part of the loop functions in exactly the same way. Inside
the loop, there's an additional difference: while Foreach always executes the same code every time the loop cycles,
Switch can utilize conditions to execute optionally different code for every loop. In the simplest case, the Switch
loop contains only the default statement. The code that is to be executed follows it in curly brackets.
That means Foreach is the right choice if you want to execute exactly the same statements for every loop cycle. On
the other hand, if you'd like to process each element of an array according to its contents, it would be preferable to
use Switch:
$array = 1..5
switch ($array)
{
1 { "The number 1" }
{$_ -lt 3} { "$_ is less than 3" }
{$_ % 2} { "$_ is odd" }
Default { "$_ is even" }
}
The number 1
1 is less than 3
1 is odd
2 is less than 3
3 is odd
4 is even
5 is odd
If you're wondering why Switch returned this result, take a look at Chapter 7 where you'll find an explanation of how
Switch evaluates conditions. What's important here is the other, loop-like aspect of Switch.
if not, the inner loop will then invoke Continue skip all instances not beginning with "a." The result is a list of all
services, user accounts, and running processes that begin with "a":
foreach ($wmiclass in "Win32_Service","Win32_UserAccount","Win32_Process")
{
foreach ($instance in Get-WmiObject $wmiclass) {
if (!(($instance.name.toLower()).StartsWith("a"))) {continue}
"{0}: {1}" f $instance.__CLASS, $instance.name
}
}
Win32_Service: AeLookupSvc
Win32_Service: AgereModemAudio
Win32_Service: ALG
Win32_Service: Appinfo
Win32_Service: AppMgmt
Win32_Service: Ati External Event Utility
Win32_Service: AudioEndpointBuilder
Win32_Service: Audiosrv
Win32_Service: Automatic LiveUpdate Scheduler
Win32_UserAccount: Administrator
Win32_Process: Ati2evxx.exe
Win32_Process: audiodg.exe
Win32_Process: Ati2evxx.exe
Win32_Process: AppSvc32.exe
Win32_Process: agrsmsvc.exe
Win32_Process: ATSwpNav.exe
As expected, the Continue statement in the inner loop has had an effect on the inner loop where the statement was
contained. But how would you change the code if you'd like to see only the first element of all services, user
accounts, and processes that begins with "a"? Actually, you would do almost the exact same thing, except now
Continue would need to have an effect on the outer loop. Once an element was found that begins with "a," the outer
loop would continue with the next WMI class:
:WMIClasses foreach ($wmiclass in
"Win32_Service","Win32_UserAccount","Win32_Process") {
:ExamineClasses foreach ($instance in Get-WmiObject $wmiclass) {
if (($instance.name.toLower()).StartsWith("a")) {
"{0}: {1}" f $instance.__CLASS, $instance.name
continue WMIClasses
}
}
}
Win32_Service: AeLookupSvc
Win32_UserAccount: Administrator
Win32_Process: Ati2evxx.exe
Summary
The cmdlet ForEach-Object will give you the option of processing single objects of the PowerShell pipeline, such as
to output the data contained in object properties as text or to invoke methods of the object. Foreach is a similar type
of loop whose contents do not come from the pipeline, but from an array or a collection.
In addition, there are endless loops that iterate a code block until a particular condition is met. The simplest type is
While, which checks its continuation criteria at the beginning of the loop. If you want to do the checking at the end
of the loop, choose DoWhile. The For loop is an extended While loop, because it can count loop cycles and
automatically terminate the loop after a designated number of iterations.
This means that For is best suited for loops which need to be counted or must complete a set number of iterations.
On the other hand, Do...While and While are designed for loops that have to be iterated as long as the respective
situation and running time conditions require it.
Finally, Switch is a combined Foreach loop with integrated conditions so that you can immediately implement
different actions independently of the read element. Moreover, Switch can step through the contents of text files lineby-line and evaluate even log files of substantial size.
All loops can exit ahead of schedule with the help of Break and skip the current loop cycle with the help of
Continue. In the case of nested loops, you can assign an unambiguous name to the loops and then use this name to
apply Break or Continue to nested loops.
Functions work pretty much like macros. As such, you can attach a script block to a name to create your own new
commands.
Functions provide the interface between your code and the user. They can define parameters, parameter types, and
even provide help, much like cmdlets.
In this chapter, you will learn how to create your own functions.
Topics Covered:
Once you enter this code in your script editor and run it dot-sourced, PowerShell learned a new command called
Get-InstalledSoftware. If you saved your code in a file called c:\somescript.ps1, you will need to run it like this:
. 'c:\somescript.ps1'
If you don't want to use a script, you can also enter a function definition directly into your interactive PowerShell
console like this:
function Get-InstalledSoftware {
However, defining functions in a script is a better approach because you won't want to enter your functions
manually all the time. Running a script to define the functions is much more practical. You may want to enable
script execution if you are unable to run a script because of your current ExecutionPolicy settings:
Set-ExecutionPolicy -Scope CurrentUser -ExecutionPolicy RemoteSigned -force
Once you defined your function, you can even use code completion. If you enter "Get-Ins" and then press TAB,
PowerShell will complete your function name. Of course, the new command Get-InstalledSoftware won't do
anything yet. The script block you attached to your function name was empty. You can add whatever code you want
to run to make your function do something useful. Here is the beef to your function that makes it report installed
software:
function Get-InstalledSoftware {
$path =
'Registry::HKEY_LOCAL_MACHINE\Software\Microsoft\Windows\CurrentVersion\Unins
tall\*'
Get-ItemProperty -path $path |
Where-Object { $_.DisplayName -ne $null } |
Select-Object DisplayName, DisplayVersion, UninstallString |
Sort-Object DisplayName
}
When you run it, it will return a sorted list of all the installed software packages, their version, and their uninstall
information:
PS > Get-InstalledSoftware
DisplayName
UninstallString
-------------64 Bit HP CIO Components Installer
/I{5737101A-27C4-40...
Apple Mobile Device Support
/I{963BFE7E-C350-43...
Bonjour
/X{E4F5E48E-7155-4C...
(...)
DisplayVersion
--------------
-----------
8.2.1
MsiExec.exe
3.3.0.69
MsiExec.exe
2.0.4.0
MsiExec.exe
As always, information may be clipped. You can pipe the results to any of the formatting cmdlets to change because
the information returned by your function will behave just like information returned from any cmdlet.
Note the way functions return their results: anything you leave behind will be automatically assigned as return value.
If you leave behind more than one piece of information, it will be returned as an array:
PS > function test { "One" }
PS > test
One
PS > function test { "Zero", "One", "Two", "Three" }
PS > test
Zero
One
Two
Three
PS > $result = test
PS > $result[0]
Zero
PS > $result[1,2]
One
Two
PS > $result[-1]
Three
Your new command Speak-Text converts (English) text to spoken language. It accesses an internal Text-to-SpeechAPI, so you can now try this:
Speak-Text 'Hello, I am hungry!'
Since the function Speak-Text now supports a parameter, it is easy to submit additional information to the function
code. PowerShell will take care of parameter parsing, and the same rules apply that you already know from cmdlets.
You can submit arguments as named parameters, as abbreviated named parameters, and as positional parameters:
Speak-Text 'This is positional'
Speak-Text -text 'This is named'
Speak-Text -t 'This is abbreviated named'
To submit more than one parameter, you can add more parameters as comma-separated list. Let's add some
parameters to Get-InstalledSoftware to make it more useful. Here, we add parameters to select the product and when
it was installed:
function Get-InstalledSoftware {
param(
$name = '*',
$days = 2000
)
$cutoff = (Get-Date) - (New-TimeSpan -days $days)
$cutoffstring = Get-Date -date $cutoff -format 'yyyyMMdd'
$path =
'Registry::HKEY_LOCAL_MACHINE\Software\Microsoft\Windows\CurrentVersion\Unins
tall\*'
$column_days = @{
Name='Days'
Expression={
if ($_.InstallDate) {
(New-TimeSpan ([DateTime]::ParseExact($_.InstallDate, 'yyyyMMdd',
$null))).Days
} else { 'n/a' }
}
}
Get-ItemProperty -path $path |
Where-Object { $_.DisplayName -ne $null } |
Where-Object { $_.DisplayName -like $name } |
Where-Object { $_.InstallDate -gt $cutoffstring } |
Select-Object DisplayName, $column_Days, DisplayVersion |
Sort-Object DisplayName
}
Now, Get-InstalledSoftware supports two optional parameters called -Name and -Days. You do not have to submit
them since they are optional. If you don't, they are set to their default values. So when you run GetInstalledSoftware, you will get all software installed within the past 2,000 days. If you want to only find software
with "Microsoft" in its name that was installed within the past 180 days, you can submit parameters:
PS > Get-InstalledSoftware -name *Microsoft* -days 180 | Format-Table AutoSize
DisplayName
Days DisplayVersion
-------------- -------------Microsoft .NET Framework 4 Client Profile
38 4.0.30319
Microsoft Antimalware
119 3.0.8107.0
Microsoft Antimalware Service DE-DE Language Pack 119 3.0.8107.0
Microsoft Security Client
119 2.0.0657.0
Microsoft Security Client DE-DE Language Pack
119 2.0.0657.0
Microsoft Security Essentials
119 2.0.657.0
Microsoft SQL Server Compact 3.5 SP2 x64 ENU
33 3.5.8080.0
param(
$dollar,
$rate=1.37
)
$dollar * $rate
}
Since -rate is an optional parameter with a default value, there is no need for you to submit it unless you want to
override the default value:
PS > ConvertTo-Euro -dollar 200 -rate 1.21
242
So, what happens when the user does not submit any parameter since -dollar is optional as well? Well, since you did
not submit anything, you get back nothing.
This function can only make sense if there was some information passed to $dollar, which is why this parameter
needs to be mandatory. Here is how you declare it mandatory:
function ConvertTo-Euro {
param(
[Parameter(Mandatory=$true)]
$dollar,
$rate=1.37
)
$dollar * $rate
}
This works because PowerShell will ask for it when you do not submit the -dollar parameter:
PS > ConvertTo-Euro -rate 6.7
cmdlet ConvertTo-Euro at command pipeline position 1
Supply values for the following parameters:
dollar: 100
100100100100100100100
However, the result looks strange because when you enter information via a prompt, PowerShell will treat it as
string (text) information, and when you multiply texts, they are repeated. So whenever you declare a parameter as
mandatory, you are taking the chance that the user will omit it and gets prompted for it. So, you always need to
make sure that you declare the target type you are expecting:
function ConvertTo-Euro {
param(
[Parameter(Mandatory=$true)]
[Double]
$dollar,
$rate=1.37
)
$dollar * $rate
}
}
}
Note that the comment-based Help block may not be separated by more than one blank line if you place it above the
function. If you did everything right, you will now be able to get the same rich help like with cmdlets after running
the code:
PS > ConvertTo-Euro -?
NAME
ConvertTo-Euro
SYNOPSIS
Converts Dollar to Euro
SYNTAX
ConvertTo-Euro [-dollar] <Double> [[-rate] <Object>] [-pretty]
[<CommonParameters>]
DESCRIPTION
Takes dollars and calculates the value in Euro by applying an exchange
rate
RELATED LINKS
REMARKS
To see the examples, type: "get-help ConvertTo-Euro -examples".
for more information, type: "get-help ConvertTo-Euro -detailed".
for technical information, type: "get-help ConvertTo-Euro -full".
true
1
false
-rate <Object>
the exchange rate. The default value is set to 1.37.
Required?
Position?
Default value
Accept pipeline input?
Accept wildcard characters?
false
2
false
-pretty [<SwitchParameter>]
Required?
Position?
Default value
Accept pipeline input?
Accept wildcard characters?
false
named
false
function ConvertTo-Euro {
param(
[Parameter(Mandatory=$true, ValueFromPipeline=$true)]
[Double]
$dollar,
$rate=1.37,
[switch]
$pretty
)
...
By adding ValueFromPipeline=$true, you are telling PowerShell that the parameter -dollar is to receive incoming
pipeline input. When you rerun the script and then try the pipeline again, there are no more exceptions. Your
function will only process the last incoming result, though:
PS > 1..10 | ConvertTo-Euro
13,7
This is because functions will by default execute all code at the end of a pipeline. If you want the code to process
each incoming pipeline data, you must assign the code manually to a process script block or rename your function
into a filter (by exchanging the keyword function by filter). Filters will by default execute all code in a process
block.
Here is how you move the code into a process block to make a function process all incoming pipeline values:
<#
.SYNOPSIS
Converts Dollar to Euro
.DESCRIPTION
Takes dollars and calculates the value in Euro by applying an exchange
rate
.PARAMETER dollar
the dollar amount. This parameter is mandatory.
.PARAMETER rate
the exchange rate. The default value is set to 1.37.
.EXAMPLE
ConvertTo-Euro 100
converts 100 dollars using the default exchange rate and positional
parameters
.EXAMPLE
ConvertTo-Euro 100 -rate 2.3
converts 100 dollars using a custom exchange rate
#>
function ConvertTo-Euro {
param(
[Parameter(Mandatory=$true, ValueFromPipeline=$true)]
[Double]
$dollar,
$rate = 1.37,
[switch]
$pretty
)
begin {"starting..."}
process {
$result = $dollar * $rate
if ($pretty) {
'${0:0.00} equals EUR{1:0.00} at a rate of {2:0:0.00}' -f
$dollar, $result, $rate
} else {
$result
}
}
end { "Done!" }
}
As you can see, your function code is now assigned to one of three special script blocks: begin, process, and end.
Once you add one of these blocks, no code will exist outside of any one of these three blocks anymore.
You can also insert information into the console screen buffer. This only works with true consoles so you cannot use
this type of prompt in non-console editors, such as PowerShell ISE.
function prompt
{
Write-Host ("PS " + $(get-location) +">") -nonewline -foregroundcolor
Green
" "
$winHeight = $Host.ui.rawui.WindowSize.Height
$curPos = $Host.ui.rawui.CursorPosition
$newPos = $curPos
$newPos.X = 0
$newPos.Y-=$winHeight
$newPos.Y = [Math]::Max(0, $newPos.Y+1)
$Host.ui.rawui.CursorPosition = $newPos
Write-Host ("{0:D} {0:T}" -f (Get-Date)) -foregroundcolor Yellow
$Host.ui.rawui.CursorPosition = $curPos
}
Another good place for additional information is the console window title bar. Here is a prompt that displays the
current location in the title bar to save room inside the console and still display the current location:
function prompt { $host.ui.rawui.WindowTitle = (Get-Location); "PS> " }
And this prompt function changes colors based on your notebook battery status (provided you have a battery):
function prompt
{
$charge = get-wmiobject Win32_Battery |
Measure-Object -property EstimatedChargeRemaining -average |
Select-Object -expandProperty Average
if ($charge -lt 25)
{
$color = "Red"
} elseif ($charge -lt 50)
{
$color = "Yellow"
} else
{
$color = "White"
}
$prompttext = "PS {0} ({1}%)>" f (get-location), $charge
Write-Host $prompttext -nonewline -foregroundcolor $color
" "
}
Summary
You can use functions to create your very own new cmdlets. In its most basic form, functions are called script
blocks, which execute code whenever you enter the assigned name. That's what distinguishes functions from aliases.
An alias serves solely as a replacement for another command name. As such, a function can execute whatever code
you want.
PBy adding parameters, you can provide the user with the option to submit additional information to your function
code. Parameters can do pretty much anything that cmdlet parameters can do. They can be mandatory, optional,
have a default value, or a special data type. You can even add Switch parameters to your function.
If you want your function to work as part of a PowerShell pipeline, you will need to declare the parameter that
should accept pipeline input from upstream cmdlets. You will also need to move the function code into a process
block so it gets executed for each incoming result.
You can play with many more parameter attributes and declarations. Try this to get a complete overview:
Help advanced_parameter
PowerShell can be used interactively and in batch mode. All the code that you entered and tested interactively can
also be stored in a script file. When you run the script file, the code inside is executed from top to bottom, pretty
much like if you had entered the code manually into PowerShell.
So script files are a great way of automating complex tasks that consist of more than just one line of code. Scripts
can also serve as a repository for functions you create, so whenever you run a script, it defines all the functions you
may need for your daily work.
You can even set up a so called "profile" script which runs automatically each time you launch PowerShell. A
profile script is used to set up your personal PowerShell environment. It can set colors, define the prompt, and load
additional PowerShell modules and snapins.
Topics Covered:
Creating a Script
o Launching a Script
o Execution Policy - Allowing Scripts to Run
Table 10.1: Execution policy setting options
Invoking Scripts like Commands
Parameters: Passing Arguments to Scripts
o Scopes: Variable Visibility
o Profile Scripts: Automatic Scripts
o Signing Scripts with Digital Signatures
o Finding Certificates
o Creating/Loading a New Certificates
Creating Self-Signed Certificates
o Making a Certificate "Trustworthy"
o Signing PowerShell Scripts
o Checking Scripts
o Table 10.3: Status reports of signature validation and their causes
o Summary
Creating a Script
A PowerShell script is a plain text file with the extension ".ps1". You can create it with any text editor or
use specialized PowerShell editors like the built-in "Integrated Script Environment" called "ise", or
commercial products like "PowerShell Plus".
You can place any PowerShell code inside your script. When you save the script with a generic text editor,
make sure you add the file extension ".ps1".
If your script is rather short, you could even create it directly from within the console by redirecting the
script code to a file:
' "Hello world" ' > $env:temp\myscript.ps1
To save multiple lines to a script file using redirection, use "here-strings":
@'
$cutoff = (Get-Date) - (New-Timespan -hours 24)
$filename = "$env:temp\report.txt"
Get-EventLog -LogName System -EntryType Error,Warning -After $cutoff |
Format-Table -AutoSize |
Out-File $filename -width 10000
Invoke-Item $filename
'@ > $env:temp\myscript.ps1
Launching a Script
To actually run your script, you need to either call the script from within an existing PowerShell window,
or prepend the path with "powershell.exe". So, to run the script from within PowerShell, use this:
& "$env:temp\myscript.ps1"
By prepending the call with "&", you tell PowerShell to run the script in isolation mode. The script runs in
its own scope, and all variables and functions defined by the script will be automatically discarded again
once the script is done. So this is the perfect way to launch a "job" script that is supposed to just "do
something" without polluting your PowerShell environment with left-overs.
By prepending the call with ".", which is called "dot-sourcing", you tell PowerShell to run the script in
global mode. The script now shares the scope with the callers' scope, and functions and variables defined
by the script will still be available once the script is done. Use dot-sourcing if you want to debug a script
(and for example examine variables), or if the script is a function library and you want to use the functions
defined by the script later.
To run a PowerShell script from outside PowerShell, for example from a batch file, use this line:
Powershell.exe -noprofile -executionpolicy Bypass -file
%TEMP%\myscript.ps1
You can use this line within PowerShell as well. Since it always starts a fresh new PowerShell
environment, it is a safe way of running a script in a default environment, eliminating interferences with
settings and predefined or changed variables and functions.
-------
MachinePolicy
Undefined
UserPolicy
Undefined
process
Undefined
CurrentUser
Bypass
LocalMachine
Unrestricted
The first two represent group policy settings. They are set to "Undefined" unless you defined
ExecutionPolicy with centrally managed group policies in which case they cannot be changed manually.
Scope "Process" refers to the current PowerShell session only, so once you close PowerShell, this setting
gets lost. CurrentUser represents your own user account and applies only to you. LocalMachine applies to
all users on your machine, so to change this setting you need local administrator privileges.
The effective execution policy is the first one from top to bottom that is not set to "Undefined". You can
view the effective execution policy like this:
PS > Get-ExecutionPolicy
Bypass
If all execution policies are "Undefined", the effective execution policy is set to "Restricted".
Setting
Restricted
Default
Description
Script execution is absolutely prohibited.
Standard system setting normally corresponding to "Restricted".
Only scripts having valid digital signatures may be executed. Signatures ensure that the
AllSigned
script comes from a trusted source and has not been altered. You'll read more about
signatures later on.
Scripts downloaded from the Internet or from some other "public" location must be signed.
Locally stored scripts may be executed even if they aren't signed. Whether a script is
RemoteSigned "remote" or "local" is determined by a feature called Zone Identifier, depending on
whether your mail client or Internet browser correctly marks the zone. Moreover, it will
work only if downloaded scripts are stored on drives formatted with the NTFS file system.
Unrestricted PowerShell will execute any script.
Table 10.1: Execution policy setting options
Many sources recommend changing the execution policy to "RemoteSigned" to allow scripts. This setting
will protect you from potentially harmful scripts downloaded from the internet while at the same time, local
scripts run fine.
The mechanism behind the execution policy is just an additional safety net for you. If you feel confident
that you won't launch malicious PowerShell code because you carefully check script content before you run
scripts, then it is ok to turn off this safety net altogether by setting the execution policy to "Bypass". This
setting may be required in some corporate scenarios where scripts are run off file servers that may not be
part of your own domain.
If you must ensure maximum security, you can also set execution policy to "AllSigned". Now, every single
script needs to carry a valid digital signature, and if a script was manipulated, PowerShell immediately
refuses to run it. Be aware that this setting does require you to be familiar with digital signatures and
imposes considerable overhead because it requires you to re-sign any script once you made changes.
The changes you made to the "Path" environment variable are temporary and only valid in your current
PowerShell session. To permanently add a folder to that variable, make sure you append the "Path"
environment variable within your special profile script. Since this script runs automatically each time
PowerShell starts, each PowerShell session automatically adds your folder to the search path. You learn
more about profile scripts in a moment.
Now you can run your script and control its behavior by using its parameters. If you copied the script to the
folder that you added to your "Path" environment variable, you can even call your script without a path
name, almost like a new command:
PS > copy-item $env:temp\myscript.ps1
$env:appdata\PSScripts\myscript.ps1
PS > myscript -hours 300
WARNING: The report has been generated here:
C:\Users\w7-pc9\AppData\Local\Temp\report.txt
PS > myscript -hours 300 -show
To learn more about parameters, how to make them mandatory or how to add help to your script, refer to
the previous chapter. Functions and scripts share the same mechanism.
The caller of this script cannot access any function or variable, so the script will not pollute the callers
context with left-over functions or variables - unless you call the script dot-sourced like described earlier in
this chapter.
By prefixing variables or function names with one of the following prefixes, you can change the default
behavior.
Script: use this for "shared" variables.
Global: use this to define variables or functions in the callers' context so they stay visible even after the
script finished
Private: use this to define variables or functions that only exist in the current scope and are invisible to both
super- and subscopes.
The most widely used profile script is your personal profile script for the current PowerShell host. You find
its path in $profile:
PS > $profile
C:\Users\w7pc9\Documents\WindowsPowerShell\Microsoft.PowerShell_profile.ps1
Since this profile script is specific to your current PowerShell host, the path may look different depending
on your host. When you run this command from inside the ISE editor, it looks like this:
PS > $profile
C:\Users\w7pc9\Documents\WindowsPowerShell\Microsoft.PowerShellISE_profile.ps1
If this file exists, PowerShell runs it automatically. To test whether the script exists, use Test-Path. Here is
a little piece of code that creates the profile file if it not yet exists and opens it in notepad so you can add
code to it:
PS > if (!(Test-Path $profile)) { New-Item $profile -Type File -Force |
Out-Null
}; notepad $profile
There are more profile scripts. $profile.CurrentUserAllHosts returns the path to the script file that
automatically runs with all PowerShell hosts, so this is the file to place code in that should execute
regardless of the host you use. It executes for both the PowerShell console and the ISE editor.
$profile.AllUsersCurrentHost is specific to your current host but runs for all users. To create or change this
file, you need local administrator privileges. $profile.AllUsersAllHosts runs for all users on all PowerShell
hosts. Again, you need local administrator privileges to create or change this file.
If you use more than one profile script, their execution order is from "general to specific", so the profile
script defined in $profile executes last (and if there are conflicting settings, overrides all others).
Finding Certificates
To find all codesigning certificates installed in your personal certificate store, use the virtual cert: drive:
Dir cert:\Currentuser\My -codeSigningCert
directory:
Microsoft.PowerShell.Security\Certificate::CurrentUser\My
Thumbprint
---------E24D967BE9519595D7D1AC527B6449455F949C77
Subject
------CN=PowerShellTestCert
The -codeSigningCert parameter ensures that only those certificates are located that are approved for the
intended "code signing" purpose and for which you have a private and secret key.
If you have a choice of several certificates, pick the certificate you want to use for signing by using WhereObject:
$certificate = Dir cert:\CurrentUser\My |
Where-Object { $_.Subject -eq "CN=PowerShellTestCert" }
You can also use low-level -NET methods to open a full-featured selection dialog to pick a certificate:
$Store = New-Object
system.security.cryptography.X509Certificates.x509Store("My",
"CurrentUser")
$store.Open("ReadOnly")
[System.Reflection.Assembly]::LoadWithPartialName("System.Security")
$certificate =
[System.Security.Cryptography.x509Certificates.X509Certificate2UI]::Sel
ectFromCollection($store.certificates, "Your certificates", "Please
select", 0)
$store.Close()
$certificate
Thumbprint
Subject
---------------372883FA3B386F72BCE5F475180CE938CE1B8674 CN=MyCertificate
popd
It will be automatically saved to the \CurrentUser\My certificate store. From this location, you can now
call and use any other certificate:
$name = "PowerShellTestCert"
$certificate = Dir cert:\CurrentUser\My | Where-Object { $_.Subject -eq
"CN=$name"}
Checking Scripts
To check all of your scripts manually and find out whether someone has tampered with them, use GetAuthenticodeSignature:
Summary
PowerShell scripts are plain text files with a ".ps1" file extension. They work like batch files and may
include any PowerShell statements.
To run a script, you need to make sure the execution policy setting is allowing the script to execute. By
default, the execution policy disables all PowerShell scripts.
You can run a script from within PowerShell: specify the absolute or relative path name to the script unless
the script file is stored in a folder that is part of the "Path" environment variable in which case it is
sufficient to specify the script file name.
By running a script "dot-sourced" (prepending the path by a dot and a space), the script runs in the callers'
context. All variables and functions defined in the script will remain intact even once the script finished.
This can be useful for debugging scripts, and it is essential for running "library" scripts that define
functions you want to use elsewhere.
To run scripts from outside PowerShell, call powershell.exe and specify the script path. There are
additional parameters like -noprofile which ensures that the script runs in a default powershell environment
that was not changed by profile scripts.
Digital signatures ensure that a script comes from a trusted source and has not been tampered with. You can
sign scripts and also verify a script signature with Set-AuthenticodeSignature and GetAuthenticodeSignature.
When you design a PowerShell script, there may be situations where you cannot eliminate all possible runtime
errors. If your script maps network drives, there could be a situation where no more drive letters are available, and
when your script performs a remote WMI query, the remote machine may not be available.
In this chapter, you learn how to discover and handle runtime errors gracefully.
Topics Covered:
Suppressing Errors
Handling Errors
o Try/Catch
o Using Traps
Handling Native Commands
o Understanding Exceptions
o Handling Particular Exceptions
o Throwing Your Own Exceptions
o Stepping And Tracing
Summary
Suppressing Errors
Every cmdlet has built-in error handling which is controlled by the -ErrorAction parameter. The default ErrorAction
is "Continue": the cmdlet outputs errors but continues to run.
This default is controlled by the variable $ErrorActionPreference. When you assign a different setting to this
variable, it becomes the new default ErrorAction. The default ErrorAction applies to all cmdlets that do not specify
an individual ErrorAction by using the parameter -ErrorAction.
To suppress error messages, set the ErrorAction to SilentlyContinue. For example, when you search the windows
folder recursively for some files or folder, your code may eventually touch system folders where you have no
sufficient access privileges. By default, PowerShell would then throw an exception but would continue to search
through the subfolders. If you just want the files you can get your hands on and suppress ugly error messages, try
this:
PS> Get-Childitem $env:windir -ErrorAction SilentlyContinue -recurse -filter
*.log
Likewise, if you do not have full local administrator privileges, you cannot access processes you did not start
yourself. Listing process files would produce a lot of error messages. Again, you can suppress these errors to get at
least those files that you are able to access:
Get-Process -FileVersion -ErrorAction SilentlyContinue
Suppress errors with care because errors have a purpose, and suppressing errors will not solve the underlying
problem. In many situations, it is invaluable to receive errors, get alarmed and act accordingly. So only suppress
errors you know are benign.
NOTE: Sometimes, errors will not get suppressed despite using SilentlyContinue. If a cmdlet encounters a serious
error (which is called "Terminating Error"), the error will still appear, and the cmdlet will stop and not continue
regardless of your ErrorAction setting.
Whether or not an error is considered "serious" or "terminating" is solely at the cmdlet authors discretion. For
example, Get-WMIObject will throw a (non-maskable) terminating error when you use -ComputerName to access a
remote computer and receive an "Access Denied" error. If Get-WMIObject encounters an "RPC system not
available" error because the machine you wanted to access is not online, that is considered not a terminating error,
so this type of error would be successfully suppressed.
Handling Errors
To handle an error, your code needs to become aware that there was an error. It then can take steps to respond to that
error. To handle errors, the most important step is setting the ErrorAction default to Stop:
$ErrorActionPreference = 'Stop'
As an alternative, you could add the parameter -ErrorAction Stop to individual cmdlet calls but chances are you
would not want to do this for every single call except if you wanted to handle only selected cmdlets errors. Changing
the default ErrorAction is much easier in most situations.
The ErrorAction setting not only affects cmdlets (which have a parameter -ErrorAction) but also native commands
(which do not have such a parameter and thus can only be controlled via the default setting).
Once you changed the ErrorAction to Stop, your code needs to set up an error handler to become aware of errors.
There is a local error handler (try/catch) and also a global error handler (trap). You can mix both if you want.
Try/Catch
To handle errors in selected areas of your code, use the try/catch statements. They always come as pair and need to
follow each other immediately. The try-block marks the area of your code where you want to handle errors. The
catch-block defines the code that is executed when an error in the try-block occurs.
Take a look at this simple example:
'localhost', '127.0.0.1', 'storage1', 'nonexistent', 'offline' |
ForEach-Object {
try {
Get-WmiObject -class Win32_BIOS -computername $_ -ErrorAction Stop |
Select-Object __Server, Version
}
catch {
Write-Warning "Error occured: $_"
}
}
It takes a list of computer names (or IP addresses) which could also come from a text file (use Get-Content to read a
text file instead of listing hard-coded computer names). It then uses Foreach-Object to feed the computer names into
Get-WMIObject which remotely tries to get BIOS information from these machines.
Get-WMIObject is encapsulated in a try-block and also uses the ErrorAction setting Stop, so any error this cmdlet
throws will execute the catch-block. Inside the catch-block, in this example a warning is outputted. The reason for
the error is available in $_ inside the catch-block.
Try and play with this example. When you remove the -ErrorAction parameter from Get-WMIObject, you will
notice that errors will no longer be handled. Also note that whenever an error occurs in the try-block, PowerShell
jumps to the corresponding catch-block and will not return and resume the try-block. This is why only GetWMIObject is placed inside the try-block, not the Foreach-Object statement. So when an error does occur, the loop
continues to run and continues to process the remaining computers in your list.
The error message created by the catch-block is not yet detailed enough:
WARNING: Error occured: The RPC server is unavailable. (Exception from
HRESULT:0x800706BA)
You may want to report the name of the script where the error occured, and of course you'd want to output the
computer name that failed. Here is a slight variant which accomplishes these tasks. Note also that in this example,
the general ErrorActionPreference was set to Stop so it no longer is necessary to submit the -ErrorAction parameter
to individual cmdlets:
'localhost', '127.0.0.1', 'storage1', 'nonexistent', 'offline' |
ForEach-Object {
try {
$ErrorActionPreference = 'Stop'
$currentcomputer = $_
Get-WmiObject -class Win32_BIOS -computername $currentcomputer
Select-Object __Server, Version
}
catch {
Write-Warning ('Failed to access "{0}" : {1} in "{2}"' -f
$currentcomputer, `
$_.Exception.Message, $_.InvocationInfo.ScriptName)
}
}
catch {
$global:test = $_
Write-Warning ('Failed to access "{0}" : {1} in "{2}"' -f
$currentcomputer, `
$_.Exception.Message, $_.InvocationInfo.ScriptName)
}
}
Then, once the script ran (and encountered an error), check the content of $test:
PS> Get-Member -InputObject $test
TypeName: System.Management.Automation.ErrorRecord
Name
MemberType
Definition
---------------------Equals
Method
bool Equals(System.Object obj)
GetHashCode
Method
int GetHashCode()
GetObjectData
Method
System.Void
GetObjectData(System.Runtime.Seriali...
GetType
Method
type GetType()
ToString
Method
string ToString()
CategoryInfo
Property
System.Management.Automation.ErrorCategoryInfo C...
ErrorDetails
Property
System.Management.Automation.ErrorDetails ErrorD...
Exception
Property
System.Exception Exception {get;}
FullyQualifiedErrorId Property
System.String FullyQualifiedErrorId
{get;}
InvocationInfo
Property
System.Management.Automation.InvocationInfo Invo...
PipelineIterationInfo Property
System.Collections.ObjectModel.ReadOnlyCollectio...
TargetObject
Property
System.Object TargetObject {get;}
PSMessageDetails
ScriptProperty System.Object PSMessageDetails {get=& {
Set-Stri...
As you see, the error information has a number of subproperties like the one used in the example. One of the more
useful properties is InvocationInfo which you can examine like this:
PS> Get-Member -InputObject $test.InvocationInfo
TypeName: System.Management.Automation.InvocationInfo
Name
MemberType Definition
------------- ---------Equals
Method
bool Equals(System.Object obj)
GetHashCode
Method
int GetHashCode()
GetType
Method
type GetType()
ToString
Method
string ToString()
BoundParameters Property
System.Collections.Generic.Dictionary`2[[System.String, m...
CommandOrigin
Property
CommandOrigin ...
ExpectingInput
Property
HistoryId
Property
InvocationName
Property
Line
Property
MyCommand
Property
MyCommand {get;}
OffsetInLine
Property
PipelineLength
Property
PipelinePosition Property
PositionMessage Property
ScriptLineNumber Property
ScriptName
Property
UnboundArguments Property
mscorli...
System.Management.Automation.CommandOrigin
System.Boolean ExpectingInput {get;}
System.Int64 HistoryId {get;}
System.String InvocationName {get;}
System.String Line {get;}
System.Management.Automation.CommandInfo
System.Int32 OffsetInLine {get;}
System.Int32 PipelineLength {get;}
System.Int32 PipelinePosition {get;}
System.String PositionMessage {get;}
System.Int32 ScriptLineNumber {get;}
System.String ScriptName {get;}
System.Collections.Generic.List`1[[System.Object,
It tells you all details about the place the error occured.
Using Traps
If you do not want to focus your error handler on a specific part of your code, you can also use a global error handler
which is called "Trap". Actually, a trap really is almost like a catch-block without a try-block. Check out this
example:
trap {
Write-Warning ('Failed to access "{0}" : {1} in "{2}"' -f
$currentcomputer, `
$_.Exception.Message, $_.InvocationInfo.ScriptName)
continue
}
'localhost', '127.0.0.1', 'storage1', 'nonexistent', 'offline' |
ForEach-Object {
$currentcomputer = $_
Get-WmiObject -class Win32_BIOS -computername $currentcomputer ErrorAction Stop |
Select-Object __Server, Version
}
This time, the script uses a trap at its top which looks almost like the catch-block used before. It does contain one
more statement to make it act like a catch-block: Continue. Without using Continue, the trap would handle the error
but then forward it on to other handlers including PowerShell. So without Continue, you would get your own error
message and then also the official PowerShell error message.
When you run this script, you will notice differences, though. When the first error occurs, the trap handles the error
just fine, but then the script stops. It does not execute the remaining computers in your list. Why?
Whenever an error occurs and your handler gets executed, it continues execution with the next statement following
the erroneous statement - in the scope of the handler. So when you look at the example code, you'll notice that the
error occurred inside the Foreach-Object loop. Whenever your code uses braces, the code inside the braces
resembles a new "territory" or "scope". So the trap did process the first error correctly and then continued with the
next statement in its own scope. Since there was no code following your loop, nothing else was executed.
This example illustrates that it always is a good idea to plan what you want your error handler to do. You can choose
between try/catch and trap, and also you can change the position of your trap.
If you placed your trap inside the "territory" or "scope" where the error occurs, you could make sure all computers in
your list are processed:
'localhost', '127.0.0.1', 'storage1', 'nonexistent', 'offline' |
ForEach-Object {
trap {
Write-Warning ('Failed to access "{0}" : {1} in "{2}"' -f
$currentcomputer, `
$_.Exception.Message, $_.InvocationInfo.ScriptName)
continue
}
$currentcomputer = $_
Get-WmiObject -class Win32_BIOS -computername $currentcomputer ErrorAction Stop |
Select-Object __Server, Version
}
When you redirect the error channel to the output channel, the error suddenly becomes red and is turned into a "real"
PowerShell error:
PS> net user willibald 2>&1
net.exe : The user name could not be found.
At line:1 char:4
You can still not handle the error. When you place the code in a try/catch-block, the catch-block never executes:
try {
net user willibald 2>&1
}
catch {
Write-Warning "Oops: $_"
}
As you know from cmdlets, to handle errors you need to set the ErrorAction to Stop. With cmdlets, this was easy
because each cmdlet has a -ErrorAction preference. Native commands do not have such a parameter. This is why
you need to use $ErrorActionPreference to set the ErrorAction to Stop:
try {
$ErrorActionPreference = 'Stop'
net user willibald 2>&1
}
catch {
Write-Warning "Oops: $_"
}
If you do not like the default colors PowerShell uses for error messages, simply change them:
$Host.PrivateData.ErrorForegroundColor = "Red"
$Host.PrivateData.ErrorBackgroundColor = "White"
You can also find additional properties in the same location which enable you to change the colors of warning and
debugging messages (like WarningForegroundColor and WarningBackgroundColor).
Understanding Exceptions
Exceptions work like bubbles in a fish tank. Whenever a fish gets sick, it burps, and the bubble bubbles up to the
surface. If it reaches the surface, PowerShell notices the bubble and throws the exception: it outputs a red error
message.
In this chapter, you learned how you can catch the bubble before it reaches the surface, so PowerShell would never
notice the bubble, and you got the chance to replace the default error message with your own or take appropriate
action to handle the error.
The level the fish swims in the fish tank resembles your code hierarchy. Each pair of braces resembles own
"territory" or "scope", and when a scope emits an exception (a "bubble"), all upstream scopes have a chance to catch
and handle the exception or even replace it with another exception. This way you can create complex escalation
scenarios.
OUTPUT: Hello
The caller can now handle the error your function emitted and choose by himself how he would like to respond to it:
PS> try { TextOutput } catch { "Oh, an error: $_" }
Oh, an error: You must enter some text.
Simple tracing will show you only PowerShell statements executed in the current context. If you invoke a function
or a script, only the invocation will be shown but not the code of the function or script. If you would like to see the
code, turn on detailed traced by using the -trace 2 parameter.
Set-PSDebug -trace 2
If you would like to turn off tracing again, select 0:
Set-PSDebug -trace 0
To step code, use this statement:
Set-PSDebug -step
Now, when you execute PowerShell code, it will ask you for each statement whether you want to continue, suspend
or abort.
If you choose Suspend by pressing "H", you will end up in a nested prompt, which you will recognize by the "<<"
sign at the prompt. The code will then be interrupted so you could analyze the system in the console or check
variable contents. As soon as you enter Exit, execution of the code will continue. Just select the "A" operation for
"Yes to All" in order to turn off the stepping mode.
Tip: You can create simple breakpoints by using nested prompts: call $host.EnterNestedPrompt() inside a script or a
function.
Set-PSDebug has another important parameter called -strict. It ensures that unknown variables will throw an error.
Without the Strict option, PowerShell will simply set a null value for unknown variables. On machines where you
develop PowerShell code, you should enable strict mode like this:
Set-StrictMode -Version Latest
This will throw exceptions for unknown variables (possible typos), nonexistent object properties and wrong cmdlet
call syntax.
Summary
To handle errors in your code, make sure you set the ErrorAction to Stop. Only then will cmdlets and native
commands place errors in your control.
To detect and respond to errors, use either a local try/catch-block (to catch errors in specific regions of your code) or
trap (to catch all errors in the current scope). With trap, make sure to also call Continue at the end of your error
handler to tell PowerShell that you handled the error. Else, it would still bubble up to PowerShell and cause the
default error messages.
To catch errors from console-based native commands, redirect their ErrOut channel to StdOut. PowerShell then
automatically converts the custom error emitted by the command into a PowerShell exception.
Anything you define in PowerShell - variables, functions, or settings - have a certain life span. Eventually, they
expire and are automatically removed from memory. This chapter talks about "scope" and how you manage the life
span of objects or scripts.
Understanding and correctly managing scope can be very important. You want to make sure that a production script
is not negatively influenced by "left-overs" from a previous script. Or you want certain PowerShell settings to apply
only within a script. Maybe you are also wondering just why functions defined in a script you run won't show up in
your PowerShell console. These questions all touch "scope".
At the end of this chapter, we will also be looking at how PowerShell finds commands and how to manage and
control commands if there are ambiguous command names.
Topics Covered:
PowerShell Session: Your PowerShell session - the PowerShell console or a development environment like
ISE - always opens the first scope which is called "global". Anything you define in that scope persists until
you close PowerShell.
Script: When you run a PowerShell script, this script by default runs in its own scope. So any variables or
functions a script declares will automatically be cleared again when the script ends. This ensures that a
script will not leave behind left-overs that may influence the global scope or other scripts that you run later.
Note that the default behavior can be changed both by the user and the programmer, enabling the script to
store variables or functions in the callers' scope. You'll learn about that in a minute.
Function: Every function runs yet in another scope, so variables and functions declared in a function are by
default not visible to the outside. This guarantees that functions won't interfere with each other and write to
the same variables - unless that is what you want. To create "shared" variables that are accessible to all
functions, you would manually change scope. Again, that'll be discussed in a minute.
Script Block: Since functions really are named script blocks, what has been said about functions also
applies to script blocks. They run in their own scope or territory too.
and the script accesses the variable $a, two things can happen: if your script has defined $a itself, you get the scripts'
version of $a. If the script has not defined $a, you get the variable from the global scope that you defined in the
console.
So here is the first golden rule that derives from this: in your scripts and functions, always declare variables and give
them an initial value. If you don't, you may get unexpected results. Here is a sample:
function Test {
if ($true -eq $hasrun) {
'This function was called before'
} else {
$hasrun = $true
'This function runs for the first time'
}
}
When you call the function Test for the first time, it will state that it was called for the first time. When you call it a
second time, it should notice that it was called before. In reality, the function does not, though. Each time you call it,
it reports that it has been running for the first time. Moreover, in the PowerShell console enter this line:
$hasrun = 'some value'
When you now run the function Test again, it suddenly reports that it ran before. So the function is not at all doing
what it was supposed to do. All of the unexpected behaviors can be explained with scopes.
Since each function creates its own scope, all variables defined within only exist while the function executes. Once
the function is done, the scope is discarded. That's why the variable $hasrun cannot be used to remember a previous
function call. Each time the function runs, a new $hasrun variable is created.
So why then does the function report that it has been called before once you define a variable $hasrun with arbitrary
content in the console?
When the function runs, the if statement checks to see whether $hasrun is equal to $true. Since at that point there is
no $hasrun variable in this scope, PowerShell starts to search for the variable in the parent scopes. Here, it finds the
variable. And since the if statement compares a boolean value with the variable content, automatic type casting takes
place: the content of the variable is automatically converted to a boolean value. Anything except $null will result in
$true. Check it out, and assign $null to the variable, then call the function again:
PS> $hasrun = $null
PS> test
This function runs for the first time
To solve this problem and make the function work, you have to use global variables. A global variable basically is
what you created manually in the PowerShell console, and you can create and access global variables
programmatically, too. Here is the revised function:
function Test {
if ($global:hasrun -eq $true) {
'This function was called before'
} else {
$global:hasrun = $true
'This function runs for the first time'
}
}
There are two changes in the code that made this happen:
Since all variables defined inside a function have a limited life span and are discarded once the function
ends, store information that continues to be present after that needs in the global scope. You do that by
adding "global:" to your variable name.
To avoid implicit type casting, reverse the order of the comparison. PowerShell always looks at the type to
the left, so if that is a boolean value, the variable content will also be turned into a boolean value. As you
have seen, this may result in unexpected cross-effects. By using your variable first and comparing it to
$true, the variable type will not be changed.
Note that in place of global:, you can also use script:. That's another scope that may be useful. If you run the
example in the console, they both represent the same scope, but when you define your function in a script and then
run the script, script: refers to the script scope, so it creates "shared variables" that are accessible from anywhere
inside the script. You will see an example of this shortly.
When you run this script, both errors are caught, and your script controls the error messages itself. Once the script is
done, check the content of $ErrorActionPreference:
PS> $ErrorActionPreference
continue
It is still set to 'Continue'. By default, the change made to $ErrorActionPreference was limited to your script and did
not change the setting in the parent scope. That's good because it prevents unwanted side-effects and left-overs from
previously running scripts.
Note: If the script did change the global setting, you may have called your script "dot-sourced". We'll discuss this
shortly. To follow the example, you need to call your script the default way: in the PowerShell console, enter the
complete path to your script file. If you have to place the path in quotes because of spaces, prepend it with "&".
Now create a second script and call it script2.ps1. Save it in the same folder:
"script2 starting"
dir nonexisting:
Get-Process noprocess
"script2 ending"
When you run script2.ps1, you get two error messages from PowerShell. As you can see, the entire script2.ps1 is
executed. You can see both the start message and the end message:
No PowerShell error messages anymore. script1.ps1 has propagated the ErrorActionPreference setting to the child
script, so the child script now also uses the setting "Continue". Any error in script2.ps1 now bubbles up to the next
available error handler which happens to be the trap in script1.ps1. That explains why the first error in script2.ps1
was output by the error handler in script1.ps1.
When you look closely at the result, you will notice though that script2.ps1 was aborted. It did not continue to run.
Instead, when the first error occurred, all remaining calls where skipped.
That again is default behavior: the error handler in script1.ps1 uses the statement "Continue", so after an error was
reported, the error handler continues. It just does not continue in script2.ps1. That's because an error handler always
continues with the next statement that resides in the same scope the error handler is defined. script2.ps1 is a child
scope, though.
Here are two rules that can correct the issues:
If you want to call child scripts without propagating information or settings, make sure you mark them as
private:. Note though that this will also prevent the changes from being visible in other child scopes such as
functions you may have defined.
If you do propagate $ErrorActionPreference='Stop' to child scripts, make sure you also implement an error
handler in that script or else the script will be aborted at the first error.
o Library Script: your script is not actually performing a task but it is rather working like a library. It
defines functions for later use.
o Debugging: you want to explore variable content after a script has run.
Here is the revised script1.ps1 that uses private:
$private:ErrorActionPreference = 'Stop'
trap {
"Something bad occured: $_"
continue
}
$folder = Split-Path $MyInvocation.MyCommand.Definition
'Starting Script'
dir nonexisting:
'Starting Subscript'
& "$folder\script2.ps1"
'Done'
Done
Now, errors in script1.ps1 are handled by the built-in error handler, and errors in script2.ps1 are handled by
PowerShell.
And this is the revised script2.ps1 that uses its own error handler.
trap {
"Something bad occured: $_"
continue
}
"script2 starting"
dir nonexisting:
Get-Process noprocess
"script2 ending"
Make sure you change script1.ps1 back to the original version by removing "private:" again before you run
it:
PS> & 'C:\scripts\script1.ps1'
Starting Script
Something bad occured: Cannot find drive. A drive with the name
'nonexisting' does not exist.
Starting Subscript
script2 starting
Something bad occured: Cannot find drive. A drive with the name
'nonexisting' does not exist.
Something bad occured: Cannot find a process with the name "noprocess".
Verify the process name and call the cmdlet again.
script2 ending
Done
This time, all code in script2.ps1 was executed and each error was handled by the new error handler in
script2.ps1.
This is how you can make sure functions and variables defined in a script remain accessible even after the
script is done. Here is a sample. Type in the code and save it as script3.ps1:
function test-function {
'I am a test function!'
}
test-function
When you run this script the default way, the function test-function runs once because it is called from
within the script. Once the script is done, the function is gone. You can no longer call test-function.
PS> & 'C:\script\script3.ps1'
I am a test function!
PS> test-function
The term 'test-function' 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:14
+ test-function <<<<
+ CategoryInfo
: ObjectNotFound: (test-function:String)
[], CommandNotFoundException
+ FullyQualifiedErrorId : CommandNotFoundException
Now, run the script dot-sourced! You do that by replacing the call operator "&" by a dot:
PS> . 'C:\script\script3.ps1'
I am a test function!
PS> test-function
I am a test function!
Since now the script scope and the global scope are identical, the script did define the function test-function
in the global scope. That's why the function is still there once the script ended.
There are two primary reasons to use dot-sourcing:
The profile script that PowerShell runs automatically during startup ($profile) is an example of a script that
is running dot-sourced, although you cannot see the actual dot-sourcing call.
Note: To make sure functions defined in a script remain accessible, a developer could also prepend the
function name with "global:". However, that may not be such a clever idea. The prefix "global:" always
creates the function in the global context. Dot-sourcing is more flexible because it creates the function in
the caller's context. So if a script runs another script dot-sourced, all functions defined in the second script
are also available in the first, but the global context (the console) remains unaffected and unpolluted.
PowerShell supports a wide range of command types, and when you call a command, there is another type
of scope. Each command type lives in its own scope, and when you ask PowerShell to execute a command,
it searches the command type scopes in a specific order.
This default behavior is completely transparent if there is no ambiguity. If however you have different
command types with the same name, this may lead to surprising results:
# Run an external command:
ping -n 1 10.10.10.10
Pinging 10.10.10.10 with 32 bytes of data:
Reply from 10.10.10.10: Bytes=32 Time<1ms TTL=128
Ping statistics for 10.10.10.10:
Packets: Sent = 1, Received = 1, Lost = 0 (0% Loss),
Ca. time in millisec:
Minimum = 2ms, Maximum = 2ms, Average = 2ms
# Create a function having the same name:
function Ping { "Ping is not allowed." }
# Function has priority over external program and turns off command:
ping -n 1 10.10.10.10
Ping is not allowed.
As you can see, your function was able to "overwrite" ping.exe. Actually, it did not overwrite anything. The
scope functions live in has just a higher priority than the scope applications live in. Aliases live in yet
another scope which has the highest priority of them all:
Set-Alias ping echo
ping -n 1 10.10.10.10
-n
1
10.10.10.10
Now, Ping calls the Echo command, which is an alias for Write-Output and simply outputs the parameters
that you may have specified after Ping in the console.
CommandType
Alias
Function
Filter
Cmdlet
Application
ExternalScript
Script
Description
An alias for another command added by using Set-Alias
A PowerShell function defined by using function
A PowerShell filter defined by using filter (a function with a process block)
A PowerShell cmdlet from a registered snap-in
An external Win32 application
An external script file with the file extension ".ps1"
A scriptblock
Priority
1
2
2
3
4
5
-
CommandType
Name
-------------function
Ping
allowed."
Alias
ping
Application
PING.EXE
C:\Windows\system32\PING.EXE
Definition
---------"Ping is not
echo
Summary
PowerShell uses scopes to manage the life span and visibility of variables and functions. By default, the
content of scopes is visible to all child scopes and does not change any parent scope.
There is always at least one scope which is called "global scope". New scopes are created when you define
scripts or functions.
The developer can control the scope to use by prepending variable and function names with one of these
keywords: global:, script:, private: and local:. The prefix local: is the default and can be omitted.
The user can control scope by optionally dot-sourcing scripts, functions or script blocks. With dot sourcing,
for the element you are calling, no new scope is created. Instead, the caller's context is used.
A different flavor of scope is used to manage the five different command types PowerShell supports. Here,
PowerShell searches for commands in a specific order. If the command name is ambiguous, PowerShell
uses the first command it finds. To find the command, it searches the command type scopes in this order:
alias, function, cmdlet, application, external script, and script. Use Get-Command to locate a command
yourself based on name and command type if you need more control.
Often, you need to deal with plain text information. You may want to read the content from some text file and
extract lines that contain a keyword, or you would like to isolate the file name from a file path. So while the objectoriented approach of PowerShell is a great thing, at the end of a day most useful information breaks down to plain
text. In this chapter, you'll learn how to control text information in pretty much any way you want.
Topics Covered:
Defining Text
o Special Characters in Text
o Resolving Variables
o "Here-Strings": Multi-Line Text
o Communicating with the User
Composing Text with "-f"
o Setting Numeric Formats
o Outputting Values in Tabular Form: Fixed Width
o String Operators
o String Object Methods
o Analyzing Methods: Split() as Example
Simple Pattern Recognition
Regular Expressions
o Describing Patterns
o Quantifiers
o Anchors
o Recognizing Addresses
o Validating E-Mail Adddresses
o Simultaneous Searches for Different Terms
o Case Sensitivity
o Finding Information in Text
o Searching for Several Keywords
o Forming Groups
o Greedy or Lazy? Shortest or Longest Possible Result
o Finding Segments
o Replacing a String
o Using Back References
o Putting Characters First at Line Beginnings
o Removing White Space
o Finding and Removing Doubled Words
Summary
Defining Text
To define text, place it in quotes. If you want PowerShell to treat the text exactly the way you type it, use single
quotes. Use double quotes with care because they can transform your text: any variable you place in your text will
get resolved, and PowerShell replaces the variable with its context. Have a look:
$text = 'This text may also contain $env:windir `: $(2+2)'
This text may also contain $env:windir `: $(2+2)
Placed in single quotes, PowerShell returns the text exactly like you entered it. With double quotes, the result is
completely different:
$text = "This text may also contain $env:windir `: $(2+2)"
This text may also contain C:\Windows: 4
If you must use the same type of quote both as delimiter and inside the text, you can "escape" quotes (remove their
special meaning) by either using two consecutive quotes, or by placing a "backtick" character in front of the quote:
'The
"The
'The
"The
''situation''
""situation""
`'situation`'
`"situation`"
was
was
was
was
really
really
really
really
not
not
not
not
that
that
that
that
bad'
bad"
bad'
bad"
The second most wanted special character you may want to include in text is a new line so you can extend text to
more than one line. Again, you have a couple of choices.
When you use double quotes to delimit text, you can insert special control characters like tabs or line breaks by
adding a backtick and then a special character where "t" stands for a tab and "n" represents a line break. This
technique does require that the text is defined by double quotes:
PS> "One line`nAnother line"
One line
Another line
PS> 'One line`nAnother line'
One line`nAnother line
Escape Sequence
`n
`r
`t
`a
`b
`'
`"
`0
``
Special Characters
New line
Carriage return
Tabulator
Alarm
Backspace
Single quotation mark
Double quotation mark
Null
Backtick character
Resolving Variables
A rather unusual special character is "$". PowerShell uses it to define variables that can hold information. Text in
double quotes also honors this special character and recognizes variables by resolving them: PowerShell
automatically places the variable content into the text:
$name = 'Weltner'
"Hello Mr $name"
This only works for text enclosed in double quotes. If you use single quotes, PowerShell ignores variables and treats
"$" as a normal character:
'Hello Mr $name'
At the same time, double quotes protect you from unwanted variable resolving. Take a look at this example:
"My wallet is low on $$$$"
As turns out, $$ is again a variable (it is an internal "automatic" variable maintained by PowerShell which happens
to contain the last command token PowerShell processed which is why the result of the previous code line can vary
and depends on what you executed right before), so as a rule of thumb, you should start using single quotes by
default unless you really want to resolve variables in your text. Resolving text can be enormously handy:
PS> $name = "report"
PS> $extension = "txt"
PS> "$name.$extension"
report.txt
$text = @"
>> Here-Strings can easily stretch over several lines and may also include
>>"quotation marks". Nevertheless, here, too, variables are replaced with
>> their values: C:\Windows, and subexpressions like 4 are likewise replaced
>> with their result. The text will be concluded only if you terminate the
>> here-string with the termination symbol "@.
>> "@
>>
$text
Here-Strings can easily stretch over several lines and may also include
"quotation marks". Nevertheless, here, too, variables are replaced with
their values: C:\Windows, and subexpressions like 4 are likewise replaced
with their result. The text will be concluded only if you terminate the
here-string with the termination symbol "@.
Text accepted by Read-Host is treated literally, so it behaves like text enclosed in single quotes. Special characters
and variables are not resolved. If you want to resolve the text a user entered, you can however send it to the internal
ExpandString() method for post-processing. PowerShell uses this method internally when you define text in double
quotes:
# Query and output text entry by user:
$text = Read-Host "Your entry"
Your entry: $env:windir
$text
$env:windir
# Treat entered text as if it were in double quotation marks:
$ExecutionContext.InvokeCommand.ExpandString($text)
C:\Windows
You can also request secret information from a user. To mask input, use the switch parameter -asSecureString. This
time, however, Read-Host won't return plain text anymore but instead an encrypted SecureString. So, not only the
input was masked with asterisks, the result is just as unreadable. To convert an encrypted SecureString into plain
text, you can use some internal .NET methods:
$pwd = Read-Host -asSecureString "Password"
Password: *************
$pwd
System.Security.SecureString
[Runtime.InteropServices.Marshal]::PtrToStringAuto([Runtime.InteropServices.M
arshal]::SecureStringToBSTR($pwd))
strictly confidential
The -f format operator formats a string and requires a string, along with wildcards on its left side and on its right
side, that the results are to be inserted into the string instead of the wildcards:
"{0} diskettes per CD" -f (720mb/1.44mb)
500 diskettes per CD
It is absolutely necessary that exactly the same results are on the right side that are to be used in the string are also
on the left side. If you want to just calculate a result, then the calculation should be in parentheses. As is generally
true in PowerShell, the parentheses ensure that the enclosed statement is evaluated first and separately and that
subsequently, the result is processed instead of the parentheses. Without parentheses, -f would report an error:
"{0} diskettes per CD" -f 720mb/1.44mb
Bad numeric constant: 754974720 diskettes per CD.
At line:1 char:33
+ "{0} diskettes per CD" -f 720mb/1 <<<< .44mb
You may use as many wildcard characters as you wish. The number in the braces states which value will appear
later in the wildcard and in which order:
"{0} {3} at {2}MB fit into one CD at {1}MB" -f (720mb/1.44mb), 1.44, 720,
"diskettes"
500 diskettes at 720MB fit into one CD at 1.44MB
Index: This number indicates which value is to be used for this wildcard. For example, you could use
several wildcards with the same index if you want to output one and the same value several times, or in
various display formats. The index number is the only obligatory specification. The other two
specifications are voluntary.
Alignment: Positive or negative numbers can be specified that determine whether the value is right
justified (positive number) or left justified (negative number). The number states the desired width. If the
value is wider than the specified width, the specified width will be ignored. However, if the value is
narrower than the specified width, the width will be filled with blank characters. This allows columns to be
set flush.
Format: The value can be formatted in very different ways. Here you can use the relevant format name to
specify the format you wish. You'll find an overview of available formats below.
Formatting statements are case sensitive in different ways than what is usual in PowerShell. You can see how large
the differences can be when you format dates:
# Formatting with a small letter d:
"Date: {0:d}" -f (Get-Date)
Date: 08/28/2007
# Formatting with a large letter D:
"Date: {0:D}" -f (Get-Date)
Date: Tuesday, August 28, 2007
Symbol
#
%
,
,.
.
0
c
d
e
e
f
g
n
x
Type
Digit placeholder
Percentage
Thousands separator
Integral multiple of 1,000
Decimal point
0 placeholder
Currency
Decimal
Scientific notation
Exponent wildcard
Fixed point
General
Thousands separator
Hexadecimal
Call
"{0:(#).##}" -f $value
"{0:0%}" -f $value
"{0:0,0}" -f $value
"{0:0,.} " -f $value
"{0:0.0}" -f $value
"{0:00.0000}" -f $value
"{0:c}" -f $value
"{0:d}" -f $value
"{0:e}" -f $value
"{0:00e+0}" -f $value
"{0:f}" -f $value
"{0:g}" -f $value
"{0:n}" -f $value
"0x{0:x4}" -f $value
Result
(1000000)
100000000%
1,000,000
1000
1000000.0
1000000.0000
1,000,000.00
1000000
1.000000e+006
10e+5
1000000.00
1000000
1,000,000.00
0x4240
There's also a very wide range of time and date formats. The relevant formats are listed in Table 13.4 and their
operation is shown in the following lines:
$date= Get-Date
foreach ($format in "d","D","f","F","g","G","m","r","s","t","T","u","U","y",`
"dddd, MMMM dd yyyy","M/yy","dd-MM-yy") {
"DATE with $format : {0}" -f $date.ToString($format)
}
DATE with d : 10/15/2007
DATE with D : Monday, 15 October, 2007
DATE with f : Monday, 15 October, 2007 02:17 PM
DATE
DATE
DATE
DATE
DATE
DATE
DATE
DATE
DATE
DATE
DATE
DATE
DATE
DATE
with
with
with
with
with
with
with
with
with
with
with
with
with
with
Symbol
d
D
t
T
f
F
g
G
M
r
s
u
U
Y
Type
Short date format
Long date format
Short time format
Long time format
Full date and time (short)
Full date and time (long)
Standard date (short)
Standard date (long)
Day of month
RFC1123 date format
Sortable date format
Universally sortable date format
Universally sortable GMT date format
Year/month format pattern
Call
"{0:d}" -f $value
"{0:D}" -f $value
"{0:t}" -f $value
"{0:T}" -f $value
"{0:f}" -f $value
"{0:F}" -f $value
"{0:g}" -f $value
"{0:G}" -f $value
"{0:M}" -f $value
"{0:r}" -f $value
"{0:s}" -f $value
"{0:u}" -f $value
"{0:U}" -f $value
"{0:Y}" -f $value
Result
09/07/2007
Friday, September 7, 2007
10:53 AM
10:53:56 AM
Friday, September 7, 2007 10:53 AM
Friday, September 7, 2007 10:53:56 AM
09/07/2007 10:53 AM
09/07/2007 10:53:56 AM
September 07
Fri, 07 Sep 2007 10:53:56 GMT
2007-09-07T10:53:56
2007-09-07 10:53:56Z
Friday, September 7, 2007 08:53:56
September 2007
System.Int16
System.Int32
System.Int64
System.IntPtr
System.SByte
System.Single
System.UInt16
System.UInt32
System.UInt64
Microsoft.PowerShell.Commands.MatchInfo
For example, among the supported data types is the "globally unique identifier" System.Guid. Because you'll
frequently require GUID, which is clearly understood worldwide, here's a brief example showing how to create and
format a GUID:
$guid = [GUID]::NewGUID()
foreach ($format in "N","D","B","P") {"GUID with $format : {0}" -f
$GUID.ToString($format)}
GUID with N : 0c4d2c4c8af84d198b698e57c1aee780
GUID with D : 0c4d2c4c-8af8-4d19-8b69-8e57c1aee780
GUID with B : {0c4d2c4c-8af8-4d19-8b69-8e57c1aee780}
GUID with P : (0c4d2c4c-8af8-4d19-8b69-8e57c1aee780)
Symbol
dd
ddd
dddd
gg
hh
HH
mm
MM
MMM
MMMM
ss
tt
yy
yyyy
zz
zzz
Type
Day of month
Abbreviated name of day
Full name of day
Era
Hours from 01 to 12
Hours from 0 to 23
Minute
Month
Abbreviated month name
Full month name
Second
AM or PM
Year in two digits
Year in four digits
Time zone including leading zero
Time zone in hours and minutes
Call
"{0:dd}" -f $value
"{0:ddd}" -f $value
"{0:dddd}" -f $value
"{0:gg}" -f $value
"{0:hh}" -f $value
"{0:HH}" -f $value
"{0:mm}" -f $value
"{0:MM}" -f $value
"{0:MMM}" -f $value
"{0:MMMM}" -f $value
"{0:ss}" -f $value
"{0:tt}" -f $value
"{0:yy}" -f $value
"{0:YY}" -f $value
"{0:zz}" -f $value
"{0:zzz}" -f $value
Result
07
Fri
Friday
A. D.
10
10
53
09
Sep
September
56
07
2007
+02
+02:00
In the following example, Dir returns a directory listing, from which a subsequent loop outputs file names and file
sizes. Because file names and sizes vary, the result is ragged right and hard to read:
dir | ForEach-Object { "$($_.name) = $($_.Length) Bytes" }
history.csv = 307 Bytes
info.txt = 8562 Bytes
layout.lxy = 1280 Bytes
list.txt = 164186 Bytes
p1.nrproj = 5808 Bytes
ping.bat = 116 Bytes
SilentlyContinue = 0 Bytes
The following result with fixed column widths is far more legible. To set widths, add a comma to the sequential
number of the wildcard and after it specify the number of characters available to the wildcard. Positive numbers will
set values to right alignment, negative numbers to left alignment:
dir | ForEach-Object
history.csv
info.txt
layout.lxy
list.txt
p1.nrproj
ping.bat
SilentlyContinue
{ "{0,-20} =
=
307
=
8562
=
1280
=
164186
=
5808
=
116
=
0
More options are offered by special text commands that PowerShell furnishes from three different areas:
String operators: PowerShell includes a number of string operators for general text tasks which you can
use to replace text and to compare text (Table 13.2).
Dynamic methods: the String data type, which saves text, includes its own set of text statements that you
can use to search through, dismantle, reassemble, and modify text in diverse ways (Table 13.6).
Static methods: finally, the String .NET class includes static methods bound to no particular text.
String Operators
All string operators work in basically the same way: they take data from the left and the right and then do something
with them. The replace operator for example takes a text and some replacement text and then replaces the
replacement text in the original text:
"Hello Carl" -replace "Carl", "Eddie"
Hello Eddie
The format operator -f works in exactly the same way. You heard about this operator at the beginning of this
chapter. It takes a static string template with placeholders and an array with values, and then fills the values into the
placeholders.
Two additional important string operators are -join and -split. They can be used to automatically join together an
array or to split a text into an array of substrings.
Let's say you want to output information that really is an array of information. When you query WMI for your
operating system to identify the installed MUI languages, the result can be an array (when more than one language is
installed). So, this line produces an incomplete output:
You would have to join the array to one string first using -join. Here is how:
PS> $mui = Get-WmiObject Win32_OperatingSystem | Select-Object ExpandProperty MuiLanguages
PS> 'Installed MUI-Languages: {0}' -f ($mui -join ', ')
Installed MUI-Languages: de-DE, en-US
The -split operator does the exact opposite. It takes a text and a split pattern, and each time it discovers the split
pattern, it splits the original text in chunks and returns an array. This example illustrates how you can use -split to
parse a path:
PS> ('c:\test\folder\file.txt' -split '\\')[-1]
file.txt
Note that -replace expects the pattern to be a regular expression, so if your pattern is composed of reserved
characters (like the backslash), you have to escape it. Note also that the Split-Path cmdlet can split paths more
easily.
To auto-escape a simple text pattern, use .NET methods. The Escape() method takes a simple text pattern and
returns the escaped version that you can use wherever a regular expression is needed:
PS> [RegEx]::Escape('some.\pattern')
some\.\\pattern
Another approach uses the dot as separator and Split() to split up the path into an array. The result is that the last
element of the array (-1 index number) will include the file extension:
$path.Split(".")[-1]
bat
Function
CompareTo()
Contains()
Description
Compares one string to another
Returns "True" if a specified comparison string is in a
string or if the comparison string is empty
Example
("Hello").CompareTo("Hello")
("Hello").Contains("ll")
CopyTo()
EndsWith()
Equals()
$a = ("Hello
World").toCharArray()
("User!").CopyTo(0, $a, 6, 5)
$a
("Hello").EndsWith("lo")
("Hello").Equals($a)
("Hello").IndexOf("l")
("Hello").IndexOfAny("loe")
("Hello World").Insert(6, "brave ")
("Hello").GetEnumerator()
("Hello").LastIndexOf("l")
("Hello").LastIndexOfAny("loe")
("Hello").PadLeft(10)
("Hello").PadRight(10) +
"World!"
("Hello World").Remove(5,6)
("Hello World").Replace("l", "x")
("Hello World").Split("l")
("Hello World").StartsWith("He")
("Hello World").Substring(4, 3)
("Hello World").toCharArray()
("Hello World").toLower()
("Hello
World").toLowerInvariant()
("Hello World").toUpper()
("Hello
World").ToUpperInvariant()
(" Hello ").Trim() + "World"
(" Hello ").TrimEnd() + "World"
(" Hello ").TrimStart() + "World"
("Hello").Chars(0)
Definition gets output, but it isn't very easy to read. Because Definition is also a string object, you can use methods
from Table 13.6, including Replace(), to insert a line break where appropriate. That makes the result much more
understandable:
("something" | Get-Member Split).Definition.Replace("), ", ")`n")
System.String[] Split(Params Char[] separator)
System.String[] Split(Char[] separator, Int32 count)
System.String[] Split(Char[] separator, StringSplitOptions options)
System.String[] Split(Char[] separator, Int32 count, StringSplitOptions
options)
System.String[] Split(String[] separator, StringSplitOptions options)
System.String[] Split(String[] separator, Int32 count, StringSplitOptions
options)
There are six different ways to invoke Split(). In simple cases, you might use Split() with only one argument, Split(),
you will expect a character array and will use every single character as a possible splitting separator. That's
important because it means that you may use several separators at once:
"a,b;c,d;e;f".Split(",;")
a
b
c
d
e
f
If the splitting separator itself consists of several characters, then it has got to be a string and not a single Char
character. There are only two signatures that meet this condition:
System.String[] Split(String[] separator, StringSplitOptions options)
System.String[] Split(String[] separator, Int32 count, StringSplitOptions
options)
You must make sure that you pass data types to the signature that is exactly right for it to be able to use a particular
signature. If you want to use the first signature, the first argument must be of the String[] type and the second
argument of the StringSplitOptions type. The simplest way for you to meet this requirement is by assigning
arguments first to a strongly typed variable. Create the variable of exactly the same type that the signature requires:
# Create a variable of the [StringSplitOptions] type:
[StringSplitOptions]$option = "None"
Split() in fact now uses a separator consisting of several characters. It splits the string only at the points where it
finds precisely the characters that were specified. There does remain the question of how do you know it is
necessary to assign the value "None" to the StringSplitOptions data type. The simple answer is: you dont know and
it isnt necessary to know. If you assign a value to an unknown data type that can't handle the value, the data type
will automatically notify you of all valid values:
[StringSplitOptions]$option = "werner wallbach"
Cannot convert value "werner wallbach" to type "System.StringSplitOptions"
due to invalid
enumeration values. Specify one of the following enumeration values and try
again.
The possible enumeration values are "None, RemoveEmptyEntries".
At line:1 char:28
+ [StringSplitOptions]$option <<<< = "werner wallbach"
By now it should be clear to you what the purpose is of the given valid values and their names. For example, what
was RemoveEmptyEntries() able to accomplish? If Split() runs into several separators following each other, empty
array elements will be the consequence. RemoveEmptyEntries() deletes such empty entries. You could use it to
remove redundant blank characters from a text:
[StringSplitOptions]$option = "RemoveEmptyEntries"
"This
text
has
too
much
whitespace".Split(" ", $option)
This
text
has
too
much
whitespace
Now all you need is just a method that can convert the elements of an array back into text. The method is called
Join(); it is not in a String object but in the String class.
Dir *.txt
# List all files in the Windows directory that begin with "n" or "w":
dir $env:windir\[nw]*.*
# List all files whose file extensions begin with "t" and which are exactly 3
characters long:
Dir *.t??
# List all files that end in one of the letters from "e" to "z"
dir *[e-z].*
Wildcard
*
?
[xyz]
[x-z]
Description
Any number of any character (including no characters at all)
Exactly one of any characters
One of specified characters
One of the characters in the specified area
Example
Dir *.txt
Dir *.??t
Dir [abc]*.*
Dir *[p-z].*
If you want to verify whether a valid e-mail address was entered, you could check the pattern like this:
$email = "tobias.weltner@powershell.de"
$email -like "*.*@*.*"
Regular Expressions
Use regular expressions for more accurate pattern recognition. Regular expressions offer highly specific wildcard
characters; that's why they can describe patterns in much greater detail. For the very same reason, however, regular
expressions are also much more complicated.
Describing Patterns
Using the regular expression elements listed in Table 13.11, you can describe patterns with much greater precision.
These elements are grouped into three categories:
Placeholder: The placeholder represents a specific type of data, for example a character or a digit.
Quantifier: Allows you to determine how often a placeholder occurs in a pattern. You could, for example,
define a 3-digit number or a 6-character-word.
Anchor: Allows you to determine whether a pattern is bound to a specific boundary. You could define a
pattern that needs to be a separate word or that needs to begin at the beginning of the text.
The pattern represented by a regular expression may consist of four different character types:
Literal characters like "abc" that exactly matches the "abc" string.
Masked or "escaped" characters with special meanings in regular expressions; when preceded by
"\", they are understood as literal characters: "\[test\]" looks for the "[test]" string. The following
characters have special meanings and for this reason must be masked if used literally: ". ^ $ * + ? { [
] \ | ( )".
Pre-defined wildcard characters that represent a particular character category and work like placeholders.
For example, "\d" represents any number from 0 to 9.
Custom wildcard characters: They consist of square brackets, within which the characters are specified
that the wildcard represents. If you want to use any character except for the specified characters, use "^" as
the first character in the square brackets. For example, the placeholder "[^f-h]" stands for all characters
except for "f", "g", and "h".
Element
.
[^abc]
[^a-z]
[abc]
[a-z]
\a
\c
\cA-\cZ
\d
\D
\e
\f
\n
\r
\s
\S
\t
\uFFFF
\v
\w
\W
\xnn
Description
Exactly one character of any kind except for a line break (equivalent to [^\n])
All characters except for those specified in brackets
All characters except for those in the range specified in the brackets
One of the characters specified in brackets
Any character in the range indicated in brackets
Bell alarm (ASCII 7)
Any character allowed in an XML name
Control+A to Control+Z, equivalent to ASCII 0 to ASCII 26
A number (equivalent to [0-9])
Any character except for numbers
Escape (ASCII 9)
Form feed (ASCII 15)
New line
Carriage return
Any whitespace character like a blank character, tab, or line break
Any character except for a blank character, tab, or line break
Tab character
Unicode character with the hexadecimal code FFFF. For example, the Euro symbol has the code 20AC
Vertical tab (ASCII 11)
Letter, digit, or underline
Any character except for letters
Particular character, where nn specifies the hexadecimal ASCII code
.*
Quantifiers
Every pattern listed in Table 13.8 represents exactly one instance of that kind. Using quantifiers, you can tell how
many instances are parts of your pattern. For example, "\d{1,3}" represents a number occurring one to three times
for a one-to-three digit number.
Element
*
*?
.*
?
??
{n,}
{n,m}
{n}
+
Description
Preceding expression is not matched or matched once or several times (matches as much as possible)
Preceding expression is not matched or matched once or several times (matches as little as possible)
Any number of any character (including no characters at all)
Preceding expression is not matched or matched once (matches as much as possible)
Preceding expression is not matched or matched once (matches as little as possible)
n or more matches
Inclusive matches between n and m
Exactly n matches
Preceding expression is matched once
Anchors
Anchors determine whether a pattern has to match a certain boundary. For example, the regular expression
"\b\d{1,3}" finds numbers only up to three digits if these turn up separately in a string. The number "123" in the
string "Bart123" would not qualify.
Elements Description
$
Matches at end of a string (\Z is less ambiguous for multi-line texts)
\A
Matches at beginning of a string, including multi-line texts
\b
Matches on word boundary (first or last characters in words)
\B
Must not match on word boundary
\Z
Must match at end of string, including multi-line texts
^
Must match at beginning of a string (\A is less ambiguous for multi-line texts)
Table 13.10: Anchor boundaries
Recognizing IP Addresses
Patterns such as an IP address can be very precisely described by regular expressions. Usually, you would use a
combination of characters and quantifiers to specify which characters may occur in a string and how often:
$ip = "10.10.10.10"
$ip -match "\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b"
True
$ip = "a.10.10.10"
$ip -match "\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b"
False
$ip = "1000.10.10.10"
$ip -match "\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b"
False
The pattern is described here as four numbers (char: \d) between one and three digits (using the quantifier {1,3}) and
anchored on word boundaries (using the anchor \b), meaning that it is surrounded by white space like blank
characters, tabs, or line breaks. Checking is far from perfect since it is not verified whether the numbers really do lie
in the permitted number range from 0 to 255.
# There still are entries incorrectly identified as valid IP addresses:
$ip = "300.400.500.999"
$ip -match "\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b"
True
= "test@somewhere.com"
-match "\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}\b"
= ".@."
-match "\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}\b"
Whenever you look for an expression that occurs as a single "word" in text, delimit your regular expression by word
boundaries (anchor: \b). The regular expression will then know you're interested only in those passages that are
demarcated from the rest of the text by white space like blank characters, tabs, or line breaks.
The regular expression subsequently specifies which characters may be included in an e-mail address. Permissible
characters are in square brackets and consist of "ranges" (for example, "A-Z0-9") and single characters (such as
"._%+-"). The "+" behind the square brackets is a quantifier and means that at least one of the given characters must
be present. However, you can also stipulate as many more characters as you wish.
Following this is "@" and, if you like, after it a text again having the same characters as those in front of "@". A dot
(\.) in the e-mail address follows. This dot is introduced with a "\" character because the dot actually has a different
meaning in regular expressions if it isn't within square brackets. The backslash ensures that the regular expression
understands the dot behind it literally.
After the dot is the domain identifier, which may consist solely of letters ([A-Z]). A quantifier ({2,4}) again follows
the square brackets. It specifies that the domain identifier may consist of at least two and at most four of the given
characters.
However, this regular expression still has one flaw. While it does verify whether a valid e-mail address is in the text
somewhere, there could be another text before or after it:
$email = "Email please to test@somewhere.com and reply!"
Because of "\b", when your regular expression searches for a pattern somewhere in the text, it only takes into
account word boundaries. If you prefer to check whether the entire text corresponds to an authentic e-mail, use the
elements for sentence beginnings (anchor: "^") and endings (anchor: "$") instead of word boundaries.
$email -match "^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}$"
The "?" character here doesn't represent any character at all, as you might expect after using simple wildcards. For
regular expressions, "?" is a quantifier and always specifies how often a character or expression in front of it may
occur. In the example, therefore, "u?" ensures that the letter "u" may, but not necessarily, be in the specified location
in the pattern. Other quantifiers are "*" (may also match more than one character) and "+" (must match characters at
least once).
If you prefer to mark more than one character as optional, put the character in a sub-expression, which are placed in
parentheses. The following example recognizes both the month designator "Nov" and "November":
"Nov" -match "\bNov(ember)?\b"
True
"November" -match "\bNov(ember)?\b"
True
If you'd rather use several alternative search terms, use the OR character "|":
"Bob and Ted" -match "Alice|Bob"
True
And if you want to mix alternative search terms with fixed text, use sub-expressions again:
# finds "and Bob":
"Peter and Bob" -match "and (Bob|Willy)"
True
# does not find "and Bob":
"Bob and Peter" -match "and (Bob|Willy)"
False
Case Sensitivity
In keeping with customary PowerShell practice, the -match operator is case insensitive. Use the operator -cmatch as
alternative if you'd prefer case sensitivity:
# -match is case insensitive:
"hello" -match "heLLO"
True
# -cmatch is case sensitive:
"hello" -cmatch "heLLO"
False
If you want case sensitivity in only some pattern segments, use match. Also, specify in your regular expression
which text segments are case sensitive and which are insensitive. Anything following the "(?i)" construct is case
insensitive. Conversely, anything following "(?-i)" is case sensitive. This explains why the word "test" in the below
example is recognized only if its last two characters are lowercase, while case sensitivity has no importance for the
first two characters:
"TEst" -match "(?i)te(?-i)st"
True
"TEST" -match "(?i)te(?-i)st"
False
If you use a .NET framework RegEx object instead of match, it will work case-sensitive by default, much like
cmatch. If you prefer case insensitivity, either use the above construct to specify the option (i?) in your regular
expression or submit extra options to the Matches() method (which is a lot more work):
[regex]::matches("test", "TEST", "IgnoreCase")
Element Description
(xyz)
Sub-expression
|
Alternation construct
When followed by a character, the character is not recognized as a formatting character but as
\
a literal character
x?
Changes the x quantifier into a "lazy" quantifier
(?xyz)
Activates of deactivates special modes, among others, case sensitivity
x+
Turns the x quantifier into a "greedy" quantifier
?:
Does not backtrack
?<name> Specifies name for back references
Category
Selection
Escape
Option
Option
Option
Reference
Reference
$ip = "300.400.500.999"
$ip -match "\b(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[05]|2[0-4][0-9]|[01]?[0-9][0-9]?)\b"
False
The expression validates only expressions running into word boundaries (the anchor is \b). The following subexpression defines every single number:
(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)
The construct ?: is optional and enhances speed. After it come three alternatively permitted number formats
separated by the alternation construct "|". 25[0-5] is a number from 250 through 255. 2[0-4][0-9] is a number from
200 through 249. Finally, [01]?[0-9][0-9]? is a number from 0-9 or 00-99 or 100-199. The quantifier "?" ensures
that the preceding pattern must be included. The result is that the sub-expression describes numbers from 0 through
255. An IP address consists of four such numbers. A dot always follows the first three numbers. For this reason, the
following expression includes a definition of the number:
(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}
A dot, (\.), is appended to the number. This construct is supposed to be present three times ({3}). When the fourth
number is also appended, the regular expression is complete. You have learned to create sub-expressions (by using
parentheses) and how to iterate sub-expressions (by indicating the number of iterations in braces after the subexpression), so you should now be able to shorten the first used IP address regular expression:
$ip = "10.10.10.10"
$ip -match "\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b"
True
$ip -match "\b(?:\d{1,3}\.){3}\d{1,3}\b"
True
Does that also work for more than one e-mail addresses in text? Unfortunately, no. The match operator finds only
the first matching expression. So, if you want to find more than one occurrence of a pattern in raw text, you have to
switch over to the RegEx object underlying the match operator and use it directly.
Since the RegEx object is case-sensitive by default, put the "(?i)" option before the regular expression to make it
work like -match.
# A raw text contains several e-mail addresses. match finds the first one
only:
$rawtext = "test@test.com sent an e-mail that was forwarded to spam@junk.de."
$rawtext -match "\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}\b"
True
$matches
Name
Value
-------0
test@test.com
# A RegEx object can find any pattern but is case sensitive by default:
$regex = [regex]"(?i)\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}\b"
$regex.Matches($rawtext)
Groups
: {test@test.com}
Success : True
Captures : {test@test.com}
Index
: 4
Length
: 13
Value
: test@test.com
Groups
Success
Captures
Index
Length
Value
:
:
:
:
:
:
{spam@junk.de}
True
{spam@junk.de}
42
13
spam@junk.de
$matches
Name
---0
Value
----Set
$matches tells you which keyword actually occurs in the string. But note the order of keywords in your regular
expressionit's crucial because the first matching keyword is the one selected. In this example, the result would be
incorrect:
"SetValue a=1" -match "Get|GetValue|Set|SetValue"
True
$matches[0]
Set
Either change the order of keywords so that longer keywords are checked before shorter ones :
"SetValue a=1" -match "GetValue|Get|SetValue|Set"
True
$matches[0]
SetValue
or make sure that your regular expression is precisely formulated, and remember that you're actually searching
for single words. Insert word boundaries into your regular expression so that sequential order no longer plays a role:
"SetValue a=1" -match "\b(Get|GetValue|Set|SetValue)\b"
True
$matches[0]
SetValue
It's true here, too, that -match finds only the first match. If your raw text has several occurrences of the keyword, use
a RegEx object again:
$regex = [regex]"\b(Get|GetValue|Set|SetValue)\b"
$regex.Matches("Set a=1; GetValue a; SetValue b=12")
Groups
: {Set, Set}
Success : True
Captures : {Set}
Index
: 0
Length
: 3
Value
: Set
Groups
Success
Captures
Index
Length
Value
:
:
:
:
:
:
{GetValue, GetValue}
True
{GetValue}
9
8
GetValue
Groups
: {SetValue, SetValue}
Success : True
Captures : {SetValue}
Index
Length
Value
: 21
: 8
: SetValue
Forming Groups
A raw text line is often a heaping trove of useful data. You can use parentheses to collect this data in subexpressions so that it can be evaluated separately later. The basic principle is that all the data that you want to find in
a pattern should be wrapped in parentheses because $matches will return the results of these sub-expressions as
independent elements. For example, if a text line contains a date first, then text, and if both are separated by tabs,
you could describe the pattern like this:
# Defining pattern: two characters separated by a tab
$pattern = "(.*)\t(.*)"
# Generate example line with tab character
$line = "12/01/2009`tDescription"
# Use regular expression to parse line:
$line -match $pattern
True
# Show result:
$matches
Name
---2
1
0
$matches[1]
12/01/2009
$matches[2]
Description
Value
----Description
12/01/2009
12/01/2009
Description
When you use sub-expressions, $matches will contain the entire searched pattern in the first array element named
"0". Sub-expressions defined in parentheses follow in additional elements. To make them easier to read and
understand, you can assign sub-expressions their own names and later use the names to call results. To assign names
to a sub-expression, type ? in parentheses for the first statement:
# Assign subexpressions their own names:
$pattern = "(?<Date>.*)\t(?<Text>.*)"
# Generate example line with tab character:
$line = "12/01/2009`tDescription"
# Use a regular expression to parse line:
$line -match $pattern
True
# Show result:
$matches
Name
----
Value
-----
Text
Date
0
$matches.Date
12/01/2009
$matches.Text
Description
Description
12/01/2009
12/01/2009
Description
Each result retrieved by $matches for each sub-expression naturally requires storage space. If you don't need the
results, discard them to increase the speed of your regular expression. To do so, type "?:" as the first statement in
your sub-expression:
# Don't return a result for the second subexpression:
$pattern = "(?<Date>.*)\t(?:.*)"
# Generate example line with tab character:
$line = "12/01/2009`tDescription"
# Use a regular expression to parse line:
$line -match $pattern
True
# No more results will be returned for the second subexpression:
$matches
Name
Value
-------Date
12/01/2009
0
12/01/2009
Description
In both cases, the regular expression recognizes the month, but returns different results in $matches. By default, the
regular expression is "greedy" and returns the longest possible match. If the text is "February," then the expression
will search for a match starting with "Feb" and then continue searching "greedily" to check whether even more
characters match the pattern. If they do, the entire (detailed) text is reported back: February.
If your main concern is just standardizing the names of months, you would probably prefer getting back the shortest
possible text: Feb. To switch regular expressions to work lazy (returning the shortest possible match), add "?" to the
expression. "Feb(ruary)??" now stands for a pattern that starts with "Feb", followed by zero or one occurance of
"ruary" (Quantifier "?"), and returning only the shortest possible match (which is turned on by the second "?").
"Feb" -match "Feb(ruary)??"
True
$matches[0]
Feb
"February" -match "Feb(ruary)??"
True
$matches[0]
Feb
Replacing a String
You already know how to replace a string because you know the string replace operator. Simply tell the operator
what term you want to replace in a string:
"Hello, Ralph" -replace "Ralph", "Martina"
Hello, Martina
But simple replacement isn't always sufficient, so you can also use regular expressions for replacements. Some of
the following examples show how that could be useful.
Let's say you'd like to replace several different terms in a string with one other term. Without regular expressions,
you'd have to replace each term separately. With regular expressions, simply use the alternation operator, "|":
"Mr. Miller and Mrs. Meyer" -replace "(Mr.|Mrs.)", "Our client"
Our client Miller and Our client Meyer
You can type any term in parentheses and use the "|" symbol to separate them. All the terms will be replaced with
the replacement string you specify.
The result looks a little peculiar, but the pattern you're looking for was correctly identified. The only replacements
were Mr. or Mrs. Miller and Mr. or Mrs. Meyer. The term "Mr. Werner" wasn't replaced. Unfortunately, the result
also shows that it doesn't make any sense here to replace the entire pattern. At least the name of the person should be
retained. Is that possible?
This is where the back referencing you've already seen comes into play. Whenever you use parentheses in your
regular expression, the result inside the parentheses is evaluated separately, and you can use these separate results in
your replacement string. The first sub-expression always reports whether a "Mr." or a "Mrs." was found in the string.
The second sub-expression returns the name of the person. The terms "$1" and "$2" provide you the sub-expressions
in the replacement string (the number is consequently a sequential number; you could also use "$3" and so on for
additional sub-expressions).
"Mr. Miller, Mrs. Meyer and Mr. Werner" -replace
"(Mr.|Mrs.)\s*(Miller|Meyer)", "Our client $2"
Our client , Our client and Mr. Werner
The back references don't seem to work. Can you see why? "$1" and "$2" look like PowerShell variables, but in
reality they are part of the regular expression. As a result, if you put the replacement string inside double quotes,
PowerShell replaces "$2" with the PowerShell variable $2, which is probably undefined. Use single quotation marks
instead, or add a backtick to the "$" special character so that PowerShell won't recognize it as its own variable and
replace it:
# Replacement text must be inside single quotation marks so that the PS
variable $2:
"Mr. Miller, Mrs. Meyer and Mr. Werner" -replace
"(Mr.|Mrs.)\s*(Miller|Meyer)", 'Our client $2'
Our client Miller, Our client Meyer and Mr. Werner
# Alternatively, $ can also be masked by `$:
"Mr. Miller, Mrs. Meyer and Mr. Werner" -replace
"(Mr.|Mrs.)\s*(Miller|Meyer)", "Our client `$2"
Our client Miller, Our client Meyer and Mr. Werner
However, to accomplish this, you need to know a little more about "multi-line" mode. Normally, this mode is turned
off, and the "^" anchor represents the text beginning and the "$" the text ending. So that these two anchors refer
respectively to the line beginning and line ending of a text of several lines, the multi-line mode must be turned on
with the "(?m)" statement. Only then will replace substitute the pattern in every single line. Once the multi-line
mode is turned on, the anchors "^" and "\A", as well as "$" and "\Z", will suddenly behave differently. "\A" will
continue to indicate the text beginning, while "^" will mark the line ending; "\Z" will indicate the text ending, while
"$" will mark the line ending.
# Using Here-String to create a text of several lines:
$text = @"
>> Here is a little text.
>> I want to attach this text to an e-mail as a quote.
>> That's why I would put a ">" before every line.
>> "@
>>
$text
Here is a little text.
I want to attach this text to an e-mail as a quote.
That's why I would put a ">" before every line.
# Normally, -replace doesn't work in multiline mode. For this reason,
# only the first line is replaced:
$text -replace "^", "> "
> Here is a little text.
I want to attach this text to an e-mail as a quote.
That's why I would put a ">" before every line.
# If you turn on multiline mode, replacement will work in every line:
$text -replace "(?m)^", "> "
> Here is a little text.
> I want to attach this text to an e-mail as a quote.
> That's why I would put a ">" before every line.
# The same can also be accomplished by using a RegEx object,
# where the multiline option must be specified:
[regex]::Replace($text, "^", "> ",
[Text.RegularExpressions.RegExOptions]::Multiline)
> Here is a little text.
> I want to attach this text to an e-mail as a quote.
> That's why I would put a ">" before every line.
# In multiline mode, \A stands for the text beginning and ^ for the line
beginning:
[regex]::Replace($text, "\A", "> ",
[Text.RegularExpressions.RegExOptions]::Multiline)
> Here is a little text.
I want to attach this text to an e-mail as a quote.
That's why I would put a ">" before every line.
Regular expressions can perform routine tasks as well, such as remove superfluous white space. The pattern
describes a blank character (char: "\s") that occurs at least twice (quantifier: "{2,}"). That is replaced with a normal
blank character.
"Too
many
blank
characters" -replace "\s{2,}", " "
Too many blank characters
Summary
Text is defined either by single or double quotation marks. If you use double quotation marks, PowerShell will
replace PowerShell variables and special characters in the text. Text enclosed in single quotation marks remains asis. If you want to prompt the user for input text, use the Read-Host cmdlet. Multi-line text can be defined with HereStrings, which start with @"(Enter) and end with "@(Enter).
By using the format operator f, you can compose formatted text. This gives you the option to display text in
different ways or to set fixed widths to output text in aligned columns (Table 13.3 through Table 13.5). Along with
the formatting operator, PowerShell has a number of string operators you can use to validate patterns or to replace a
string (Table 13.2).
PowerShell stores text in string objects, which support methods to work on the stored text. You can use these
methods by typing a dot after the string object (or the variable in which the text is stored) and then activating auto
complete (Table 13.6). Along with the dynamic methods that always refer to text stored in a string object, there are
also static methods that are provided directly by the string data type by qualifying the string object with "[string]::".
The simplest way to describe patterns is to use the simple wildcards in Table 13.7. Simple wildcard patterns, while
easy to use, only support very basic pattern recognition. Also, simple wildcard patterns can only recognize the
patterns; they cannot extract data from them.
A far more sophisticated tool are regular expressions. They consist of very specific placeholders, quantifiers and
anchors listed in Table 13.11. Regular expressions precisely identify even complex patterns and can be used with the
operators -match or replace. Use the .NET object [regex] if you want to match multiple pattern instances.
In todays world, data is no longer presented in plain-text files. Instead, XML (Extensible Markup Language) has
evolved to become a de facto standard because it allows data to be stored in a flexible yet standard way. PowerShell
takes this into account and makes working with XML data much easier than before.
Topics Covered:
The XML data is wrapped in an XML node which is the top node of the document:
XML is case-sensitive!
Conversion or loading XML from a file of course only works when the XML is valid and contains no syntactic
errors. Else, the conversion will throw an exception.
Once the XML data is stored in an XML object, it is easy to read its content because PowerShell automatically turns
XML nodes and attributes into object properties. So, to read the staff from the sample XML data, try this:
$xmldata.staff.employee
Name
Age
-------Tobias Weltner
39
Cofi Heidecke
4
function
----management
security
If you want to save changes you applied to XML data, call the Save() method:
$xmldata.Save("$env:temp\updateddata.xml")
function
----management
Cofi Heidecke
4
security
The result is pretty much the same as before, but XPath is very flexible and supports wildcards and additional
control. The next statement retrieves just the first employee node:
$xmldata.SelectNodes('staff/employee[1]')
Name
Age
-------Tobias Weltner
39
function
----management
If you'd like, you can get a list of all employees who are under the age of 18:
$xmldata.SelectNodes('staff/employee[age<18]')
Name
function
Age
-----------Cofi Heidecke
security
4
----Cofi Heidecke
Accessing Attributes
Attributes are pieces of information that describe an XML node. If you'd like to read the attributes of a node, use
Attributes:
$xmldata.staff.Attributes
#text
----Hanover
sales
function
----management
security
expert
-------
Dictionary
TableControl
ProcessModule
TableControl
process
TableControl
PSSnapInInfo
PSSnapInInfo
TableControl
Priority
TableControl
StartTime
TableControl
service
TableControl
(...)
ViewSelectedBy
ViewSelectedBy
ViewSelectedBy
ViewSelectedBy
ViewSelectedBy
ViewSelectedBy
ViewSelectedBy
ViewSelectedBy
To find out which views exist, take a look into the format.ps1xml files that describe the object type.
[xml]$file = Get-Content "$pshome\dotnettypes.format.ps1xml"
$view = @{ Name='ObjectType'; Expression= {$_.ViewSelectedBy.TypeName}}
$file.Configuration.ViewDefinitions.View | Select-Object Name, $view |
Where-Object { $_.Name -ne $_. ObjectType } | Sort-Object ObjectType
Name
---Dictionary
DateTime
Priority
StartTime
process
process
ProcessModule
DirectoryEntry
System.DirectoryServices.DirectoryEntry
PSSnapInInfo
System.Management.Automation.PSSnapI...
PSSnapInInfo
System.Management.Automation.PSSnapI...
service
System.ServiceProcess.ServiceController
ObjectType
---------System.Collections.DictionaryEntry
System.DateTime
System.Diagnostics.Process
System.Diagnostics.Process
System.Diagnostics.Process
System.Diagnostics.Process
System.Diagnostics.ProcessModule
Here you see all views defined in this XML file. The object types for which the views are defined are listed in the
second column. The Priority and StartTime views, which we just used, are on that list. However, the list just shows
views that use Table format. To get a complete list of all views, here is a more sophisticated example:
[xml]$file = Get-Content "$pshome\dotnettypes.format.ps1xml"
$view = @{ Name='ObjectType'; Expression= {$_.ViewSelectedBy.TypeName}}
$type = @{ Name='Type'; expression={if ($_.TableControl) { "Table" } elseif
($_.ListControl) {
"List" } elseif ($_.WideControl) { "Wide" } elseif ($_.CustomControl) {
"Custom" }}}
$file.Configuration.ViewDefinitions.View | Select-Object Name, $view, $type |
Sort-Object ObjectType | Group-Object ObjectType | Where-Object { $_.Count gt 1} |
ForEach-Object { $_.Group}
Name
---Dictionary
System.Collections.Dict...
System.Diagnostics.Even...
System.Diagnostics.Even...
System.Diagnostics.Even...
System.Diagnostics.Even...
System.Diagnostics.File...
System.Diagnostics.File...
Priority
process
StartTime
process
PSSnapInInfo
PSSnapInInfo
System.Reflection.Assembly
System.Reflection.Assembly
System.Security.AccessC...
System.Security.AccessC...
service
System.ServiceProcess.S...
System.TimeSpan
System.TimeSpan
System.TimeSpan
ObjectType
---------System.Collections.Dict...
System.Collections.Dict...
System.Diagnostics.Even...
System.Diagnostics.Even...
System.Diagnostics.Even...
System.Diagnostics.Even...
System.Diagnostics.File...
System.Diagnostics.File...
System.Diagnostics.Process
System.Diagnostics.Process
System.Diagnostics.Process
System.Diagnostics.Process
System.Management.Autom...
System.Management.Autom...
System.Reflection.Assembly
System.Reflection.Assembly
System.Security.AccessC...
System.Security.AccessC...
System.ServiceProcess.S...
System.ServiceProcess.S...
System.TimeSpan
System.TimeSpan
System.TimeSpan
Type
---Table
List
List
Table
Table
List
List
Table
Table
Wide
Table
Table
Table
List
Table
List
List
Table
Table
List
Wide
Table
List
Remember there are many format.ps1xml-files containing formatting information. You'll only get a complete list of
all view definitions when you generate a list for all of these files.
Working with files and folders is traditionally one of the most popular areas for administrators. PowerShell eases
transition from classic shell commands with the help of a set of predefined "historic" aliases and functions. So, if
you are comfortable with commands like "dir" or "ls" to list folder content, you can still use them. Since they are
just aliases - references to PowerShells own cmdlets - they do not necessarily work exactly the same anymore,
though.
In this chapter, you'll learn how to use PowerShell cmdlets to automate the most common file system tasks.
Topics Covered:
Name
---cli
clp
copy
cp
cpi
cpp
del
erase
gi
gp
ii
mi
move
mp
mv
ni
rd
ren
ri
rm
ModuleName
----------
Definition
---------Clear-Item
Clear-ItemProperty
Copy-Item
Copy-Item
Copy-Item
Copy-ItemProperty
Remove-Item
Remove-Item
Get-Item
Get-ItemProperty
Invoke-Item
Move-Item
Move-Item
Move-ItemProperty
Move-Item
New-Item
Remove-Item
Rename-Item
Remove-Item
Remove-Item
Alias
Alias
Alias
ItemProperty
Alias
ItemProperty
Alias
Alias
rmdir
rni
rnp
Remove-Item
Rename-Item
Rename-
rp
Remove-
si
sp
Set-Item
Set-ItemProperty
In addition, PowerShell provides a set of cmdlets that help dealing with path names. They all use the noun "Path",
and you can use these cmdlets to construct paths, split paths into parent and child, resolve paths or check whether
files or folders exist.
PS> Get-Command -Noun path
CommandType
----------Cmdlet
Cmdlet
Cmdlet
Cmdlet
Cmdlet
Name
---Convert-Path
Join-Path
Resolve-Path
Split-Path
Test-Path
ModuleName
---------Microsoft.PowerSh...
Microsoft.PowerSh...
Microsoft.PowerSh...
Microsoft.PowerSh...
Microsoft.PowerSh...
Definition
---------...
...
...
...
...
You also see functional differences because -Include only works right when you also use the -Recurse parameter.
The reason for these differences is the way these parameters work. -Filter is implemented by the underlying drive
provider, so it is retrieving only those files and folders that match the criteria in the first place. That's why -Filter is
fast and efficient. To be able to use -Filter, though, the drive provider must support it.
-Include on the contrary is implemented by PowerShell and thus is independent of provider implementations. It
works on all drives, no matter which provider is implementing that drive. The provider returns all items, and only
then does -Include filter out the items you want. This is slower but universal. -Filter currently only works for file
system drives. If you wanted to select items on Registry drives like HKLM:\ or HKCU:\, you must use -Include.
-Include has some advantages, too. It understands advanced wildcards and supports multiple search criteria:
# -Filter looks for all files that begin with "[A-F]" and finds none:
PS> Get-ChildItem $home -Filter [a-f]*.ps1 -Recurse
# -Include understands advanced wildcards and looks for files that begin with
a-f and
# end with .ps1:
PS> Get-ChildItem $home -Include [a-f]*.ps1 -Recurse
The counterpart to -Include is -Exclude. Use -Exclude if you would like to suppress certain files. Unlike -Filter, the Include and -Exclude parameters accept arrays, which enable you to get a list of all image files in your profile or the
windows folder:
Get-Childitem -Path $home, $env:windir -Recurse -Include *.bmp,*.png,*.jpg,
*.gif -ea 0
If you want to filter results returned by Get-ChildItem based on criteria other than file name, use Where-Object
(Chapter 5).
For example, to find the largest files in your profile, use this code - it finds all files larger than 100MB:
PS> Get-ChildItem $home -Recurse | Where-Object { $_.length -gt 100MB }
If you want to count files or folders, pipe the result to Measure-Object:
PS> Get-ChildItem $env:windir -Recurse -Include *.bmp,*.png,*.jpg, *.gif -ea
0 |
>> Measure-Object | Select-Object -ExpandProperty Count
>>
6386
You can also use Measure-Object to count the total folder size or the size of selected files. This line will count the
total size of all .log-files in your windows folder:
PS> Get-ChildItem $env:windir -Filter *.log -ea 0 | Measure-Object -Property
Length -Sum |
>> Select-Object -ExpandProperty Sum
PSDrive
PSProvider
PSIsContainer
VersionInfo
:
:
:
:
C
Microsoft.PowerShell.Core\FileSystem
False
File:
C:\Windows\explorer.exe
InternalName:
explorer
OriginalFilename: EXPLORER.EXE.MUI
FileVersion:
6.1.7600.16385 (win7_rtm.090713-1255)
FileDescription: Windows Explorer
Product:
Microsoft Windows Operating System
ProductVersion:
6.1.7600.16385
Debug:
False
Patched:
False
PreRelease:
False
PrivateBuild:
False
SpecialBuild:
False
Language:
English (United States)
BaseName
Mode
Name
Length
DirectoryName
Directory
IsReadOnly
Exists
FullName
Extension
CreationTime
CreationTimeUtc
LastAccessTime
LastAccessTimeUtc
LastWriteTime
LastWriteTimeUtc
Attributes
:
:
:
:
:
:
:
:
:
:
:
:
:
:
:
:
:
explorer
-a--explorer.exe
2871808
C:\Windows
C:\Windows
False
True
C:\Windows\explorer.exe
.exe
27.04.2011 17:02:33
27.04.2011 15:02:33
27.04.2011 17:02:33
27.04.2011 15:02:33
25.02.2011 07:19:30
25.02.2011 06:19:30
Archive
You can even change item properties provided the file or folder is not in use, you have the proper permissions, and
the property allows write access. Take a look at this piece of code:
"Hello" > $env:temp\testfile.txt
$file = Get-Item $env:temp\testfile.txt
$file.CreationTime
$file.CreationTime = '1812/4/11 09:22:11'
Explorer $env:temp
This will create a test file in your temporary folder, read its creation time and then changes the creation time to
November 4, 1812. Finally, explorer opens the temporary file so you can right-click the test file and open its
properties to verify the new creation time. Amazing, isn't it?
For example, the next code snippet finds all jpg files in your Windows folder and copies them to a new folder:
PS> New-Item -Path c:\WindowsPics -ItemType Directory -ea 0
PS> Get-ChildItem $env:windir -Filter *.jpg -Recurse -ea 0 |
>> Copy-Item -Destination c:\WindowsPics
Get-ChildItem first retrieved the files and then handed them over to Copy-Item which copied the files to a new
destination.
You can also combine the results of several separate Get-ChildItem commands. In the following example, two
separate Get-ChildItem commands generate two separate file listings, which PowerShell combines into a total list
and sends on for further processing in the pipeline. The example takes all the DLL files from the Windows system
directory and all program installation directories, and then returns a list with the name, version, and description of
DLL files:
PS> $list1 = @(Get-ChildItem $env:windir\system32\*.dll)
PS> $list2 = @(Get-ChildItem $env:programfiles -Recurse -Filter *.dll)
PS> $totallist = $list1 + $list2
PS> $totallist | Select-Object -ExpandProperty VersionInfo | Sort-Object Property FileName
ProductVersion
-------------3,0,0,2
2, 1, 0, 1
Sh...
2008.1108.641...
Sh...
(...)
FileVersion
----------3,0,0,2
2, 1, 0, 1
FileName
-------C:\Program Files\Bonjour\mdnsNSP.dll
C:\Program Files\Common Files\Microsoft
Where-Object can filter files according to other criteria as well. For example, use the following pipeline filter if
you'd like to locate only files that were created after May 12, 2011:
PS> Get-ChildItem $env:windir | Where-Object { $_.CreationTime -gt
[datetime]::Parse("May 12, 2011") }
You can use relative dates if all you want to see are files that have been changed in the last two weeks:
PS> Get-ChildItem $env:windir | Where-Object { $_.CreationTime -gt (GetDate).AddDays(-14) }
If you want to navigate to another location in the file system, use Set-Location or the Cd alias:
# One directory higher (relative):
PS> Cd ..
# In the parent directory of the current drive (relative):
PS> Cd \
# In a specified directory (absolute):
PS> Cd c:\windows
# Take directory name from environment variable (absolute):
PS> Cd $env:windir
# Take directory name from variable (absolute):
PS> Cd $home
Meaning
Current directory
Parent directory
Root directory
Home directory
Example
ii .
Cd ..
Cd \
Cd ~
Result
Opens the current directory in Windows Explorer
Changes to the parent directory
Changes to the topmost directory of a drive
Changes to the directory that PowerShell initially creates automatically
Table 15.2: Important special characters used for relative path specifications
Be careful though: Resolve-Path only works for files that actually exist. If there is no file in your current directory
that's called test.txt, Resolve-Path errors out.
Resolve-Path can also have more than one result if the path that you specify includes wildcard characters. The
following call will retrieve the names of all ps1xml files in the PowerShell home directory:
PS> Resolve-Path $PsHome\*.ps1xml
Path
---C:\Windows\System32\WindowsPowerShell\v1.0\Certificate.format.ps1xml
C:\Windows\System32\WindowsPowerShell\v1.0\DotNetTypes.format.ps1xml
C:\Windows\System32\WindowsPowerShell\v1.0\FileSystem.format.ps1xml
C:\Windows\System32\WindowsPowerShell\v1.0\Help.format.ps1xml
C:\Windows\System32\WindowsPowerShell\v1.0\PowerShellCore.format.ps1xml
C:\Windows\System32\WindowsPowerShell\v1.0\PowerShellTrace.format.ps1xml
C:\Windows\System32\WindowsPowerShell\v1.0\Registry.format.ps1xml
C:\Windows\System32\WindowsPowerShell\v1.0\types.ps1xml
hard-code these paths into your scripts - hardcoded system paths may run well on your machine and break on
another.
That's why it is important to understand where you can find the exact location of these folders. Some are covered by
the Windows environment variables, and others can be retrieved via .NET methods.
Special directory
Application data
User profile
Data used in common
Public directory
Program directory
Roaming Profiles
Temporary files (private)
Temporary files
Windows directory
Description
Application data locally stored on the machine
User directory
Directory for data used by all programs
Common directory of all local users
Directory in which programs are installed
Application data for roaming profiles
Directory for temporary files of the user
Directory for temporary files
Directory in which Windows is installed
Access
$env:localappdata
$env:userprofile
$env:commonprogramfiles
$env:public
$env:programfiles
$env:appdata
$env:tmp
$env:temp
$env:windir
Table 15.3: Important Windows directories that are stored in environment variables
Environment variables cover only the most basic system paths. If you'd like to put a file directly on a users
Desktop, you'll need the path to the Desktop which is missing in the list of environment variables. The
GetFolderPath() method of the System.Environment class of the .NET framework (Chapter 6) can help. The
following code illustrates how you can put a link on the Desktop.
PS> [Environment]::GetFolderPath("Desktop")
C:\Users\Tobias Weltner\Desktop
# Put a link on the Desktop:
PS> $path = [Environment]::GetFolderPath("Desktop") + "\EditorStart.lnk"
PS> $comobject = New-Object -ComObject WScript.Shell
PS> $link = $comobject.CreateShortcut($path)
PS> $link.targetpath = "notepad.exe"
PS> $link.IconLocation = "notepad.exe,0"
PS> $link.Save()
To get a list of system folders known by GetFolderPath(), use this code snippet:
PS> [System.Environment+SpecialFolder] | Get-Member -Static -MemberType
Property
TypeName: System.Environment+SpecialFolder
Name
MemberType
------------ApplicationData
Property
ApplicationData {get;}
CommonApplicationData Property
CommonApplicationData ...
CommonProgramFiles
Property
CommonProgramFiles {get;}
Cookies
Property
Cookies {get;}
Definition
---------static System.Environment+SpecialFolder
static System.Environment+SpecialFolder
static System.Environment+SpecialFolder
static System.Environment+SpecialFolder
Desktop
Property
Desktop {get;}
DesktopDirectory
Property
DesktopDirectory {get;}
Favorites
Property
Favorites {get;}
History
Property
History {get;}
InternetCache
Property
InternetCache {get;}
LocalApplicationData Property
LocalApplicationData {...
MyComputer
Property
MyComputer {get;}
MyDocuments
Property
MyDocuments {get;}
MyMusic
Property
MyMusic {get;}
MyPictures
Property
MyPictures {get;}
Personal
Property
Personal {get;}
ProgramFiles
Property
ProgramFiles {get;}
Programs
Property
Programs {get;}
Recent
Property
Recent {get;}
SendTo
Property
SendTo {get;}
StartMenu
Property
StartMenu {get;}
Startup
Property
Startup {get;}
System
Property
System {get;}
Templates
Property
Templates {get;}
static System.Environment+SpecialFolder
static System.Environment+SpecialFolder
static System.Environment+SpecialFolder
static System.Environment+SpecialFolder
static System.Environment+SpecialFolder
static System.Environment+SpecialFolder
static System.Environment+SpecialFolder
static System.Environment+SpecialFolder
static System.Environment+SpecialFolder
static System.Environment+SpecialFolder
static System.Environment+SpecialFolder
static System.Environment+SpecialFolder
static System.Environment+SpecialFolder
static System.Environment+SpecialFolder
static System.Environment+SpecialFolder
static System.Environment+SpecialFolder
static System.Environment+SpecialFolder
static System.Environment+SpecialFolder
static System.Environment+SpecialFolder
And this would get you a list of all system folders covered plus their actual paths:
PS> [System.Environment+SpecialFolder] | Get-Member -Static -MemberType
Property |
>> ForEach-Object {"{0,-25}= {1}" -f $_.name,
[Environment]::GetFolderPath($_.Name) }
>>
ApplicationData
= C:\Users\Tobias Weltner\AppData\Roaming
CommonApplicationData
= C:\ProgramData
CommonProgramFiles
= C:\Program Files\Common Files
Cookies
= C:\Users\Tobias
Weltner\AppData\Roaming\Microsoft\Windows\Cookies
Desktop
= C:\Users\Tobias Weltner\Desktop
DesktopDirectory
= C:\Users\Tobias Weltner\Desktop
Favorites
= C:\Users\Tobias Weltner\Favorites
History
= C:\Users\Tobias
Weltner\AppData\Local\Microsoft\Windows\History
InternetCache
= C:\Users\Tobias
Weltner\AppData\Local\Microsoft\Windows\Temporary Internet Files
LocalApplicationData
= C:\Users\Tobias Weltner\AppData\Local
MyComputer
=
MyDocuments
= C:\Users\Tobias Weltner\Documents
MyMusic
= C:\Users\Tobias Weltner\Music
MyPictures
= C:\Users\Tobias Weltner\Pictures
Personal
= C:\Users\Tobias Weltner\Documents
ProgramFiles
= C:\Program Files
Programs
= C:\Users\Tobias
Weltner\AppData\Roaming\Microsoft\Windows\Start Menu\Programs
Recent
= C:\Users\Tobias
Weltner\AppData\Roaming\Microsoft\Windows\Recent
SendTo
= C:\Users\Tobias
Weltner\AppData\Roaming\Microsoft\Windows\SendTo
StartMenu
= C:\Users\Tobias
Weltner\AppData\Roaming\Microsoft\Windows\Start Menu
Startup
= C:\Users\Tobias
Weltner\AppData\Roaming\Microsoft\Windows\
Start Menu\Programs\Startup
System
= C:\Windows\system32
Templates
= C:\Users\Tobias
Weltner\AppData\Roaming\Microsoft\Windows\Templates
You can use this to create a pretty useful function that maps drives to all important file locations. Here it is:
function Map-Profiles {
[System.Environment+SpecialFolder] | Get-Member -Static -MemberType Property
|
ForEach-Object {
New-PSDrive -Name $_.Name -PSProvider FileSystem -Root
([Environment]::GetFolderPath($_.Name)) `
-Scope Global
}
}
Map-Profiles
When you run this function, it adds a bunch of new drives. You can now easily take a look at your browser cookies or even get rid of them:
PS> Get-ChildItem cookies:
PS> Get-ChildItem cookies: | del -WhatIf
Note that all custom drives are added only for your current PowerShell session. If you want to use them daily, make
sure you add Map-Profiles and its call to your profile script:
PS> if ((Test-Path $profile) -eq $false) { New-Item $profile -ItemType File Force }
PS> Notepad $profile
Constructing Paths
Path names are plain-text, so you can set them up any way you like. To put a file onto your desktop, you could add
the path segments together using string operations:
PS> $path = [Environment]::GetFolderPath("Desktop") + "\file.txt"
PS> $path
C:\Users\Tobias Weltner\Desktop\file.txt
A more robust way is using Join-Path because it keeps track of the backslashes:
PS> $path = Join-Path ([Environment]::GetFolderPath("Desktop")) "test.txt"
PS> $path
C:\Users\Tobias Weltner\Desktop\test.txt
The System.IO.Path class includes a number of additionally useful methods that you can use to put together paths or
extract information from paths. Just prepend [System.IO.Path]:: to methods listed in Table 15.4, for example:
PS> [System.IO.Path]::ChangeExtension("test.txt", "ps1")
test.ps1
Method
ChangeExtension()
Combine()
GetDirectoryName()
GetExtension()
GetFileName()
Description
Changes the file extension
Combines path strings;
corresponds to Join-Path
Returns the directory;
corresponds to Split-Path parent
Returns the file extension
Returns the file name;
corresponds to Split-Path -leaf
Example
ChangeExtension("test.txt", "ps1")
Combine("C:\test", "test.txt")
GetDirectoryName("c:\test\file.txt")
GetExtension("c:\test\file.txt")
GetFileName("c:\test\file.txt")
GetFileNameWithoutExtension()
GetFullPath()
GetInvalidFileNameChars()
GetInvalidPathChars()
GetPathRoot()
GetRandomFileName()
GetTempFileName()
GetTempPath()
HasExtension()
IsPathRooted()
GetFileNameWithoutExtension("c:\test\file.txt")
GetFullPath(".\test.txt")
GetInvalidFileNameChars()
GetInvalidPathChars()
GetPathRoot("c:\test\file.txt")
GetRandomFileName()
GetTempFileName()
GetTempPath()
HasExtension("c:\test\file.txt")
IsPathRooted("c:\test\file.txt")
LastWriteTime
------------12.10.2011
17:14
Length Name
------ ---Test1
LastWriteTime
------------12.10.2011
17:14
Length Name
------ ---Test2
You can also create several sub-directories in one step as PowerShell automatically creates all the directories that
don't exist yet in the specified path:
PS> md test\subdirectory\somethingelse
Three folders will be created with one line.
LastWriteTime
------------10.12.2011
17:16
Length Name
------ ---0 new file.txt
If you add the -Force parameter, creating new files with New-Item becomes even more interesting - and a bit
dangerous, too. The -Force parameter will overwrite any existing file, but it will also make sure that the folder the
file is to be created it exists. So, New-Item can create several folders plus a file if you use -Force.
Another way to create files is to use old-fashioned redirection using the ">" and ">>" operators, Set-Content or OutFile.
Get-ChildItem > info1.txt
.\info1.txt
Get-ChildItem | Out-File info2.txt
.\info2.txt
Get-ChildItem | Set-Content info3.txt
.\info3.txt
Set-Content info4.txt (Get-Date)
.\info4.txt
As it turns out, redirection and Out-File work very similar: when PowerShell converts pipeline results, file contents
look just like they would if you output the information in the console. Set-Content works differently: it does not use
PowerShells sophisticated ETS (Extended Type System) to convert objects into text. Instead, it converts objects into
text by using their own private ToString() method - which provides much less information. That is because SetContent is not designed to convert objects into text. Instead, this cmdlet is designed to write text to a file.
You can use all of these cmdlets to create text files. For example, ConvertTo-HTML produces HTML but does not
write it to a file. By sending that information to Out-File, you can create HTML- or HTA-files and display them.
PS>
PS>
PS>
PS>
If you want to control the "columns" (object properties) that are converted into HTML, simply use Select-Object
(Chapter 5):
Get-ChildItem | Select-Object name, length, LastWriteTime | ConvertTo-HTML |
Out-File report.htm
.\report.htm
If you rather want to export the result as a comma-separated list, use Export-Csv cmdlet instead of ConvertToHTML | Out-File. Don't forget to use its -UseCulture parameter to automatically use the delimiter that is right for
your culture.
To add content to an existing file, again you can use various methods. Either use the appending redirection operator
">>", or use Add-Content. You can also pipe results to Out-File and use its -Append parameter to make sure it does
not overwrite existing content.
There is one thing you should keep in mind, though: do not mix these methods, stick to one. The reason is that they
all use different default encodings, and when you mix encodings, the result may look very strange:
PS> Set-Content info.txt "First line"
PS> "Second line" >> info.txt
PS> Add-Content info.txt "Third line"
PS> Get-Content info.txt
First Line
S e c o n d L i n e
Third line
All three cmdlets support the -Encoding parameter that you can use to manually pick an encoding. In contrast, the
old redirection operators have no way of specifying encoding which is why you should avoid using them.
You can also use -Wait with Get-Content to turn the cmdlet into a monitoring mode: once it read the entire file, it
keeps monitoring it, and when new content is appended to the file, it is immediately processed and returned by GetContent. This is somewhat similar to "tailing" a file in Unix.
Finally, you can use Select-String to filter information based on keywords and regular expressions. The next line
gets only those lines from the windowsupdate.log file that contain the phrase " successfully installed ":
PS> Get-Content $env:windir\windowsupdate.log | Select-String "successfully
installed"
Note that Select-String will change the object type to a so-called MatchInfo object. That's why when you forward the
filtered information to a file, the result lines are cut into pieces:
PS> Get-Content $env:windir\windowsupdate.log -Encoding UTF8 | Select-String
"successfully installed" |
>> Out-File $env:temp\report.txt
>>
PS> Invoke-Item $env:temp\report.txt
To turn the results delivered by Select-String into real text, make sure you pick the property Line from the MatchInfo
object which holds the text line that matched your keyword:
PS> Get-Content $env:windir\windowsupdate.log -Encoding UTF8 | Select-String
"successfully installed" |
>> Select-Object -ExpandProperty Line | Out-File $env:temp\report.txt
>>
PS> Invoke-Item $env:temp\report.txt
Bulk Renames
Because Rename-Item can be used as a building block in the pipeline, it provides simple solutions to complex tasks.
For example, if you wanted to remove the term -temporary from a folder and all its sub-directories, as well as all
the included files, this instruction will suffice:
PS> Get-ChildItem | ForEach-Object { Rename-Item $_.Name $_.Name.Replace('temporary', '') }
This line would now rename all files and folders, even if the term '"-temporary" you're looking for isn't even in the
file name. So, to speed things up and avoid errors, use Where-Object to focus only on files that carry the keyword in
its name:
PS> Get-ChildItem | Where-Object { $_.Name -like "*-temporary" } |
>> ForEach-Object { Rename-Item $_.Name $_.Name.replace('-temporary', '') }
Rename-Item even accepts a script block, so you could use this code as well:
PS> Get-ChildItem | $_.Name -like '*-temporary' } | Rename-Item {
$_.Name.replace('-temporary', '') }
When you look at the different code examples, note that ForEach-Object is needed only when a cmdlet cannot
handle the input from the upstream cmdlet directly. In these situations, use ForEach-Object to manually feed the
incoming information to the appropriate cmdlet parameter.
Most file system-related cmdlets are designed to work together. That's why Rename-Item knows how to interpret the
output from Get-ChildItem. It is "Pipeline-aware" and does not need to be wrapped in ForEach-Object.
Directory: Microsoft.PowerShell.Core\FileSystem::C:\Users\Tobias
Weltner\Sources\docs
Mode
---d----
LastWriteTime
------------13.10.2011
13:31
Length Name
------ ---testdirectory
ii
Cmdlet
Add-Content
Clear-Host
Clear-Item
Copy-Item
Get-Childitem
Get-Content
Get-Item
GetItemProperty
Invoke-Item
Join-Path
Move-Item
New-Item
Remove-Item
Rename-Item
Resolve-Path
Set-ItemProperty
Set-Location
Split-Path
Test-Path
Using Providers
o Available Providers
o Creating Drives
o Searching for Keys
o Reading One Registry Value
o Reading Multiple Registry Values
o Reading Multiple Keys and Values
o Creating Registry Keys
o Deleting Registry Keys
o Creating Values
o Securing Registry Keys
o Taking Ownership
o Setting New Access Permissions
o Removing an Access Rule
o Controlling Access to Sub-Keys
o Revealing Inheritance
o Controlling Your Own Inheritance
The Registry stores many crucial Windows settings. That's why it's so cool to read and sometimes change
information in the Windows Registry: you can manage a lot of configuration settings and sometimes tweak
Windows in ways that are not available via the user interface.
However, if you mess things up - change the wrong values or deleting important settings - you may well
permanently damage your installation. So, be very careful, and don't change anything that you do not know well.
Using Providers
To access the Windows Registry, there are no special cmdlets. Instead, PowerShell ships with a so-called provider
named "Registry". A provider enables a special set of cmdlets to access data stores. You probably know these
cmdlets already: they are used to manage content on drives and all have the keyword "item" in their noun part:
PS> Get-Command -Noun Item*
CommandType
----------Cmdlet
Cmdlet
Cmdlet
Cmdlet
Cmdlet
Cmdlet
Cmdlet
Cmdlet
Cmdlet
Cmdlet
Cmdlet
Cmdlet
Cmdlet
Cmdlet
Name
---Clear-Item
Clear-ItemProperty
Copy-Item
Copy-ItemProperty
Get-Item
Get-ItemProperty
Invoke-Item
Move-Item
Move-ItemProperty
New-Item
New-ItemProperty
Remove-Item
Remove-ItemProperty
Rename-Item
ModuleName
---------Microsoft.PowerSh...
Microsoft.PowerSh...
Microsoft.PowerSh...
Microsoft.PowerSh...
Microsoft.PowerSh...
Microsoft.PowerSh...
Microsoft.PowerSh...
Microsoft.PowerSh...
Microsoft.PowerSh...
Microsoft.PowerSh...
Microsoft.PowerSh...
Microsoft.PowerSh...
Microsoft.PowerSh...
Microsoft.PowerSh...
Definition
---------...
...
...
...
...
...
...
...
...
...
...
...
...
...
Cmdlet
Cmdlet
Cmdlet
Rename-ItemProperty
Set-Item
Set-ItemProperty
Microsoft.PowerSh... ...
Microsoft.PowerSh... ...
Microsoft.PowerSh... ...
Many of these cmdlets have historic aliases, and when you look at those, the cmdlets probably become a lot more
familiar:
PS> Get-Alias -Definition *-Item*
CommandType
----------Alias
Alias
Alias
Alias
Alias
Alias
Alias
Alias
Alias
Alias
Alias
Alias
Alias
Alias
Alias
Alias
Alias
Alias
Alias
Alias
Alias
Alias
Alias
ItemProperty
Alias
ItemProperty
Alias
Alias
Name
---cli
clp
copy
cp
cpi
cpp
del
erase
gi
gp
ii
mi
move
mp
mv
ni
rd
ren
ri
rm
rmdir
rni
rnp
ModuleName
----------
Definition
---------Clear-Item
Clear-ItemProperty
Copy-Item
Copy-Item
Copy-Item
Copy-ItemProperty
Remove-Item
Remove-Item
Get-Item
Get-ItemProperty
Invoke-Item
Move-Item
Move-Item
Move-ItemProperty
Move-Item
New-Item
Remove-Item
Rename-Item
Remove-Item
Remove-Item
Remove-Item
Rename-Item
Rename-
rp
Remove-
si
sp
Set-Item
Set-ItemProperty
Thanks to the "Registry" provider, all of these cmdlets (and their aliases) can also work with the Registry. So if you
wanted to list the keys of HKEY_LOCAL_MACHINE\Software, this is how you'd do it:
Dir HKLM:\Software
Available Providers
Get-PSProvider gets a list of all available providers. Your list can easily be longer than in the following example.
Many PowerShell extensions add additional providers. For example, the ActiveDirectory module that ships with
Windows Server 2008 R2 (and the RSAT tools for Windows 7) adds a provider for the Active Directory. Microsoft
SQL Server (starting with 2007) comes with an SQLServer provider.
Get-PSProvider
Name
---Alias
Environment
FileSystem
function
Registry
Variable
Certificate
Capabilities
-----------ShouldProcess
ShouldProcess
filter, ShouldProcess
ShouldProcess
ShouldProcess
ShouldProcess
ShouldProcess
Drives
-----{Alias}
{Env}
{C, E, S, D}
{function}
{HKLM, HKCU}
{Variable}
{cert}
What's interesting here is the Drives column, which lists the drives that are managed by a respective provider. As
you see, the registry provider manages the drives HKLM: (for the registry root HKEY_LOCAL_MACHINE) and
HKCU: (for the registry root HKEY_CURRENT_USER). These drives work just like traditional file system drives.
Check this out:
Cd HKCU:
Dir
Hive: Microsoft.PowerShell.Core\Registry::HKEY_CURRENT_USER
SKC VC Name
Property
--- -- ----------2
0 AppEvents
{}
7
1 Console
{CurrentPage}
15
0 Control Panel
{}
0
2 Environment
{TEMP, TMP}
4
0 EUDC
{}
1
6 Identities
{Identity Ordinal, Migrated7, Last ...
3
0 Keyboard Layout
{}
0
0 Network
{}
4
0 Printers
{}
38
1 Software
{(default)}
2
0 System
{}
0
1 SessionInformation
{ProgramCount}
1
8 Volatile Environment
{LOGONSERVER, USERDOMAIN, USERNAME,...
You can navigate like in the file system and dive deeper into subfolders (which here really are registry keys).
Provider
Description
Manages aliases, which enable you to address a command under another
Alias
name. You'll learn more about aliases in Chapter 2.
Provides access to the environment variables of the system. More in Chapter
Environment
3.
Lists all defined functions. Functions operate much like macros and can
Function
combine several commands under one name. Functions can also be an
alternative to aliases and will be described in detail in Chapter 9.
FileSystem
Registry
Variable
Manages all the variables that are defined in the PowerShell console.
Variables are covered in Chapter 3.
Example
Dir Alias:
$alias:Dir
Dir env:
$env:windir
Dir function:
$function:tabexpansion
Dir c:
$(c:\autoexec.bat)
Dir HKCU:
Dir HKLM:
Dir variable:
$variable:pshome
Certificate
Provides access to the certificate store with all its digital certificates. These
are examined in detail in Chapter 10.
Dir cert:
Dir cert: -recurse
Creating Drives
PowerShell comes with two drives built-in that point to locations in the Windows Registry: HKLM: and HKCU:.
Get-PSDrive -PSProvider Registry
Name
Provider
Root
CurrentLocation
---------------------------HKCU
Registry
HKEY_CURRENT_USER
HKLM
Registry
HKEY_LOCAL_MACHINE
That's a bit strange because when you open the Registry Editor regedit.exe, you'll see that there are more than just
two root hives. If you wanted to access another hive, let's say HKEY_USERS, you'd have to add a new drive like
this:
New-PSDrive -Name HKU -PSProvider Registry -Root HKEY_USERS
Dir HKU:
You may not have access to all keys due to security settings, but your new drive HKU: works fine. Using NewPSDrive, you now can access all parts of the Windows Registry. To remove the drive, use Remove-PSDrive (which
only works if HKU: is not the current drive in your PowerShell console):
Remove-PSDrive HKU
You can of course create additional drives that point to specific registry keys that you may need to access often.
New-PSDrive InstalledSoftware registry
'HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall'
Dir InstalledSoftware:
Note that PowerShell drives are only visible inside the session you defined them. Once you close PowerShell, they
will automatically get removed again. To keep additional drives permanently, add the New-PSDrive statements to
your profile script so they get automatically created once you launch PowerShell.
Dir Registry::HKEY_CLASSES_ROOT\.ps1
With this technique, you can even list all the Registry hives:
Dir Registry::
Note that this example searches both HKCU: and HKLM:. The error action is set to SilentlyContinue because in the
Registry, you will run into keys that are access-protected and would raise ugly "Access Denied" errors. All errors
are suppressed that way.
Unfortunately, the Registry provider adds a number of additional properties so you don't get back the value alone.
Add another Select-Object to really get back only the content of the value you are after:
PS> Get-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\Windows
NT\CurrentVersion' -Name RegisteredOwner |
>> Select-Object -ExpandProperty RegisteredOwner
Tim Telbert
EditionID
--------Ultimate
CSDVersion
---------Service Pack 1
RegisteredOwner
--------------Tim Telbert
EditionID
--------Ultimate
CSDVersion
---------Service Pack 1
RegisteredOwner
--------------Tim Telbert
Yet maybe you want to read values not just from one Registry key but rather a whole bunch of them. In
HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall, you find a lot of keys, one for each installed
software product. If you wanted to get a list of all software installed on your machine, you could read all of these
keys and display some values from them.
That again is just a minor adjustment to the previous code because Get-ItemProperty supports wildcards. Have a
look:
PS> Get-ItemProperty -Path
'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*' |
>> Select-Object -Property DisplayName, DisplayVersion, UninstallString
DisplayName
----------Microsoft IntelliPoint 8.1
{3ED4AD...
Microsoft Security Esse...
Files\Micro...
NVIDIA Drivers
C:\Windows\system32\nv...
WinImage
Files\WinI...
Microsoft Antimalware
/X{05BFB06...
Windows XP Mode
/X{1374CC6...
Windows Home Server-Con...
/I{21E4979...
Idera PowerShellPlus Pr...
/I{7a71c8a...
Intel(R) PROSet/Wireles...
(...)
DisplayVersion
--------------
UninstallString
---------------
0.8.2.232
8.15.406.0
msiexec.exe /I
2.1.1116.0
C:\Program
1.9
"C:\Program
3.0.8402.2
MsiExec.exe
1.3.7600.16422
MsiExec.exe
6.0.3436.0
MsiExec.exe
4.0.2703.2
MsiExec.exe
13.01.1000
Voil, you get a list of installed software. Some of the lines are empty, though. This occurs when a key does not
have the value you are looking for.
To remove empty entries, simply add Where-Object like this:
PS> Get-ItemProperty -Path
'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*' |
>> Select-Object -Property DisplayName, DisplayVersion, UninstallString |
>> Where-Object { $_.DisplayName -ne $null }
Hive: Registry::HKEY_CURRENT_USER\Software
Name
---NewKey1
PS> md HKCU:\Software\NewKey2
Property
--------
Hive: Registry::HKEY_CURRENT_USER\Software
Name
---NewKey2
Property
--------
If a key name includes blank characters, enclose the path in quotation marks. The parent key has to exist.
To create a new key with a default value, use New-Item and specify the value and its data type:
PS> New-Item HKCU:\Software\NewKey3 -Value 'Default Value Text' -Type String
Hive: Registry::HKEY_CURRENT_USER\Software
Name
---NewKey3
Property
-------(default) : Default Value Text
This process needs to be manually confirmed if the key you are about to remove contains other keys:
Del HKCU:\Software\KeyWithSubKeys
Confirm
The item at "HKCU:\Software\KeyWithSubKeys" has children and the Recurse
parameter was not specified. if you continue, all children will be removed
with the item. Are you sure you want to continue?
[Y] Yes [A] Yes to All [N] No [L] No to All [S] Suspend [?] Help
(default is "Y"):
Use the Recurse parameter to delete such keys without manual confirmation:
Creating Values
Each Registry key can have an unlimited number of values. Earlier in this chapter, you learned how to read these
values. Values are called "ItemProperties", so they belong to an "Item", the Registry key.
To add new values to a Registry key, either use New-ItemProperty or Set-ItemProperty. New-ItemProperty cannot
overwrite an existing value and returns the newly created value in its object form. Set-ItemProperty is more easy
going. If the value does not yet exist, it will be created, else changed. Set-ItemProperty does not return any object.
Here are some lines of code that first create a Registry key and then add a number of values with different data
types:
PS> New-Item HKCU:\Software\TestKey4
PS> Set-ItemProperty HKCU:\Software\TestKey4
PS> Set-ItemProperty HKCU:\Software\TestKey4
PS> Set-ItemProperty HKCU:\Software\TestKey4
Type ExpandString
PS> Set-ItemProperty HKCU:\Software\TestKey4
Note','Second Note' `
>> -Type MultiString
>>
PS> Set-ItemProperty HKCU:\Software\TestKey4
4,8,12,200,90 -Type Binary
PS> Get-ItemProperty HKCU:\Software\TestKey4
Name
ID
Path
Notes
DigitalInfo
PSPath
PSParentPath
PSChildName
PSDrive
PSProvider
:
:
:
:
:
:
:
:
:
:
Smith
12
C:\Windows
{First Note, Second Note}
{4, 8, 12, 200...}
Registry::HKEY_CURRENT_USER\Software\TestKey4
Registry::HKEY_CURRENT_USER\Software
TestKey4
HKCU
Registry
If you wanted to set the keys' default value, use '(default)' as value name.
ItemType
String
ExpandString
Binary
DWord
MultiString
QWord
Description
A string
A string with environment variables that are resolved when invoked
Binary values
Numeric values
Text of several lines
64-bit numeric values
DataType
REG_SZ
REG_EXPAND_SZ
REG_BINARY
REG_DWORD
REG_MULTI_SZ
REG_QWORD
Use Remove-ItemProperty to remove a value. This line deletes the value Name value that you created in the previous
example:
Remove-ItemProperty HKCU:\Software\Testkey4 Name
Clear-ItemProperty clears the content of a value, but not the value itself.
Be sure to delete your test key once you are done playing:
Remove-Item HKCU:\Software\Testkey4 -Recurse
Access
------
To apply new security settings to a key, you need to know the different access rights that can be assigned to a key.
Here is how you get a list of these rights:
PS> [System.Enum]::GetNames([System.Security.AccessControl.RegistryRights])
QueryValues
SetValue
CreateSubKey
EnumerateSubKeys
Notify
CreateLink
Delete
ReadPermissions
WriteKey
ExecuteKey
ReadKey
ChangePermissions
TakeOwnership
FullControl
Taking Ownership
Always make sure that you are the owner of the key before modifying Registry key access permissions. Only
owners can recover from lock-out situations, so if you set permissions wrong, you may not be able to undo the
changes unless you are the owner of the key.
This is how to take ownership of a Registry key (provided your current access permissions allow you to take
ownership. You may want to run these examples in a PowerShell console with full privileges):
The modifications immediately take effect.Try creating new subkeys in the Registry editor or from within
PowerShell, and youll get an error message:
md HKCU:\Software\Testkey\subkey
New-Item : Requested Registry access is not allowed.
At line:1 char:34
+ param([string[]]$paths); New-Item <<<< -type directory -path $paths
Why does the restriction applies to you as an administrator? Aren't you supposed to have full access? No,
restrictions always have priority over permissions, and because everyone is a member of the Everyone group, the
restriction applies to you as well. This illustrates that you should be extremely careful applying restrictions. A better
approach is to assign permissions only.
$inheritance = [System.Security.AccessControl.InheritanceFlags]"None"
$propagation = [System.Security.AccessControl.PropagationFlags]"None"
$type = [System.Security.AccessControl.AccessControlType]"Deny"
$rule = New-Object System.Security.AccessControl.RegistryAccessRule(`
$person,$access,$inheritance,$propagation,$type)
$acl.RemoveAccessRule($rule)
Set-Acl HKCU:\Software\Testkey $acl -Force
However, removing your access rule may not be as straightforward because you have effectively locked yourself
out. Since you no longer have modification rights to the key, you are no longer allowed to modify the keys' security
settings as well.
You can overrule this only if you take ownership of the key: Open the Registry editor, navigate to the key, and by
right-clicking and then selecting Permissions open the security dialog box and manually remove the entry for
Everyone.
Youve just seen how relatively easy it is to lock yourself out. Be careful with restriction rules.
Note that in this case the new rules were not entered by using AddAccessRule() but by ResetAccessRule(). This
results in removal of all existing permissions for respective users. Still, the result isnt right because regular users
could still create subkeys and write values:
md hkcu:\software\Testkey2\Subkey
Hive:
Microsoft.PowerShell.Core\Registry::HKEY_CURRENT_USER\software\Testkey2
SKC
--0
VC Name
-- ---0 Subkey
Property
-------{}
Revealing Inheritance
Look at the current permissions of the key to figure out why your permissions did not work the way you planned:
(Get-Acl HKCU:\Software\Testkey2).Access | Format-Table -Wrap
RegistryRights AccessControlType IdentityReference IsInherited
InheritanceFlags PropagationFlags
-------------- ----------------- ----------------- ----------- --------------- ---------------ReadKey
Allow Everyone
False
None
None
FullControl
Allow BUILT-in\Admi
False
None
None
nistrators
FullControl
Allow TobiasWeltne-PC\T
True
ContainerInherit,
None
obias Weltner
ObjectInherit
FullControl
Allow NT AUTHORITY\SYST
True
ContainerInherit,
None
EM
ObjectInherit
FullControl
Allow BUILT-in\Admi
True
ContainerInherit,
None
nistrators
ObjectInherit
ReadKey
Allow NT AUTHORITY\REST
True
ContainerInherit,
None
RICTED ACCESS
ObjectInherit
The key includes more permissions than what you assigned to it. It gets these additional permissions by inheritance
from parent keys. If you want to turn off inheritance, use SetAccessRuleProtection():
$acl = Get-Acl HKCU:\Software\Testkey2
$acl.SetAccessRuleProtection($true, $false)
Set-Acl HKCU:\Software\Testkey2 $acl
Now, when you look at the permissions again, the key now contains only the permissions you explicitly set. It no
longer inherits any permissions from parent keys:
------------
In your daily work as an administrator, you will probably often deal with applications (processes), services, and
event logs so let's take some of the knowledge you gained from the previous chapters and play with it. The examples
and topics covered in this chapter are meant to give you an idea of what you can do. By no means are they a
complete list of what you can do. They will provide you with a great starting point, though.
Process objects returned from Get-Process contain a lot more information that you can see when you pipe the result
to Select-Object and have it display all object properties:
PS> Get-Process | Select-Object *
You can then examine the object properties available, and put together your own reports by picking the properties
that you need:
PS> Get-Process | Select-Object Name, Description, Company, MainWindowTitle
Name
---AppleMobileDevic...
conhost
csrss
csrss
DataCardMonitor
Dropbox
dwm
(...)
Description
-----------
Company
-------
MainWindowTitle
---------------
When you do that, you'll notice that there may be blank lines. They occur when a process object has no information
for the particular property you selected. For example, the property MainWindowTitle represents the text in the title
bar of an application window. So, if a process has no application window, MainWindowTitle is empty.
You can use the standard pipeline cmdlets to take care of that. Use Where-Object to filter out processes that do not
meet your requirements. For example, this line will get you only processes that do have an application window:
PS> Get-Process | Where-Object { $_.MainWindowTitle -ne '' } |
>> Select-Object Description, MainWindowTitle, Name, Company
>>
Description
----------DataCardMonitor ...
Technolog...
Remote Desktop C...
Corpor...
Windows PowerShell
Corpor...
Microsoft Office...
Corpor...
MainWindowTitle
--------------DataCardMonitor
Name
---DataCardMonitor
Company
------Huawei
Microsoft
Windows PowerShell
Microsoft
powershell
eBook_Chap17_V2.... WINWORD
Microsoft
Note that you can also retrieve information about processes by using WMI:
PS> Get-WmiObject Win32_Process
WMI will get you even more details about running processes.
Both Get-Process and Get-WmiObject support the parameter -ComputerName, so you can use both to retrieve
processes remotely from other machines. However, only Get-WmiObject also supports the parameter -Credential so
you can authenticate. Get-Process always uses your current identity, and unless you are Domain Administrator or
otherwise have local Administrator privileges at the target machine, you will get an Access Denied error.
Note that even with Get-Process, you can authenticate. Establish an IPC network connection to the target machine,
and use this connection for authentication. Here is an example:
PS> net use \\someRemoteMachine Password /USER:domain\username
Here are some more examples of using pipeline cmdlets to refine the results returned by Get-Process. Can you
decipher what these lines would do?
PS> Get-Process | Where-Object { $_.StartTime -gt (Get-Date).AddMinutes(180)}
PS> @(Get-Process notepad -ea 0).Count
PS> Get-Process | Measure-Object -Average -Maximum -Minimum -Property
PagedSystemMemorySize
Each Process object contains methods and properties. As discussed in detail in Chapter 6, many properties may be
read as well as modified, and methods can be executed like commands. This allows you to control many fine
settings of processes. For example, you can specifically raise or lower the priority of a process. The next statement
lowers the priority of all Notepads:
PS> Get-Process notepad | ForEach-Object { $_.PriorityClass = "BelowNormal" }
This works great, but eventually you'll run into situations where you cannot seem to launch an application.
PowerShell might complain that it would not recognize the application name although you know for sure that it
exists.
When this happens, you need to specify the absolute or relative path name to the application file. That can become
tricky because in order to escape spaces in path names, you have to quote them, and in order to run quoted text (and
not echo it back), you need to prepend it with an ampersand. The ampersand tells PowerShell to treat the text as if it
was a command you entered.
So if you wanted to run Internet Explorer from its standard location, this is the line that would do the job:
& 'C:\Program Files\Internet Explorer\iexplore.exe'
When you run applications from within PowerShell, these are the rules to know:
Environment variable $env:path: All folders listed in $env:path are special. Applications stored inside
these folders can be launched by name only. You do not need to specify the complete or relative path.
That's the reason why you can simply enter notepad and press ENTER to launch the Windows Editor, or
run commands like ping or ipconfig.
Escaping Spaces: If the path name contains spaces, the entire path name needs to be quoted. Once you
quote a path, though, it becomes a string (text), so when you press ENTER, PowerShell happily echoes the
text back to you but won't start the application. Whenever you quote paths, you need to prepend the string
with "&" so PowerShell knows that you want to launch something.
Synchronous and asynchronous execution: when you run a console-based application such as
ipconfig.exe or netstat.exe, it shares the console with PowerShell so its output is displayed in the
PowerShell console. That's why PowerShell pauses until console-based applications finished. Windowbased applications such as notepad.exe or regedit.exe use their own windows for output. Here, PowerShell
continues immediately and won't wait for the application to complete.
Using Start-Process
Whenever you need to launch a new process and want more control, use Start-Process. This cmdlet has a number of
benefits over launching applications directly. First of all, it is a bit smarter and knows where a lot of applications are
stored. It can for example find iexplore.exe without the need for a path:
Stopping Processes
If you must kill a process immediately, use Stop-Process and specify either the process ID, or use the parameter Name to specify the process name. This would close all instances of the Notepad:
PS> Stop-Process -Name Notepad
Stopping processes this way shouldnt be done on a regular basis: since the application is immediately terminated, it
has no time to save unsaved results (which might result in data loss), and it cannot properly clean up (which might
result in orphaned temporary files and inaccurate open DLL counters). Use it only if a process won't respond
otherwise. Use WhatIf to simulate. Use Confirm when you want to have each step confirmed.
To close a process nicely, you can close its main window (which is the automation way of closing the application
window by a mouse click). Here is a sample that closes all instances of notepad:
PS> Get-Process Notepad -ea 0 | ForEach-Object { $_.CloseMainWindow() }
Managing Services
Services are basically processes, too. They are just executed automatically and in the background and do not
necessarily require a user logon. Services provide functionality usually not linked to any individual user.
Cmdlet
Get-Service
New-Service
Restart-Service
Resume-Service
Set-Service
Start-Service
Stop-Service
Suspend-Service
Description
Lists services
Registers a service
Stops a service and then restarts it. For example, to allow modifications of settings to take effect
Resumes a stopped service
Modifies settings of a service
Starts a service
Stops a service
Suspends a service
Examining Services
Use Get-Service to list all services and check their basic status.
PS> Get-Service
You can also check an individual service and find out whether it is running or not:
PS> Get-Service Spooler
ExitCode
log, anyone can now use it to log events. You could for example use this line inside of your logon scripts to log
status information:
PS> Write-EventLog -LogName Application -Source PowerShellScript -EntryType
Information `
>> -EventId 123 -Message 'This is my first own event log entry'
You can now use Get-EventLog to read back your entries:
PS> Get-EventLog -LogName Application -Source PowerShellScript
Index Time
----- ---163833 Nov 14 10:47
is...
EntryType
Source
-------------Information PowerShellScript
InstanceID Message
---------- ------123 This
Or you can open the system dialog to view your new event entry that way:
PS> Show-EventLog
And of course you can remove your event source if this was just a test and you want to get rid of it again (but you do
need administrator privileges again, just like when you created the event source):
PS> Remove-EventLog -Source PowerShellScript
Windows Management Instrumentation (WMI) is a technique available on all Windows systems starting with
Windows 2000. WMI can provide you with a wealth of information about the Windows configuration and setup. It
works both locally and remotely, and PowerShell makes accessing WMI a snap.
A "class" pretty much is like the "kind of an animal". There are dogs, cats, horses, and each kind is a class. So there
is always only one class of a kind.
An "object" works like an "animal", so there are zillions of real dogs, cats, and horses. So, there may be one, ten,
thousands, or no objects (or "instances") of a class. Let's take the class "mammoth". There are no instances of this
class these days.
WMI works the same. If you'd like to know something about a computer, you ask WMI about a class, and WMI
returns the objects. When you ask for the class "Win32_BIOS", you get back exactly one instance (or object)
because your computer has just one BIOS. When you ask for "Win32_Share", you get back a number of instances,
one for each share. And when you ask for "Win32_TapeDrive", you get back nothing because most likely, your
computer has no built-in tape drive. Tape drives thus work like mammoths in the real world. While there is a class
("kind"), there is no more instance.
Retrieving Information
How do you ask WMI for objects? It's easy! Just use the cmdlet Get-WmiObject. It accepts a class name and returns
objects, just like the cmdlet name and its parameter suggest:
PS> Get-WmiObject -Class Win32_BIOS
SMBIOSBIOSVersion
Manufacturer
Name
SerialNumber
Version
:
:
:
:
:
RKYWSF21
Phoenix Technologies LTD
Phoenix TrustedCore(tm) NB Release SP1 1.0
701KIXB007922
PTLTD - 6040000
Methods
------{}
Properties
---------{BitsPerPel,
{}
{Pause, Resume}
{Element, Setting}
{Caption, Color,
{ByteCount,
Win32_PrinterShare
Depend...
Win32_PrinterDriverDll
Depend...
Win32_PrinterController
Antec...
{}
{Antecedent,
{}
{Antecedent,
{}
{AccessState,
:
:
:
:
:
02LV.MP00.20081121.hkk
Phoenix Technologies Ltd.
Phoenix SecureCore(tm) NB Version 02LV.MP00.20081121.hkk
ZAMA93HS600210
SECCSD - 6040000
To see the red-pill-world, pipe the results to Select-Object and ask it to show all available properties:
PS> Get-WmiObject -Class Win32_BIOS | Select-Object -Property *
Status
Name
: OK
: Phoenix SecureCore(tm) NB Version
02LV.MP00.20081121.hkk
Caption
: Phoenix SecureCore(tm) NB Version
02LV.MP00.20081121.hkk
SMBIOSPresent
: True
__GENUS
: 2
__CLASS
: Win32_BIOS
__SUPERCLASS
: CIM_BIOSElement
__DYNASTY
: CIM_ManagedSystemElement
__RELPATH
: Win32_BIOS.Name="Phoenix SecureCore(tm) NB Version
02LV.MP00.20081121.hkk",SoftwareElementID="Phoenix
SecureCore(tm) NB Version
02LV.MP00.20081121.hkk",Softw
areElementState=3,TargetOperatingSystem=0,Version="SECC
SD - 6040000"
__PROPERTY_COUNT
: 27
__DERIVATION
: {CIM_BIOSElement, CIM_SoftwareElement,
CIM_LogicalElement, CIM_ManagedSystemElement}
__SERVER
: DEMO5
__NAMESPACE
: root\cimv2
__PATH
: \\DEMO5\root\cimv2:Win32_BIOS.Name="Phoenix
SecureCore(tm) NB Version
02LV.MP00.20081121.hkk",SoftwareElementID="Phoenix
SecureCore(tm) NB Version
02LV.MP00.20081121.hkk",Softw
areElementState=3,TargetOperatingSystem=0,Version="SECC
SD - 6040000"
BiosCharacteristics
: {4, 7, 8, 9...}
BIOSVersion
: {SECCSD - 6040000, Phoenix SecureCore(tm) NB Version
02LV.MP00.20081121.hkk, Ver 1.00PARTTBL}
BuildNumber
:
CodeSet
:
CurrentLanguage
:
Description
: Phoenix SecureCore(tm) NB Version
02LV.MP00.20081121.hkk
IdentificationCode
:
InstallableLanguages :
InstallDate
:
LanguageEdition
:
ListOfLanguages
:
Manufacturer
: Phoenix Technologies Ltd.
OtherTargetOS
:
PrimaryBIOS
: True
ReleaseDate
: 20081121000000.000000+000
SerialNumber
: ZAMA93HS600210
SMBIOSBIOSVersion
: 02LV.MP00.20081121.hkk
SMBIOSMajorVersion
: 2
SMBIOSMinorVersion
: 5
SoftwareElementID
: Phoenix SecureCore(tm) NB Version
02LV.MP00.20081121.hkk
SoftwareElementState : 3
TargetOperatingSystem : 0
Version
: SECCSD - 6040000
Scope
: System.Management.ManagementScope
Path
: \\DEMO5\root\cimv2:Win32_BIOS.Name="Phoenix
SecureCore(tm) NB Version
02LV.MP00.20081121.hkk",SoftwareElementID="Phoenix
SecureCore(tm) NB Version
02LV.MP00.20081121.hkk",Softw
areElementState=3,TargetOperatingSystem=0,Version="SECC
SD - 6040000"
Options
: System.Management.ObjectGetOptions
ClassPath
: \\DEMO5\root\cimv2:Win32_BIOS
Properties
: {BiosCharacteristics, BIOSVersion, BuildNumber,
Caption...}
SystemProperties
: {__GENUS, __CLASS, __SUPERCLASS, __DYNASTY...}
Qualifiers
: {dynamic, Locale, provider, UUID}
Site
:
Container
:
Once you see the real world, you can pick the properties you find interesting and then put together a custom
selection. Note that PowerShell adds a couple of properties to the object which all start with "__". These properties
are available on all WMI objects. __Server is especially useful because it always reports the name of the computer
system the WMI object came from. Once you start retrieving WMI information remotely, you should always add
__Server to the list of selected properties.
PS> Get-WmiObject Win32_BIOS | Select-Object __Server, Manufacturer,
SerialNumber, Version
__SERVER
-------DEMO5
Manufacturer
SerialNumber
----------------------Phoenix Technolo... ZAMA93HS600210
Version
------SECCSD - 6040000
MACAddress
---------00:13:77:B9:F2:64
20:41:53:59:4E:FF
00:22:FA:D9:E1:50
AdapterType
----------Ethernet 802.3
Wide Area Network (WAN)
Ethernet 802.3
Client-side filtering is easy because it really just uses Where-Object to pick out those objects that fulfill a given
condition. However, it is slightly inefficient as well. All WMI objects need to travel to your computer first before
PowerShell can pick out the ones you want.
If you only expect a small number of objects and/or if you are retrieving objects from a local machine, there is no
need to create more efficient code. If however you are using WMI remotely via network and/or have to deal with
hundreds or even thousands of objects, you should instead use server-side filters.
These filters are transmitted to WMI along with your query, and WMI only returns the wanted objects in the first
place. Since these filters are managed by WMI and not PowerShell, they use WMI syntax and not PowerShell
syntax. Have a look:
PS> Get-WmiObject Win32_NetworkAdapter -Filter 'MACAddress != NULL' |
>> Select-Object Name, MACAddress, AdapterType
>>
Name
---Intel(R) 82567LM-Gigabi...
RAS Async Adapter
Intel(R) WiFi Link 5100...
MACAddress
---------00:13:77:B9:F2:64
20:41:53:59:4E:FF
00:22:FA:D9:E1:50
AdapterType
----------Ethernet 802.3
Wide Area Network (WAN)
Ethernet 802.3
Simple filters like the one above are almost self-explanatory. WMI uses different operators ("!=" instead of "-ne" for
inequality) and keywords ("NULL" instead of $null), but the general logic is the same.
Sometimes, however, WMI filters can be tricky. For example, to find all network cards that have an IP address
assigned to them, in PowerShell (using client-side filtering) you would use:
The reason for this is the nature of the IPAddress property. When you look at the results from your client-side
filtering, you'll notice that the column IPAddress has values in braces and displays more than one IP address. The
property IPAddress is an array. WMI filters cannot check array contents.
So in this scenario, you would have to either stick to client-side filtering or search for another object property that is
not an array and could still separate network cards with IP address from those without. There happens to be a
property called IPEnabled that does just that:
PS> Get-WmiObject Win32_NetworkAdapterConfiguration -Filter 'IPEnabled =
true' |
>> Select-Object Caption, IPAddress, MACAddress
>>
Caption
IPAddress
MACAddress
-----------------------[00000011] Intel(R) WiF... {192.168.2.109, fe80::a... 00:22:FA:D9:E1:50
A special WMI filter operator is "LIKE". It works almost like PowerShells comparison operator -like. Use "%"
instead of "*" for wildcards, though. So, to find all services with the keyword "net" in their name, try this:
PS> Get-WmiObject Win32_Service -Filter 'Name LIKE "%net%"' | Select-Object
Name, DisplayName, State
Name
---aspnet_state
Net Driver HPZ12
Netlogon
Netman
NetMsmqActivator
NetPipeActivator
netprofm
NetTcpActivator
NetTcpPortSharing
DisplayName
----------ASP.NET-Zustandsdienst
Net Driver HPZ12
Netlogon
Network Connections
Net.Msmq Listener Adapter
Net.Pipe Listener Adapter
Network List Service
Net.Tcp Listener Adapter
Net.Tcp Port Sharing Se...
State
----Stopped
Stopped
Running
Running
Stopped
Stopped
Running
Stopped
Stopped
WMPNetworkSvc
PowerShell supports the [WmiSearcher] type accelerator, which you can use to achieve basically the same thing you
just did with the query parameter:
$searcher = [WmiSearcher]"select caption,commandline from Win32_Process where
name like 'p%'"
$searcher.Get()| Format-Table [a-z]* -Wrap
The path consists basically of the class name as well as one or more key properties. For services, the key property is
Name and is the English-language name of the service. If you want to work directly with a particular service through
WMI, specify its path and do a type conversion. Use either the [wmi] type accelerator or the underlying
[System.Management.ManagementObject] .NET type:
[wmi]"Win32_Service.Name='Fax'"
ExitCode : 1077
Name
: Fax
ProcessId : 0
StartMode : Manual
State
: Stopped
Status
: OK
In fact, you dont necessarily need to specify the name of the key property as long as you at least specify its value.
This way, youll find all the properties of a specific WMI instance right away.
$disk = [wmi]'Win32_LogicalDisk="C:"'
$disk.FreeSpace
10181373952
[int]($disk.FreeSpace / 1MB)
9710
$disk | Format-List [a-z]*
Status
Availability
DeviceID
StatusInfo
Access
BlockSize
Caption
Compressed
ConfigManagerErrorCode
ConfigManagerUserConfig
CreationClassName
Description
DriveType
ErrorCleared
ErrorDescription
ErrorMethodology
FileSystem
FreeSpace
InstallDate
LastErrorCode
MaximumComponentLength
MediaType
Name
NumberOfBlocks
PNPDeviceID
PowerManagementCapabilities
PowerManagementSupported
ProviderName
Purpose
QuotasDisabled
QuotasIncomplete
QuotasRebuilding
Size
SupportsDiskQuotas
SupportsFileBasedCompression
SystemCreationClassName
SystemName
VolumeDirty
VolumeName
VolumeSerialNumber
:
:
:
:
:
:
:
:
:
:
:
:
:
:
:
:
:
:
:
:
:
:
:
:
:
:
:
:
:
:
:
:
:
:
:
:
:
:
:
:
C:
0
C:
False
Win32_LogicalDisk
Local hard drive
3
NTFS
10181373952
255
12
C:
100944637952
False
True
Win32_ComputerSystem
JSMITH-PC
AC039C05
Modifying Properties
Most of the properties that you find in WMI objects are read-only. There are few, though, that can be modified. For
example, if you want to change the description of a drive, add new text to the VolumeName property of the drive:
$drive = [wmi]"Win32_LogicalDisk='C:'"
$drive.VolumeName = "My Harddrive"
$drive.Put()
Path
: \\.\root\cimv2:Win32_LogicalDisk.DeviceID="C:"
RelativePath : Win32_LogicalDisk.DeviceID="C:"
Server
: .
NamespacePath : root\cimv2
ClassName
: Win32_LogicalDisk
IsClass
: False
IsInstance
: True
IsSingleton
: False
If youd rather check your hard disk drive C:\ for errors, the proper invocation is:
([wmi]"Win32_LogicalDisk='C:'").Chkdsk(...
However, since this method requires additional arguments, the question here is what you should specify. Invoke the
method without parentheses in order to get initial brief instructions:
([wmi]"Win32_LogicalDisk='C:'").Chkdsk
MemberType
: Method
OverloadDefinitions : {System.Management.ManagementBaseObject
Chkdsk(System.Boolean FixErrors, System.Boolean
VigorousIndexCheck, System.Boolean SkipFolderCycle,
System.Boolean ForceDismount, Syst
em.Boolean RecoverBadSectors, System.Boolean
OkToRunAtBootUp)}
TypeNameOfValue
: System.Management.Automation.PSMethod
Value
: System.Management.ManagementBaseObject
Chkdsk(System.Boolean FixErrors, System.Boolean
VigorousIndexCheck, System.Boolean SkipFolderCycle,
System.Boolean ForceDismount, Syste
m.Boolean RecoverBadSectors, System.Boolean
OkToRunAtBootUp)
Name
: Chkdsk
IsInstance
: True
Static Methods
There are WMI methods not just in WMI objects that you retrieved with Get-WmiObject. Some WMI classes also
support methods. These methods are called "static".
If you want to renew the IP addresses of all network cards, use the Win32_NetworkAdapterConfiguration class and
its static method RenewDHCPLeaseAll():
([wmiclass]"Win32_NetworkAdapterConfiguration").RenewDHCPLeaseAll().ReturnVal
ue
You get the WMI class by using type conversion. You can either use the [wmiclass] type accelerator or the
underlying [System.Management.ManagementClass].
The methods of a WMI class are also documented in detail inside WMI. For example, you get the description of the
Win32Shutdown() method of the Win32_OperatingSystem class like this:
$class = [wmiclass]'Win32_OperatingSystem'
$class.Options.UseAmendedQualifiers = $true
(($class.methods["Win32Shutdown"]).Qualifiers["Description"]).Value
The Win32Shutdown method provides the full set of shutdown options supported
by Win32
operating systems. The method returns an integer value that can be
interpretted as follows:
0 Successful completion.
Other for integer values other than those listed above, refer to Win32
error code documentation.
If youd like to learn more about a WMI class or a method, navigate to an Internet search page like Google and
specify as keyword the WMI class name, as well as the method. Its best to limit your search to the Microsoft
MSDN pages: Win32_NetworkAdapterConfiguration RenewDHCPLeaseAll site:msdn2.microsoft.com.
In a similarly way, all the properties of the class are documented. The next example retrieves the documentation for
the property VolumeDirty and explains what its purpose is:
$class = [wmiclass]'Win32_LogicalDisk'
$class.psbase.Options.UseAmendedQualifiers = $true
($class.psbase.properties["VolumeDirty"]).Type
Boolean
(($class.psbase.properties["VolumeDirty"]).Qualifiers["Description"]).Value
The VolumeDirty property indicates whether the disk requires chkdsk to be run
at next boot up time.
The property is applicable to only those instances of logical disk that
represent a physical disk in
the machine. It is not applicable to mapped logical drives.
WMI Events
WMI returns not only information but can also wait for certain events. If the events occur, an action will be started.
In the process, WMI can alert you when one of the following things involving a WMI instance happens:
__InstanceCreationEvent: A new instance was added such as a new process was started or a new file
created.
__InstanceModificationEvent: The properties of an instance changed. For example, the FreeSpace
property of a drive was modified.
__InstanceDeletionEvent: An instance was deleted, such as a program was shut down or a file deleted.
__InstanceOperationEvent: This is triggered in all three cases.
You can use these to set up an alarm signal. For example, if you want to be informed as soon as Notepad is started,
type:
Select * from __InstanceCreationEvent WITHIN 1 WHERE targetinstance ISA
'Win32_Process' AND
targetinstance.name = 'notepad.exe'
WITHIN specifies the time interval of the inspection and WITHIN 1 means that you want to be informed no later
than one second after the event occurs. The shorter you set the interval, the more effort involved, which means that
WMI will require commensurately more computing power to perform your task. As long as the interval is kept at
not less than one second, the computation effort will be scarcely perceptible. Here is an example:
$alarm = New-Object Management.EventQuery
$alarm.QueryString = "Select * from __InstanceCreationEvent WITHIN 1 WHERE
targetinstance ISA 'Win32_Process' AND `
targetinstance.name = 'notepad.exe'"
$watch = New-Object Management.ManagementEventWatcher $alarm
Start Notepad after issuing a wait command:
$result = $watch.WaitForNextEvent()
Get target instance of Notepad:
$result.targetinstance
Access the live instance:
$path = $result.targetinstance.__path
$live = [wmi]$path
# Close Notepad using the live instance
$live.terminate()
WMI comes with built-in remoting so you can retrieve WMI objects not just from your local machine but also
across the network. WMI uses "traditional" remoting techniques like DCOM which are also used by the Microsoft
Management Consoles.
To be able to use WMI remoting, your network must support DCOM calls (thus, the firewall needs to be set up
accordingly). Also, you need to have Administrator privileges on the target machine.
In addition to the built-in remoting capabilities, you can use Get-WmiObject via PowerShell Remoting (if you have
set up PowerShell Remoting correctly). Here, you send the WMI command off to the remote system:
Invoke-Command { Get-WmiObject Win32_BIOS } -ComputerName server12, server16
Note that all objects returned by PowerShell Remoting are read-only and do not contain methods anymore. If you
want to change WMI properties or call WMI methods, you need to do this inside the script block you send to the
remote system - so it needs to be done before PowerShell Remoting sends back objects to your own system.
directory
ServiceModel
MSAPPS12
aspnet
Policy
SecurityCenter
Microsoft
As you see, the cimv2 directory is only one of them. What other directories are shown here depends on the software
and hardware that you use. For example, if you use Microsoft Office, you may find a directory called MSAPPS12.
Take a look at the classes in it:
Get-WmiObject -Namespace root\msapps12 -List | Where-Object {
$_.Name.StartsWith("Win32_") }
Win32_PowerPoint12Tables
Win32_Publisher12PageNumber
Win32_Publisher12Hyperlink
Win32_PowerPointSummary
Win32_Word12Fonts
Win32_PowerPointActivePresentation
Win32_OutlookDefaultFileLocation
Win32_Word12Document
Win32_ExcelAddIns
Win32_PowerPoint12Table
Win32_ADOCoreComponents
Win32_Publisher12SelectedTable
Win32_Word12CharacterStyle
Win32_Word12Styles
Win32_OutlookSummary
Win32_Word12DefaultFileLocation
Win32_WordComAddins
Win32_PowerPoint12AlternateStartupLoc
Win32_OutlookComAddins
Win32_ExcelCharts
Win32_Word12Settings
Win32_FrontPageActiveWeb
Win32_OdbcDriver
Win32_AccessProject
Win32_Word12StartupFileLocation
Win32_ExcelActiveWorkbook
Win32_FrontPagePageProperty
Win32_Publisher12MailMerge
Win32_Language
Win32_FrontPageAddIns
Win32_Word12PageSetup
Win32_Word12HeaderAndFooter
Win32_ServerExtension
Win32_Publisher12ActiveDocumentNoTable
Win32_Word12Addin
Win32_WordComAddin
Win32_PowerPoint12PageNumber
Win32_JetCoreComponents
Win32_Publisher12Fonts
Win32_Word12Table
Win32_OutlookAlternateStartupFile
Win32_Word12Tables
Win32_Access12ComAddins
Win32_Excel12AlternateStartupFileLoc
Win32_Word12FileConverters
Win32_Access12StartupFolder
Win32_Word12ParagraphStyle
Win32_Access12ComAddin
Win32_Excel12StartupFolder
Win32_PowerPointPresentation
Win32_FrontPageWebProperty
Win32_Publisher12Table
Win32_Publisher12StartupFolder
Win32_WebConnectionErrorText
Win32_ExcelSheet
Win32_Publisher12Tables
Win32_FrontPageTheme
Win32_PowerPoint12ComAddins
Win32_Word12Template
Win32_Access12AlternateStartupFileLoc
Win32_Word12ActiveDocument
Win32_PublisherSummary
Win32_Publisher12DefaultFileLocation
Win32_Word12Field
Win32_Publisher12Hyperlinks
Win32_PowerPoint12ComAddin
Win32_PowerPoint12Hyperlink
Win32_PowerPoint12DefaultFileLoc
Win32_Publisher12Sections
Win32_OutlookStartupFolder
Win32_Access12JetComponents
Win32_Word12ActiveDocumentNotable
Win32_Publisher12CharacterStyle
Win32_Word12Hyperlinks
Win32_Word12FileConverter
Win32_PowerPoint12Hyperlinks
Win32_FrontPageActivePage
Win32_OleDbProvider
Win32_Publisher12PageSetup
Win32_Word12SelectedTable
Win32_PowerPoint12StartupFolder
Win32_OdbcCoreComponent
Win32_PowerPoint12PageSetup
Win32_FrontPageSummary
Win32_Word12Hyperlink
Win32_Publisher12Font
Win32_WebConnectionErrorMessage
Win32_AccessDatabase
Win32_Publisher12Styles
Win32_Publisher12ActiveDocument
Win32_Word12AlternateStartupFileLocation
Win32_PowerPoint12Fonts
Win32_ExcelComAddin
Win32_Excel12DefaultFileLoc
Win32_Word12Fields
Win32_ExcelActiveWorkbookNotable
Win32_Publisher12COMAddIn
Win32_OutlookComAddin
Win32_FrontPageAddIn
Win32_WebConnectionError
Win32_RDOCoreComponents
Win32_Publisher12ParagraphStyle
Win32_Publisher12COMAddIns
Win32_Transport
Win32_Access12DefaultFileLoc
Win32_FrontPageThemes
Win32_ExcelAddIn
Win32_Publisher12AlternateStartupFileLocation
Win32_PowerPoint12SelectedTable
Win32_ExcelComAddins
Win32_Word12MailMerge
Win32_Word12Summary
Win32_AccessSummary
Win32_OfficeWatsonLog
Win32_Word12Sections
Win32_ExcelWorkbook
Win32_PowerPoint12Font
Win32_ExcelChart
Win32_Word12Font
Win32_Word12PageNumber
Win32_ExcelSummary
The date and time are represented a sequence of numbers: first the year, then the month, and finally the day.
Following this is the time in hours, minutes, and milliseconds, and then the time zone. This is the so-called DMTF
standard, which is hard to read. However, you can use ToDateTime() of the ManagementDateTimeConverter .NET
class to decipher this cryptic format:
$boottime = (Get-WmiObject win32_OperatingSystem).LastBootUpTime
$boottime
20111016085609.375199+120
$realtime =
[System.Management.ManagementDateTimeConverter]::ToDateTime($boottime)
$realtime
Tuesday, October 16, 2011 8:56:09 AM
Now you can also use standard date and time cmdlets such as New-TimeSpan to calculate the current system uptime:
New-TimeSpan $realtime (Get-Date)
Days
: 0
Hours
: 6
Minutes
: 47
Seconds
: 9
Milliseconds
: 762
Ticks
: 244297628189
TotalDays
: 0.282751884478009
TotalHours
: 6.78604522747222
TotalMinutes
: 407.162713648333
TotalSeconds
: 24429.7628189
TotalMilliseconds : 24429762.8189
User administration in the Active Directory was a dark spot in PowerShell Version 1. Microsoft did not ship any
cmdlets to manage AD user accounts or other aspects in Active Directory. That's why the 3rd party vendor Quest
stepped in and published a free PowerShell Snap-In with many useful AD cmdlets. Over the years, this extension
has grown to become a de-facto standard, and many PowerShell scripts use Quest AD cmdlets. You can freely
download this extension from the Quest website.
Beginning with PowerShell Version 2.0, Microsoft finally shipped their own AD management cmdlets. They are
included with Server 2008 R2 and also available for download as "RSAT tools (remote server administration
toolkit). The AD cmdlets are part of a module called "ActiveDirectory". This module is installed by default when
you enable the Domain Controller role on a server. On a member server or client with installed RSAT tools, you
have to go to control panel and enable that feature first.
This chapter is not talking about either one of these extensions. It is introducing you to the build-in low level support
for ADSI methods. They are the beef that makes these two extensions work and can be called directly, as well.
Don't get me wrong: if you work a lot with the AD, it is much easier for you to get one of the mentioned AD
extensions and use cmdlets for your tasks. If you (or your scripts) just need to get a user, change some attributes or
determine group membership details, it can be easier to use the direct .NET framework methods shown in this
chapter. They do not introduce dependencies: your script runs without the need to either install the Quest toolkit or
the RSAT tools.
Topics Covered:
Connecting to a Domain
o Logging On Under Other User Names
Accessing a Container
o Listing Container Contents
Accessing Individual Users or Groups
o Using Filters and the Pipeline
o Directly Accessing Elements
o Obtaining Elements from a Container
o Searching for Elements
Table 19.1: Examples of LDAP queries
o Accessing Elements Using GUID
Reading and Modifying Properties
o Just What Properties Are There?
o Practical Approach: Look
o Theoretical Approach: Much More Thorough
o Reading Properties
o Modifying Properties
o Deleting Properties
Table 19.2: PutEx() operations
o The Schema of Domains
o Setting Properties Having Several Values
Invoking Methods
o Changing Passwords
o Controlling Group Memberships
o In Which Groups Is a User a Member?
o Which Users Are Members of a Group?
o Adding Users to a Group
Creating New Objects
o Creating New Organizational Units
o Create New Groups
Table 19.3: Group Types
o Creating New Users
Connecting to a Domain
If your computer is a member of a domain, the first step in managing users is to connect to a log-on domain. You
can set up a connection like this:
$domain = [ADSI]""
$domain
distinguishedName
----------------{DC=scriptinternals,DC=technet}
If your computer isnt a member of a domain, the connection setup will fail and generate an error message:
out-lineoutput : Exception retrieving member
"ClassId2e4f51ef21dd47e99d3c952918aff9cd":
"The specified domain either does not exist or could not be contacted."
If you want to manage local user accounts and groups, instead of LDAP: use the WinNT: moniker. But watch out:
the text is case-sensitive here. For example, you can access the local administrator account like this:
$user = [ADSI]"WinNT://./Administrator,user"
$user | Select-Object *
We wont go into local user accounts in any more detail in the following examples. If you must manage local users,
also look at net.exe. It provides easy to use options to manage local users and groups.
This is important to know when you want to log on under a different identity. The [ADSI] type accelerator always
logs you on using your current identity. Only the underlying DirectoryServices.DirectoryEntry .NET type gives you
the option of logging on with another identity. But why would anyone want to do something like that? Here are a
few reasons:
External consultant: You may be visiting a company as an external consultant and have brought along
your own notebook computer, which isnt a member of the company domain. This prevents you from
setting up a connection to the company domain. But if you have a valid user account along with its
password at your disposal, you can use your notebook and this identity to access the company domain.
Your notebook doesnt have to be a domain member to access the domain.
Several domains: Your company has several domains and you want to manage one of them, but it isnt
your log-on domain. More likely than not, youll have to log on to the new domain with an identity known
to it.
Logging onto a domain that isnt your own with another identity works like this:
$domain = new-object
DirectoryServices.DirectoryEntry("LDAP://10.10.10.1","domain\user", `
"secret")
$domain.name
scriptinternals
$domain.distinguishedName
DC=scriptinternals,DC=technet
Two things are important for ADSI paths: first, their names are case-sensitive. Thats why the two following
approaches are wrong:
$domain = [ADSI]"ldap://10.10.10.1"
# Wrong!
$useraccount = [ADSI]"Winnt://./Administrator,user"
# Wrong!
Second, surprisingly enough, ADSI paths use a normal slash. A backslash like the one commonly used in the file
system would generate error messages:
$domain = [ADSI]"LDAP:\\10.10.10.1"
$useraccount = [ADSI]"WinNT:\\.\Administrator,user"
# Wrong!
# Wrong!
If you dont want to put log-on data in plain text in your code, use Get-Credential. Since the password has to be
given when logging on in plain text, and Get-Credential returns the password in encrypted form, an intermediate
step is required in which it is converted into plain text:
$cred = Get-Credential
$pwd = [Runtime.InteropServices.Marshal]::PtrToStringAuto(
[Runtime.InteropServices.Marshal]::SecureStringToBSTR( $cred.Password ))
$domain = new-object
DirectoryServices.DirectoryEntry("LDAP://10.10.10.1",$cred.UserName, $pwd)
$domain.name
scriptinternals
Log-on errors are initially invisible. PowerShell reports errors only when you try to connect with a domain. This
procedure is known as binding. Calling the $domain.Name property wont cause any errors because when the
connection fails, there isnt even any property called Name in the object in $domain.
So, how can you find out whether a connection was successful or not? Just invoke the Bind() method, which does
the binding. Bind() always throws an exception and Trap can capture this error.
The code called by Bind() must be in its own scriptblock, which means it must be enclosed in brackets. If an error
occurs in the block, PowerShell will cut off the block and execute the Trap code, where the error will be stored in a
variable. This is created using script: so that the rest of the script can use the variable. Then If verifies whether an
error occurred. A connection error always exists if the exception thrown by Bind() has the -2147352570 error code.
In this event, If outputs the text of the error message and stops further instructions from running by using Break.
$cred = Get-Credential
$pwd = [Runtime.InteropServices.Marshal]::PtrToStringAuto(
[Runtime.InteropServices.Marshal]::SecureStringToBSTR( $cred.Password ))
$domain = new-object
DirectoryServices.DirectoryEntry("LDAP://10.10.10.1",$cred.UserName, $pwd)
trap { $script:err = $_ ; continue } &{ $domain.Bind($true); $script:err =
$null }
if ($err.Exception.ErrorCode -ne -2147352570)
{
Write-Host -Fore Red $err.Exception.Message
break
}
else
{
By the way, the error code -2147352570 means that although the connection was established, Bind() didnt find an
object to which it could bind itself. Thats OK because you didnt specify any particular object in your LDAP path
when the connection was being set up..
Accessing a Container
Domains have a hierarchical structure like the file system directory structure. Containers inside the domain are either
pre-defined directories or subsequently created organizational units. If you want to access a container, specify the
LDAP path to the container. For example, if you want to access the pre-defined directory Users, you could access
like this:
$ldap = "/CN=Users,DC=scriptinternals,DC=technet"
$cred = Get-Credential
$pwd = [Runtime.InteropServices.Marshal]::PtrToStringAuto(
[Runtime.InteropServices.Marshal]::SecureStringToBSTR( $cred.Password ))
$users = new-object
DirectoryServices.DirectoryEntry("LDAP://10.10.10.1$ldap",$cred.UserName,
$pwd)
$users
distinguishedName
----------------{CN=Users,DC=scriptinternals,DC=technet}
The fact that you are logged on as a domain member naturally simplifies the procedure considerably because now
you need neither the IP address of the domain controller nor log-on data. The LDAP name of the domain is also
returned to you by the domain itself in the distinguishedName property. All you have to do is specify the container
that you want to visit:
$ldap = "CN=Users"
$domain = [ADSI]""
$dn = $domain.distinguishedName
$users = [ADSI]"LDAP://$ldap,$dn"
$users
While in the LDAP language pre-defined containers use names including CN=, specify OU= for organizational
units. So, when you log on as a user to connect to the sales OU, which is located in the company OU, you should
type:
$ldap = "OU=sales, OU=company"
$domain = [ADSI]""
$dn = $domain.distinguishedName
$users = [ADSI]"LDAP://$ldap,$dn"
$users
At some point, youd like to know who or what the container contains to which you have set up a connection. The
approach here is somewhat less intuitive because now you need the PSBase object. PowerShell wraps Active
Directory objects and adds new properties and methods while removing others. Unfortunately, , PowerShell also in
the process gets rid of the necessary means to get to the contents of a container. PSBase returns the original (raw)
object just like PowerShell received it before conversion, and this object knows the Children property:
$ldap = "CN=Users"
$domain = [ADSI]""
$dn = $domain.distinguishedName
$users = [ADSI]"LDAP://$ldap,$dn"
$users.PSBase.Children
distinguishedName
----------------{CN=admin,CN=Users,DC=scriptinternals,DC=technet}
{CN=Administrator,CN=Users,DC=scriptinternals,DC=technet}
{CN=All,CN=Users,DC=scriptinternals,DC=technet}
{CN=ASPNET,CN=Users,DC=scriptinternals,DC=technet}
{CN=Belle,CN=Users,DC=scriptinternals,DC=technet}
{CN=Consultation2,CN=Users,DC=scriptinternals,DC=technet}
{CN=Consultation3,CN=Users,DC=scriptinternals,DC=technet}
{CN=ceimler,CN=Users,DC=scriptinternals,DC=technet}
(...)
Another approach makes use of the class that you can always find in the objectClass property.
$users.PSBase.Children | Select-Object -first 1 |
ForEach-Object { $_.sAMAccountName + $_.objectClass }
admin
top
person
organizationalPerson
user
As it happens, the objectClass property contains an array with all the classes from which the object is derived. The
listing process proceeds from the general to the specific so you can find only those elements that are derived from
the user class:
$users.PSBase.Children | Where-Object { $_.objectClass -contains "user" }
distinguishedName
----------------{CN=admin,CN=Users,DC=scriptinternals,DC=technet}
{CN=Administrator,CN=Users,DC=scriptinternals,DC=technet}
{CN=ASPNET,CN=Users,DC=scriptinternals,DC=technet}
{CN=Belle,CN=Users,DC=scriptinternals,DC=technet}
(...)
For example, if you want to access the Guest account directly, specify its distinguishedName. If youre a domain
member, you dont have to go to the trouble of using the distinguishedName of the domain:
$ldap = "CN=Guest,CN=Users"
$domain = [ADSI]""
$dn = $domain.distinguishedName
$guest = [ADSI]"LDAP://$ldap,$dn"
$guest | Format-List *
objectClass
: {top, person, organizationalPerson, user}
cn
: {Guest}
description
: {Predefined account for guest access to the computer
or domain)
distinguishedName
: {CN=Guest,CN=Users,DC=scriptinternals,DC=technet}
instanceType
: {4}
whenCreated
: {12.11.2005 12:31:31 PM}
whenChanged
: {06.27.2006 09:59:59 AM}
uSNCreated
: {System.__ComObject}
memberOf
: {CN=Guests,CN=Builtin,DC=scriptinternals,DC=technet}
uSNChanged
: {System.__ComObject}
name
: {Guest}
objectGUID
: {240 255 168 180 1 206 85 73 179 24 192 164 100 28
221 74}
userAccountControl
: {66080}
badPwdCount
: {0}
codePage
: {0}
countryCode
: {0}
badPasswordTime
: {System.__ComObject}
lastLogoff
: {System.__ComObject}
lastLogon
: {System.__ComObject}
logonHours
: {255 255 255 255 255 255 255 255 255 255 255 255 255
255 255 255 255 255 255 255 255
}
pwdLastSet
: {System.__ComObject}
primaryGroupID
: {514}
objectSid
: {1 5 0 0 0 0 0 5 21 0 0 0 184 88 34 189 250 183 7
172 165 75 78 29 245 1 0 0}
accountExpires
: {System.__ComObject}
logonCount
: {0}
sAMAccountName
: {Guest}
sAMAccountType
: {805306368}
objectCategory
:
{CN=Person,CN=Schema,CN=Configuration,DC=scriptinternals,DC=technet}
isCriticalSystemObject : {True}
nTSecurityDescriptor
: {System.__ComObject}
Using the asterisk as wildcard character, Format-List makes all the properties of an ADSI object visible so that you
can easily see which information is contained in it and under which names.
Once you have logged on to a domain that you want to search, you need only the following few lines to find all of
the user accounts that match the user name in $UserName. Wildcard characters are allowed:
$UserName = "*mini*"
$searcher = new-object DirectoryServices.DirectorySearcher([ADSI]"")
$searcher.filter = "(&(objectClass=user)(sAMAccountName= $UserName))"
$searcher.findall()
If you havent logged onto the domain that you want to search, get the domain object through the log-on:
$domain = new-object
DirectoryServices.DirectoryEntry("LDAP://10.10.10.1","domain\user","secret")
$UserName = "*mini*"
$searcher = new-object DirectoryServices.DirectorySearcher($domain)
$searcher.filter = "(&(objectClass=user)(sAMAccountName= $UserName))"
$searcher.findall() | Format-Table -wrap
The results of the search are all the objects that contain the string mini in their names, no matter where theyre
located in the domain:
Path
Properties
------------LDAP://10.10.10.1/CN=Administrator,CN=Users,DC=scripti {samaccounttype,
lastlogon, objectsid,
nternals,DC=technet
whencreated...}
The crucial part takes place in the search filter, which looks a bit strange in this example:
$searcher.filter = "(&(objectClass=user)(sAMAccountName= $UserName))"
The filter merely compares certain properties of elements according to certain requirements. It checks accordingly
whether the term user turns up in the objectClass property and whether the sAMAccountName property matches the
specified user name. Both criteria are combined by the & character, so they both have to be met. This would
enable you to assemble a convenient search function.
The search function Get-LDAPUser searches the current log-on domain by default. If you want to log on to another
domain, note the appropriate lines in the function and specify your log-on data.
function Get-LDAPUser([string]$UserName, [string]$Start)
{
# Use current logon domain:
$domain = [ADSI]""
# OR: log on to another domain:
#
$domain = new-object
DirectoryServices.DirectoryEntry("LDAP://10.10.10.1","domain\user",
# "secret")
if ($start -ne "")
{
$startelement = $domain.psbase.Children.Find($start)
}
else
{
$startelement = $domain
}
$searcher = new-object DirectoryServices.DirectorySearcher($startelement)
$searcher.filter = "(&(objectClass=user)(sAMAccountName=$UserName))"
$Searcher.CacheResults = $true
$Searcher.SearchScope = "Subtree"
$Searcher.PageSize = 1000
$searcher.findall()
}
Get-LDAPUser can be used very flexibly and locates user accounts everywhere inside the domain. Just specify the
name youre looking for or a part of it:
# Find all users who have an "e" in their names:
Get-LDAPUser *e*
# Find only users with "e" in their names that are in the "main office" OU or
come under it.
Get-LDAPUser *e* OU=main office,OU=company
Get-LDAPUser gets the found user objects right back. You can subsequently process them in the PowerShell
pipelinejust like the elements that you previously got directly from children. How does Get-LDAPUser manage to
search only the part of the domain you want it to? The following snippet of code is the reason:
if ($start -ne "")
{
$startelement = $domain.psbase.Children.Find($start)
}
else
{
$startelement = $domain
}
First, we checked whether the user specified the $start second parameter. If yes, Find() is used to access the
specified container in the domain container (of the topmost level) and this is defined as the starting point for the
search. If $start is missing, the starting point is the topmost level of the domain, meaning that every location is
searched.
The function also specifies some options that are defined by the user:
$Searcher.CacheResults = $true
$Searcher.SearchScope = "Subtree"
$Searcher.PageSize = 1000
SearchScope determines whether all child directories should also be searched recursively beginning from the
starting point, or whether the search should be limited to the start directory. PageSize specifies in which chunk the
results of the domain are to be retrieved. If you reduce the PageSize, your script may respond more freely, but will
also require more network traffic. If you request more, the respective chunk will still include only 1,000 data
records.
You could now freely extend the example function by extending or modifying the search filter. Here are some useful
examples:
Search Filter
(&(objectCategory=person)(objectClass=User))
(sAMAccountType=805306368)
(&(objectClass=user)(sn=Weltner)
(givenName=Tobias))
(&(objectCategory=person)(objectClass=user)
(msNPAllowDialin=TRUE))
(&(objectCategory=person)(objectClass=user)
(pwdLastSet=0))
(&(objectCategory=computer)(!description=*))
(&(objectCategory=person)(description=*))
(&(objectCategory=person)(objectClass=user)
(whenCreated>=20050318000000.0Z))
(&(objectCategory=person)(objectClass=user)
(|(accountExpires=9223372036854775807)
(accountExpires=0)))
(&(objectClass=user)(userAccountControl:
1.2.840.113556.1.4.803:=2))
(&(objectCategory=person)(objectClass=user)
(userAccountControl:1.2.840.113556.1.4.803:=32))
(&(objectClass=user)(!userAccountControl:
1.2.840.113556.1.4.803:=65536))
(&(objectCategory=group)(!groupType:
1.2.840.113556.1.4.803:=2147483648))
(&(objectCategory=Computer)(!userAccountControl
:1.2.840.113556.1.4.803:=8192))
Description
Find only user accounts, not computer accounts
Find only user accounts (much quicker, but harder to read)
Find user accounts with a particular name
Find user with dial-in permission
Find user who has to change password at next logon
Find all computer accounts having no description
Find all user accounts having no description
Find all elements created after March 18, 2005
Find all users whose account never expires (OR condition,
where only one condition must be met)
Find all disabled user accounts (bitmask logical AND)
Find all users whose password never expires
Find all users whose password expires (logical NOT using
"!")
Finding all distribution groups
Finding all computer accounts that are not domain controllers
Because the results returned by the search include no genuine user objects, but only reduced SearchResult objects,
you must first use GetDirectoryEntry() to get the real user object. This step is only necessary if you want to process
search results. You can find the GUID of an account in PSBase.NativeGUID.
In the future, you can access precisely this account via its GUID. Then you wont have to care whether the location,
the name, or some other property of the user accounts changes. The GUID will always remain constant:
$acccount = [ADSI]"LDAP://<GUID=f0ffa8b401ce5549b318c0a4641cdd4a>"
$acccount
distinguishedName
----------------{CN=Guest,CN=Users,DC=scriptinternals,DC=technet}
Specify the GUID when you log on if you want to log on to the domain:
$guid = "<GUID=f0ffa8b401ce5549b318c0a4641cdd4a>"
$acccount = new-object
DirectoryServices.DirectoryEntry("LDAP://10.10.10.1/$guid","domain\user", `
"secret")
distinguishedName
----------------{CN=Guest,CN=Users,DC=scriptinternals,DC=technet}
Twin objects: Every ADSI object actually exists twice: first, as an object PowerShell synthesizes and then
as a raw ADSI object. You can access the underlying raw object via the PSBase property of the processed
object. The processed object contains all Active Directory attributes, including possible schema extensions.
The underlying base object contains the .NET properties and methods you need for general management.
You already saw how to access these two objects when you used Children to list the contents of a
container.
Phantom objects: Search results of a cross-domain search look like original objects only at first sight. In
reality, these are reduced SearchResult objects. You can get the real ADSI object by using the
GetDirectoryEntry() method. You just saw how that happens in the section on GUIDs.
Properties: All the changes you made to ADSI properties wont come into effect until you invoke the
SetInfo() method.
In the following examples, we will use the Get-LDAPUser function described above to access user accounts, but you
can also get at user accounts with one of the other described approaches.
The result is meager but, as you know by now, search queries only return a reduced SearchResult object. You get the
real user object from it by calling GetDirectoryEntry(). Then youll get more information:
$useraccount = $useraccount.GetDirectoryEntry()
$useraccount | Format-List *
objectClass
: {top, person, organizationalPerson, user}
cn
: {Guest}
description
: {Predefined account for guest access to the computer
or domain)
distinguishedName
: {CN=Guest,CN=Users,DC=scriptinternals,DC=technet}
instanceType
: {4}
whenCreated
: {12.12.2005 12:31:31 PM}
whenChanged
: {06.27.2006 09:59:59 AM}
uSNCreated
: {System.__ComObject}
memberOf
: {CN=Guests,CN=Builtin,DC=scriptinternals,DC=technet}
uSNChanged
: {System.__ComObject}
name
: {Guest}
objectGUID
: {240 255 168 180 1 206 85 73 179 24 192 164 100 28
221 74}
userAccountControl
: {66080}
badPwdCount
: {0}
codePage
: {0}
countryCode
: {0}
badPasswordTime
: {System.__ComObject}
lastLogoff
: {System.__ComObject}
lastLogon
: {System.__ComObject}
logonHours
: {255 255 255 255 255 255 255 255 255 255 255 255 255
255 255 255 255 255 255 255 255
}
pwdLastSet
: {System.__ComObject}
primaryGroupID
: {514}
objectSid
: {1 5 0 0 0 0 0 5 21 0 0 0 184 88 34 189 250 183 7
172 165 75 78 29 245 1 0 0}
accountExpires
: {System.__ComObject}
logonCount
: {0}
sAMAccountName
: {Guest}
sAMAccountType
: {805306368}
objectCategory
:
{CN=Person,CN=Schema,CN=Configuration,DC=scriptinternals,DC=technet}
isCriticalSystemObject : {True}
nTSecurityDescriptor
: {System.__ComObject}
The difference between these two objects: the object that was returned first represents the respective user. The
underlying base object is responsible for the ADSI object itself and, for example, reports where it is stored inside a
domain or what is its unique GUID. The UserName property, among others, does not state whom the user account
represents (which in this case is Guest), but who called it (Administrator).
accountExpires {get;set;}
badPasswordTime {get;set;}
badPwdCount {get;set;}
cn {get;set;}
codePage {get;set;}
countryCode {get;set;}
description {get;set;}
distinguishedName
Property
System.DirectoryServices.PropertyValueCollection
instanceType
Property
System.DirectoryServices.PropertyValueCollection
isCriticalSystemObject Property
System.DirectoryServices.PropertyValueCollection
lastLogoff
Property
System.DirectoryServices.PropertyValueCollection
lastLogon
Property
System.DirectoryServices.PropertyValueCollection
logonCount
Property
System.DirectoryServices.PropertyValueCollection
logonHours
Property
System.DirectoryServices.PropertyValueCollection
memberOf
Property
System.DirectoryServices.PropertyValueCollection
name
Property
System.DirectoryServices.PropertyValueCollection
nTSecurityDescriptor
Property
System.DirectoryServices.PropertyValueCollection
objectCategory
Property
System.DirectoryServices.PropertyValueCollection
objectClass
Property
System.DirectoryServices.PropertyValueCollection
objectGUID
Property
System.DirectoryServices.PropertyValueCollection
objectSid
Property
System.DirectoryServices.PropertyValueCollection
primaryGroupID
Property
System.DirectoryServices.PropertyValueCollection
pwdLastSet
Property
System.DirectoryServices.PropertyValueCollection
sAMAccountName
Property
System.DirectoryServices.PropertyValueCollection
sAMAccountType
Property
System.DirectoryServices.PropertyValueCollection
userAccountControl
Property
System.DirectoryServices.PropertyValueCollection
uSNChanged
Property
System.DirectoryServices.PropertyValueCollection
uSNCreated
Property
System.DirectoryServices.PropertyValueCollection
whenChanged
Property
System.DirectoryServices.PropertyValueCollection
whenCreated
Property
System.DirectoryServices.PropertyValueCollection
distinguishedName {get;...
instanceType {get;set;}
isCriticalSystemObject ...
lastLogoff {get;set;}
lastLogon {get;set;}
logonCount {get;set;}
logonHours {get;set;}
memberOf {get;set;}
name {get;set;}
nTSecurityDescriptor {g...
objectCategory {get;set;}
objectClass {get;set;}
objectGUID {get;set;}
objectSid {get;set;}
primaryGroupID {get;set;}
pwdLastSet {get;set;}
sAMAccountName {get;set;}
sAMAccountType {get;set;}
userAccountControl {get...
uSNChanged {get;set;}
uSNCreated {get;set;}
whenChanged {get;set;}
whenCreated {get;set;}
In this list, you will also learn whether properties are only readable or if they can also be modified. Modifiable
properties are designated by {get;set;} and read-only by {get;}. If you change a property, the modification wont
come into effect until you subsequently call SetInfo().
$useraccount.Description = guest account
$useraccount.SetInfo()
Moreover, Get-Member can supply information about the underlying PSBase object:
$useraccount.PSBase | Get-Member -MemberType *Property
TypeName: System.Management.Automation.PSMemberSet
Name
MemberType Definition
------------- ---------AuthenticationType Property
System.DirectoryServices.AuthenticationTypes
AuthenticationType {get;set;}
Children
Property
System.DirectoryServices.DirectoryEntries
Children {get;}
Container
Property
System.ComponentModel.IContainer Container
{get;}
Guid
Property
System.Guid Guid {get;}
Name
Property
System.String Name {get;}
NativeGuid
Property
System.String NativeGuid {get;}
NativeObject
Property
System.Object NativeObject {get;}
ObjectSecurity
Property
System.DirectoryServices.ActiveDirectorySecurity ObjectSecurity {get;set;}
Options
Property
System.DirectoryServices.DirectoryEntryConfiguration Options {get;}
Parent
Property
System.DirectoryServices.DirectoryEntry Parent
{get;}
Password
Property
System.String Password {set;}
Path
Property
System.String Path {get;set;}
Properties
Property
System.DirectoryServices.PropertyCollection
Properties {get;}
SchemaClassName
Property
System.String SchemaClassName {get;}
SchemaEntry
Property
System.DirectoryServices.DirectoryEntry
SchemaEntry {get;}
Site
Property
System.ComponentModel.ISite Site {get;set;}
UsePropertyCache
Property
System.Boolean UsePropertyCache {get;set;}
Username
Property
System.String Username {get;set;}
Reading Properties
The convention is that object properties are read using a dot, just like all other objects (see Chapter 6). So, if you
want to find out what is in the Description property of the $useraccount object, formulate:
$useraccount.Description
Predefined account for guest access
But there are also two other options and they look like this:
$useraccount.Get("Description")
$useraccount.psbase.InvokeGet("Description")
At first glance, both seem to work identically. However, differences become evident when you query another
property: AccountDisabled.
$useraccount.AccountDisabled
$useraccount.Get("AccountDisabled")
Exception calling "Get" with 1 Argument(s):"The directory property cannot be
found in the cache.
At line:1 Char:14
+ $useraccount.Get( <<<< "AccountDisabled")
$useraccount.psbase.InvokeGet("AccountDisabled")
False
The first variant returns no information at all, the second an error message, and only the third the right result. What
happened here?
The object in $useraccount is an object processed by PowerShell. All attributes (directory properties) become
visible in this object as properties. However, ADSI objects can contain additional properties, and among these is
AccountDisabled. PowerShell doesnt take these additional properties into consideration. The use of a dot
categorically suppresses all errors as only Get() reports the problem: nothing was found for this element in the
LDAP directory under the name AccountDisabled.
In fact, AccountDisabled is located in another interface of the element as only the underlying PSBase object, with its
InvokeGet() method, does everything correctly and returns the contents of this property.
As long as you want to work on properties that are displayed when you use Format-List * to output the object to the
console, you wont have any difficulty using a dot or Get(). For all other properties, youll have to use
PSBase.InvokeGet().Use GetEx() iIf you want to have the contents of a property returned as an array.
Modifying Properties
In a rudimentary case, you can modify properties like any other object: use a dot to assign a new value to the
property. Dont forget afterwards to call SetInfo() so that the modification is saved. Thats a special feature of ADSI.
For example, the following line adds a standard description for all users in the user directory if there isnt already
one:
$ldap = "CN=Users"
$domain = [ADSI]""
$dn = $domain.distinguishedName
$users = [ADSI]"LDAP://$ldap,$dn"
$users.PSBase.Children | Where-Object { $_.sAMAccountType -eq 805306368 } |
Where-Object { $_.Description.toString() -eq "" } |
ForEach-Object { $_.Description = "Standard description"; $_.SetInfo();
$_.sAMAccountName + " was changed." }
In fact, there are also a total of three approaches to modifying a property. That will soon become very important as
the three ways behave differently in some respects:
$searchuser = Get-LDAPUser Guest
$useraccount = $searchuser.GetDirectoryEntry()
# Method 1:
$useraccount.Description = "A new description"
$useraccount.SetInfo()
# Method 2:
$useraccount.Put("Description", "Another new description")
$useraccount.SetInfo()
# Method 3:
$useraccount.PSBase.InvokeSet("Description", "A third description")
$useraccount.SetInfo()
As long as you change the normal directory attributes of an object, all three methods will work in the same way.
Difficulties arise when you modify properties that have special functions. For example among these is the
AccountDisabled property, which determines whether an account is disabled or not. The Guest account is normally
disabled:
$useraccount.AccountDisabled
The result is nothing because this property isas you already know from the last sectionnot one of the
directory attributes that PowerShell manages in this object. Thats not good because something very peculiar will
occur in PowerShell if you now try to set this property to another value:
$useraccount.AccountDisabled = $false
$useraccount.SetInfo()
Exception calling "SetInfo" with 0 Argument(s): "The specified directory
service attribute
or value already exists. (Exception from HRESULT: 0x8007200A)"
At line:1 Char:18
+ $useraccount.SetInfo( <<<< )
$useraccount.AccountDisabled
False
PowerShell has summarily input to the object a new property called AccountDisabled. If you try to pass this object
to the domain, it will resist: the AccountDisabled property added by PowerShell does not match the
AccountDisabled domain property. This problem always occurs when you want to set a property of an ADSI object
that hadnt previously been specified.
To eliminate the problem, you have to first return the object to its original state so you basically remove the property
that PowerShell added behind your back. You can do that by using GetInfo() to reload the object from the domain.
This shows that GetInfo() is the opposite number of SetInfo():
$useraccount.GetInfo()
Once PowerShell has added an illegal property to the object, all further attempts will fail to store this object in the
domain by using SetInfo(). You must call GetInfo() or create the object again:
Finally, use the third above-mentioned variant to set the property, namely not via the normal object processed by
PowerShell, but via its underlying raw version:
$useraccount.psbase.InvokeSet("AccountDisabled", $false)
$useraccount.SetInfo()
Now the modification works. The lesson: the only method that can reliably and flawlessly modify properties is
InvokeSet() from the underlying PSBase object. The other two methods that modify the object processed by
PowerShell will only work properly with the properties that the object does display when you output it to the
console.
Deleting Properties
If you want to completely delete a property, you dont have to set its contents to 0 or empty text. If you delete a
property, it will be completely removed. PutEx() can delete properties and also supports properties that store arrays.
PutEx() requires three arguments. The first specifies what PutEx() is supposed to do and corresponds to the values
listed in Table 19.2. . The second argument is the property name that is supposed to be modified. Finally, the third
argument is the value that you assign to the property or want to remove from it.
Numerical Value
1
2
3
4
Meaning
Delete property value (property remains intact)
Replace property value completely
Add information to a property
Delete parts of a property
Then, the Description property will be gone completely when you call all the properties of the object:
$useraccount | Format-List *
objectClass
: {top, person, organizationalPerson, user}
cn
: {Guest}
distinguishedName
:
{CN=Guest,CN=Users,DC=scriptinternals,DC=technet}instanceType
: {4}
whenCreated
: {11.12.2005 12:31:31}
whenChanged
: {17.10.2007 11:59:36}
uSNCreated
: {System.__ComObject}
memberOf
: {CN=Guests,CN=Builtin,DC=scriptinternals,DC=technet}
uSNChanged
: {System.__ComObject}
name
: {Guest}
objectGUID
: {240 255 168 180 1 206 85 73 179 24 192 164 100 28
221 74}
userAccountControl
: {66080}
badPwdCount
: {0}
codePage
: {0}
countryCode
: {0}
badPasswordTime
: {System.__ComObject}
lastLogoff
: {System.__ComObject}
lastLogon
: {System.__ComObject}
logonHours
: {255 255 255 255 255 255 255 255 255 255 255 255 255
255 255 255 255 255 255 255 255
}
pwdLastSet
: {System.__ComObject}
primaryGroupID
: {514}
objectSid
: {1 5 0 0 0 0 0 5 21 0 0 0 184 88 34 189 250 183 7
172 165 75 78 29 245 1 0 0}
accountExpires
: {System.__ComObject}
logonCount
: {0}
sAMAccountName
: {Guest}
sAMAccountType
: {805306368}
objectCategory
:
{CN=Person,CN=Schema,CN=Configuration,DC=scriptinternals,DC=technet}
isCriticalSystemObject : {True}
nTSecurityDescriptor
: {System.__ComObject}
ImportantEven Get-Member wont return to you any more indications of the Description property. Thats a real
deficiency as you have no way to recognize what other properties the ADSI object may possibly support as long as
youre using PowerShells own resources.. PowerShell always shows only properties that are defined.
However, this doesnt mean that the Description property is now gone forever. You can create a new one any time:
$useraccount.Description = "New description"
$useraccount.SetInfo()
Interesting, isnt it? This means you could add entirely different properties that the object didnt have before:
$useraccount.wwwHomePage = "http://www.powershell.com"
$useraccount.favoritefood = "Meatballs"
Cannot set the Value property for PSMemberInfo object of type
"System.Management.Automation.PSMethod".
At line:1 Char:11
+ $useraccount.L <<<< oritefood = "Meatballs"
$useraccount.SetInfo()
It turns out that the user account accepts the wwwHomePage property (and so sets the Web page of the user on user
properties), while favoritefood was rejected. Only properties allowed by the schema can be set.
Take a look under this name in the schema of the domain. The result is the schema object for user objects, which
returns the names of all permitted properties in SystemMayContain.
$schema = $domain.PSBase.Children.find("CN=user,CN=Schema,CN=Configuration")
$schema.systemMayContain | Sort-Object
accountExpires
aCSPolicyName
adminCount
badPasswordTime
badPwdCount
businessCategory
codepage
controlAccessRights
dBCSPwd
defaultClassStore
desktopProfile
dynamicLDAPServer
groupMembershipSAM
groupPriority
groupsToIgnore
homeDirectory
homeDrive
homePhone
initials
lastLogoff
lastLogon
lastLogonTimestamp
lmPwdHistory
localeID
lockoutTime
logonCount
logonHours
logonWorkstation
mail
manager
maxStorage
mobile
msCOM-UserPartitionSetLink
msDRM-IdentityCertificate
msDS-Cached-Membership
msDS-Cached-Membership-Time-Stamp
mS-DS-CreatorSID
msDS-Site-Affinity
msDS-User-Account-Control-Computed
msIIS-FTPDir
msIIS-FTPRoot
mSMQDigests
mSMQDigestsMig
mSMQSignCertificates
mSMQSignCertificatesMig
msNPAllowDialin
msNPCallingStationID
msNPSavedCallingStationID
msRADIUSCallbackNumber
msRADIUSFramedIPAddress
msRADIUSFramedRoute
msRADIUSServiceType
msRASSavedCallbackNumber
msRASSavedFramedIPAddress
msRASSavedFramedRoute
networkAddress
ntPwdHistory
o
operatorCount
otherLoginWorkstations
pager
preferredOU
primaryGroupID
profilePath
pwdLastSet
scriptPath
servicePrincipalName
terminalServer
unicodePwd
userAccountControl
userCertificate
userParameters
userPrincipalName
userSharedFolder
userSharedFolderOther
userWorkstations
But note that this would delete any other previously entered telephone numbers. If you want to add a new telephone
number to an existing list, proceed as follows:
$useraccount.PutEx(3, "otherHomePhone", @("555"))
$useraccount.SetInfo()
A very similar method allows you to delete selected telephone numbers on the list:
$useraccount.PutEx(4, "otherHomePhone", @("456", "789"))
$useraccount.SetInfo()
Invoking Methods
All the objects that youve been working with up to now contain not only properties, but also methods. In contrast to
properties, methods do not require you to call SetInfo() when you invoke a method that modifies an object. . To find
out which methods an object contains, use Get-Member to make them visible (see Chapter 6):
$guest | Get-Member -memberType *Method
Surprisingly, the result is something of a disappointment because the ADSI object PowerShell delivers contains no
methods. The true functionality is in the base object, which you get by using PSBase:
$guest.psbase | Get-Member -memberType *Method
TypeName: System.Management.Automation.PSMemberSet
Name
MemberType Definition
------------- ---------add_Disposed
Method
System.Void add_Disposed(EventHandler
value)
Close
Method
System.Void Close()
CommitChanges
Method
System.Void CommitChanges()
CopyTo
Method
System.DirectoryServices.DirectoryEntry
CopyTo(DirectoryEntry newPare...
CreateObjRef
Method
System.Runtime.Remoting.ObjRef
CreateObjRef(Type requestedType)
DeleteTree
Method
System.Void DeleteTree()
Dispose
Method
System.Void Dispose()
Equals
Method
System.Boolean Equals(Object obj)
GetHashCode
Method
System.Int32 GetHashCode()
GetLifetimeService
Method
System.Object GetLifetimeService()
GetType
Method
System.Type GetType()
get_AuthenticationType
Method
System.DirectoryServices.AuthenticationTypes get_AuthenticationType()
get_Children
Method
System.DirectoryServices.DirectoryEntries get_Children()
get_Container
Method
System.ComponentModel.IContainer
get_Container()
get_Guid
Method
System.Guid get_Guid()
get_Name
Method
System.String get_Name()
get_NativeGuid
Method
System.String get_NativeGuid()
get_ObjectSecurity
Method
System.DirectoryServices.ActiveDirectorySecurity get_ObjectSecurity()
get_Options
Method
System.DirectoryServices.DirectoryEntryConfiguration get_Options()
get_Parent
Method
System.DirectoryServices.DirectoryEntry
get_Parent()
get_Path
Method
System.String get_Path()
get_Properties
Method
System.DirectoryServices.PropertyCollection get_Properties()
get_SchemaClassName
Method
System.String get_SchemaClassName()
get_SchemaEntry
Method
System.DirectoryServices.DirectoryEntry
get_SchemaEntry()
get_Site
Method
System.ComponentModel.ISite get_Site()
get_UsePropertyCache
Method
System.Boolean get_UsePropertyCache()
get_Username
Method
System.String get_Username()
InitializeLifetimeService Method
System.Object
InitializeLifetimeService()
Invoke
Method
System.Object Invoke(String methodName,
Params Object[] args)
InvokeGet
Method
System.Object InvokeGet(String
propertyName)
InvokeSet
Method
System.Void InvokeSet(String
propertyName, Params Object[] args)
MoveTo
Method
System.Void MoveTo(DirectoryEntry
newParent), System.Void MoveTo(Dire...
RefreshCache
Method
System.Void RefreshCache(), System.Void
RefreshCache(String[] propert...
remove_Disposed
Method
System.Void remove_Disposed(EventHandler
value)
Rename
Method
System.Void Rename(String newName)
set_AuthenticationType
Method
System.Void
set_AuthenticationType(AuthenticationTypes value)
set_ObjectSecurity
Method
System.Void
set_ObjectSecurity(ActiveDirectorySecurity value)
set_Password
Method
System.Void set_Password(String value)
set_Path
Method
System.Void set_Path(String value)
set_Site
Method
System.Void set_Site(ISite value)
set_UsePropertyCache
Method
System.Void set_UsePropertyCache(Boolean
value)
set_Username
Method
System.Void set_Username(String value)
ToString
Method
System.String ToString()
Changing Passwords
The password of a user account is an example of information that isnt stored in a property. Thats why you cant
just read out user accounts. Instead, methods ensure the immediate generation of a completely confidential hash
value out of the user account and that it is deposited in a secure location. You can use the SetPassword() and
ChangePassword() methods to change passwords:
$useraccount.SetPassword("New password")
$useraccount.ChangePassword("Old password", "New password")
Here, too, the deficiencies of Get-Member become evident when it is used with ADSI objects because Get-Member
suppresses both methods instead of displaying them. You just have to know that they exist.
SetPassword() requires administrator privileges and simply resets the password. That can be risky because in the
process you lose access to all your certificates outside a domain, including the crucial certificate for the Encrypting
File System (EFS), though its necessary when users forget their passwords. ChangePassword doesnt need any
higher level of permission because confirmation requires giving the old password.
When you change a password, be sure that it meets the demands of the domain. Otherwise, youll be rewarded with
an error message like this one:
Exception calling "SetPassword" with 1 Argument(s):
"The password does not meet the password policy requirements.
Check the minimum password length, password complexity and password
history requirements. (Exception from HRESULT: 0x800708C5)"
At line:1 Char:22
+ $realuser.SetPassword( <<<< "secret")
Groups on their part can also be members in other groups. So, every group object has not only the Member property
with its members, but also MemberOf with the groups in which this group is itself a member.
In the example, the user Cofi1 is added to the group of Domain Admins. It would have sufficed to specify the users
correct ADSI path to the Add() method. But its easier to get the user and pass the path property of the PSBase
object.
Aside from Add(), there are other ways to add users to groups:
$administrators.Member = $administrators.Member + $user.distinguishedName
$administrators.SetInfo()
$administrators.Member += $user.distinguishedName
$administrators.SetInfo()
Instead of Add() use the Remove() method to remove users from the group again..
Group
Global
Local
Universal
As security group
Code
2
4
8
Add -2147483648
Table of Contents
Since PowerShell is layered on the .NET Framework, you already know from Chapter 6 how you can use .NET code
in PowerShell to make up for missing functions. In this chapter, well take up this idea once again. Youll learn
about the options PowerShell has for creating command extensions on the basis of the .NET Framework. You
should be able to even create your own cmdlets at the end of this chapter.
Topics Covered:
In Chapter 6, you learned in detail about how this works and what an assembly is. PowerShell used Add-Type to
load a system library and was then able to use the classes from it to call a static method like MsgBox().
Thats extremely useful when there is already a system library that offers the method youre looking for, but for
some functionality even the .NET Framework doesnt have any right commands. For example, you have to rely on
your own resources if you want to move text to the clipboard. The only way to get it done is to access the low-level
API functions outside the .NET Framework.
You have to first compile the code before PowerShell can execute it. Compilation is a translation of your source
code into machine-readable intermediate language (IL). There are two options here.
In-Memory Compiling
To compile the source code and make it a type that you can use, feed the source code to Add-Type and specify the
programming language the source code used:
$type = Add-Type -TypeDefinition $code -Language VisualBasic
Now, you can derive an object from your new type and call the method CopyToClipboad(). Done!
$object = New-Object ClipboardAddon.Utility
$object.CopyToClipboard(Hi Everyone!)
You might be wondering why in your custom type, you needed to use New-Object first to get an object. With
MsgBox() in the previous example, you could call that method directly from the type.
CopyToClipboard() is created in your source code as a dynamic method, which requires you to first create an
instance of the class, and thats exactly what New-Object does. Then the instance can call the method.
Alternatively, methods can also be static. For example, MsgBox() in the first example is a static method. To call
static methods, you need neither New-Object nor any instances. Static methods are called directly through the class
in which they are defined.
If you would rather use CopyToClipboard() as a static method, all you need to do is to make a slight change to your
source code. Replace this line:
Public Sub CopyToClipboard(ByVal text As String)
Type this line instead:
Public Shared Sub CopyToClipboard(ByVal text As String)
Once you have compiled your source code, then you can immediately call the method like this:
[ClipboardAddon.Utility]::CopyToClipboard(Hi Everyone!)
DLL Compilation
With Add-Type, you can even compile and generate files. In the previous example, your source code was compiled
in-memory on the fly. What if you wanted to protect your intellectual property somewhat and compile a DLL that
your solution would then load?
Here is how you create your own DLL (make sure the folder c:\powershell exists, or else create it or change the
output path in the command below):
PS> $code = @'
Imports Microsoft.VisualBasic
Imports System
Namespace ClipboardAddon
Public Class Utility
Private Declare Function OpenClipboard Lib "user32" (ByVal hwnd As
Integer) As Integer
Private Declare Function EmptyClipboard Lib "user32" () As Integer
Private Declare Function CloseClipboard Lib "user32" () As Integer
Private Declare Function SetClipboardData Lib "user32"(ByVal wFormat As
Integer, ByVal hMem As Integer) As Integer
Private Declare Function GlobalAlloc Lib "kernel32" (ByVal wFlags As
Integer, ByVal dwBytes As Integer) As Integer
Private Declare Function GlobalLock Lib "kernel32" (ByVal hMem As
Integer) As Integer
Private Declare Function GlobalUnlock Lib "kernel32" (ByVal hMem As
Integer) As Integer
Private Declare Function lstrcpy Lib "kernel32" (ByVal lpString1 As
Integer, ByVal lpString2 As String) As Integer
Public Shared Sub CopyToClipboard(ByVal text As String)
Dim result As Boolean = False
Dim mem As Integer = GlobalAlloc(&H42, text.Length + 1)
Dim lockedmem As Integer = GlobalLock(mem)
lstrcpy(lockedmem, text)
If GlobalUnlock(mem) = 0 Then
If OpenClipboard(0) Then
EmptyClipboard()
result = SetClipboardData(1, mem)
CloseClipboard()
End If
End If
End Sub
End Class
End Namespace
'@
PS> Add-Type -TypeDefinition $code -Language VisualBasic -OutputType Library
`
>> -OutputAssembly c:\powershell\extension.dll
After you run these commands, you should find a file called c:\powershell\extension.dll with the compiled content
of your code. If not, try this code in a new PowerShell console. Your experiments with the in-memory compilation
may have interfered.
To load and use your DLL from any PowerShell session, go ahead and use this code:
PS> Add-Type -Path C:\powershell\extension.dll
PS> [Clipboardaddon.utility]::CopyToClipboard("Hello World!")
You can even compile and create console applications and windows programs that way - although that is an edge
case. To create applications, you better use a specific development environment like Visual Studio.