PowerShell eBook
December 22, 2016 | Author: trdunsworth | Category: N/A
Short Description
Converted several pages to a 1 document piece...
Description
Chapter 1. The PowerShell Console Welcome to PowerShell! This chapter will introduce you to the PowerShell console and show you how to configure it, including font colors and sizes, editing and display options. Topics Covered:
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.
Figure 1.1: How to always open PowerShell with administrator rights (Run without administrative privileges whenever possible)
First Steps with the Console After PowerShell starts, its console window opens, and you see a blinking text prompt, asking for your input with no icons or menus. PowerShell is a command console and almost entirely operated via keyboard input. The prompt begins with "PS" and after it is the path name of the directory where you are located. Start by trying out a few commands. For example, type: hello (Enter) As soon as you press (Enter), your entry will be sent to PowerShell. Because PowerShell has never heard of the command "hello" you will be confronted with an error message highlighted in red.
Figure 1.2: First commands in the PowerShell console For example, if you'd like to see which files and folders are in your current directory, then type dir (Enter). You'll get a text listing of all the files in the directory. PowerShell's communication with you is always text-based. PowerShell can do much more than display simple directory lists. You can just as easily list all running processes or all installed hotfixes: Just pick a different command as the next one provides a list of all running processes: Get-Process (Enter) Get-Hotfix (Enter)
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.
Incomplete and Multi-line Entries Whenever you enter something PowerShell cannot understand, you get a red error message, explaining what went wrong. However, if you enter something that isn't wrong but incomplete (like a string with one missing closing quote), PowerShell gives you a chance to complete your input. You then see a double-prompt (">>"), and once you completed the line and pressed ENTER twice, PowerShell executes the command. You can also bail out at any time and cancel the current command or input by pressing: (Ctrl)+(C). The "incomplete input" prompt will also appear when you enter an incomplete arithmetic problem like this one: 2 + (Enter) >> 6 (Enter) >> (Enter) 8 This feature enables you to make multi-line PowerShell entries: "This is my little multiline entry.(Enter) >> I'm now writing a text of several lines. (Enter) >> And I'll keep on writing until it's no longer fun."(Enter) >>(Enter) This is my little multiline entry. I'm now writing a text of several lines. And I'll keep on writing until it's no longer fun. The continuation prompt generally takes its cue from initial and terminal characters like open and closed brackets or quotation marks at both ends of a string. As long as the symmetry of these characters is incorrect, you'll continue to see the prompt. However, you can activate it even in other cases: dir `(Enter) >> -recurse(Enter) >>(Enter) So, if the last character of a line is what is called a "back-tick" character, the line will be continued. You can retrieve that special character by pressing (`).
Important Keyboard Shortcuts Shortcuts are important since almost everything in PowerShell is keyboard-based. For example, by pressing the keys (Arrow left) and (Arrow right), you can move the blinking cursor to the left or right. Use it to go back and correct a typo. If you want to move the cursor word by word, hold down (Ctrl) while pressing the arrow keys. To place the cursor at the beginning of a line, hit (Home). Pressing (End) will send the cursor to the end of a line. If you haven't entered anything, then the cursor won't move since it will only move within entered text. There's one exception: if you've already entered a line and pressed (Enter) to execute the line, you can make this line appear again character-by-character by pressing (Arrow right).
Deleting Incorrect Entries
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.
Command History: Reusing Entered Commands The most awesome feature is a built-in search through all of the commands you used in your current session: simply type "#" and then some search text that you know exists in one or more of your previous commands. Next, type TAB one or more times to see all the commands that contained your keyword. Press ENTER to execute the command once you found it, or edit the command line to your liking. If you just wanted to polish or correct one of your most recent commands, press (Arrow up) to re-display the command that you entered. Press (Arrow up) and (Arrow down) to scroll up and down your command history. Using (F5) and (F8) do the same as the up and down arrow keys. This command history feature is extremely useful. Later, you'll learn how to configure the number of commands the console ―remembers‖. The default setting is the last 50 commands. You can display all the commands in your history by pressing (F7) and then scrolling up and down the list to select commands using (Arrow up) and (Arrow down) and (Enter). The numbers before the commands in the Command History list only denote the sequence number. You cannot enter a number to select the associated command. What you can do is move up and down the list by hitting the arrow keys. Simply press (F9) to ‗activate‘ the numbers so that you can select a command by its number. This opens a menu that accepts the numbers and returns the desired command. The keyboard sequence (Alt)+(F7) will clear the command history and start you off with a new list. (F8) provides more functionality than (Arrow up) as it doesn't just show the last command you entered, but keeps a record of the characters you've already typed in. If, for example, you'd like to see all the commands you've entered that begin with "d", type: d(F8) Press (F8) several times. Every time you press a key another command will be displayed from the command history provided that you've already typed in commands with an initial "d."
Automatically Completing Input
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.
Scrolling Console Contents The visible part of your console depends on the size of your console window, which you can change with your mouse. Drag the window border while holding down your left mouse button until the window is the size you want. Note that the actual contents of the console, the "screen buffer," don't change. So, if the window is too small to show everything, you should use the scroll bars.
Selecting and Inserting Text Use your mouse if you'd like to select text inside the PowerShell window and copy it onto the clipboard. Move the mouse pointer to the beginning of the selected text, hold down the left mouse button and drag it over the text area that you want to select.
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.
Figure 1.3: Marking and copying text areas in QuickEdit mode
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.
Customizing the Console You can customize a variety of settings in the console including edit mode, screen buffer size, font colors, font sizes etc.
Opening Console Properties The basic settings of your PowerShell console are configured in a special Properties dialog box. Click on the PowerShell icon on the far left of the title bar of the console window to open it.
Figure 1.4: Opening console properties That will open a context menu. You should select Properties and a dialog box will open. To get help, click on the question mark button on the title bar of the window. A question mark is then pinned to your mouse pointer. Next, click on the option you need help for. The help appears as a ScreenTip window.
Defining Options Under the heading Options are four panels of options:
Figure 1.5: Defining the QuickEdit and Insert modes
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.
Specifying Fonts and Font Sizes On the Font tab, you can choose both the font and the font size displayed in the console. The console often uses the raster font as its default. This font is available in a specific range of sizes with available sizes shown in the "Size" list. Scalable TrueType fonts are much more flexible. They're marked in the list by a "TT" symbol. When you select a TrueType font, you can choose any size in the size list or enter them as text in the text box. TrueType fonts can be dynamically scaled.
Figure 1.6: Specifying new fonts and font sizes You should also try experimenting with TrueType fonts by using the "bold fonts" option. TrueType fonts are often more readable if they're displayed in bold. Your choice of fonts may at first seem a bit limited. To get more font choices, you can add them to the console font list. The limited default font list is supposed to prevent you from choosing unsuitable fonts for your console. One reason for this is that the console always uses the same width for each character (fixed width fonts). This restricts the use of most Windows fonts because they're proportional typefaces: every character has its own width. For example, an "i" is narrower than an "m". If you're sure that a certain font will work in the console, then here's how to add the font to the console font list. Open your registry editor. In the key HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\ CurrentVersion\Console\TrueTypeFont insert a new "string value" and give this entry the name "00" (numbers, not letters). If there's already an entry that has this name, then call the new entry "000" or add as many zeroes as required to avoid conflicts with existing entries. You should then double-click your new entry to open it and enter the name of the font. The name must be exactly the same as the official font name, just the way it's stated under the key HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Fonts. The newly added font will now turn up in the console's option field. However, the new font will work only after you either log off at least once or restart your computer. If you fail to do so, the console will ignore your new font when you select it in the dialog box.
Setting Window and Buffer Size
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.
Figure 1.7: Specifying the size of the window buffer You can also set the window size and position on this tab if you'd like your console to open at a certain size and screen position on your display. Choose the option Let system position window and Windows will automatically determine at what location the console window will open.
Selecting Colors On the Colors tab, you can select your own colors for four areas:
Screen text: Console font Screen background: Console background color Popup text: Popup window font, such as command history's (F7) Popup background: Popup window background color
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.
Figure 1.8: Select better colors for your console
Directly Assigning Modifications in PowerShell Some of the console configuration can also be done from within PowerShell code. You'll hear more about this later. To give you a quick impression, take a look at this: $host.ui.rawui (Enter) $host.ui.rawui.ForegroundColor = "Yellow" (Enter) $host.ui.rawui.WindowTitle = "My Console" (Enter) These changes will only be temporary. Once you close and re-open PowerShell, the changes are gone. You would have to include these lines into one of your "profile scripts" which run every time you launch PowerShell to make them permanent. You can read more about this in Chapter 10.
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).
Piping and Routing You may want to view the information page by page or save it in a file since some commands output a lot of information.
Piping: Outputting Information Page by Page The pipe command more outputs information screen page by screen page. You will need to press a button (like Space) to continue to the next page. Piping uses the vertical bar (|). The results of the command to the left of the pipe symbol are then fed into the command on the right side of the pipe symbol. This kind of piping is also known in PowerShell as the "pipeline": Get-Process | more (Enter) You can press (Ctrl)+(C) to stop output. Piping also works with other commands, not just more. For example, if you‘d like to get a sorted directory listing, pipe the result to Sort-Object and specify the columns you would like to sort: dir | Sort-Object -Property Length, Name (Enter) You'll find more background information on piping as well as many useful examples in Chapter 5.
Redirecting: Storing Information in Files If you‘d like to redirect the result of a command to a file, you can use the redirection symbol ">": Help > help.txt (Enter) The information won't appear in the console but will instead be redirected to the specified file. You can then open the file. However, opening a file in PowerShell is different from opening a file in the classic console: help.txt (Enter) The term "help.txt" is not recognized as a cmdlet, function, operable program, or script file. Verify the term and try again. At line:1 character:8 + help.txt " and " 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
Adding Mandatory Parameters Let's assume you want to create a function that converts dollars to Euros . Here is a simple version: function ConvertTo-Euro {
param( $dollar, $rate=1.37 ) $dollar * $rate }
And here is how you run your new command: PS > ConvertTo-Euro -dollar 200 274
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 }
Now, the function performs as expected: PS > ConvertTo-Euro -rate 6.7 cmdlet ConvertTo-Euro at command pipeline position 1 Supply values for the following parameters: dollar: 100 670
Adding Switch Parameters There is one parameter type that is special: switch parameters do not take arguments. They are either present or not. You can assign the type [Switch] to that parameter to add switch parameters to your function. If you wanted to provide a way for your users to distinguish between raw and pretty output, your currency converter could implement a switch parameter called -Pretty. When present, the output would come as a nice text line, and when it is not present, it would be the raw numeric value: function ConvertTo-Euro { param( [Parameter(Mandatory=$true)] [Double] $dollar, $rate=1.37, [switch] $pretty ) $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 }
}
Now, it is up to your user to decide which output to choose: PS > ConvertTo-Euro -dollar 200 -rate 1.28 256 PS > ConvertTo-Euro -dollar 200 -rate 1.28 -pretty $200,00 equals EUR256,00 at a rate of 1.28
Adding Help to your Functions Get-Help returns Help information for all of your cmdlets. It can also return Help information for your self-defined functions. All you will need to do is add the Help text. To do that, add a specially formatted comment block right before the function or at the beginning or end of the function script block: function ConvertTo-Euro { param( [Parameter(Mandatory=$true)] [Double] $dollar, $rate=1.37, [switch] $pretty ) $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
} }
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] [[-rate] ] [-pretty] [] 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".
PS > Get-Help -name ConvertTo-Euro -Examples NAME ConvertTo-Euro SYNOPSIS Converts Dollar to Euro -------------------------- EXAMPLE 1 -------------------------C:\PS>ConvertTo-Euro 100 converts 100 dollars using the default exchange rate and positional parameters
-------------------------- EXAMPLE 2 --------------------------
C:\PS>ConvertTo-Euro 100 -rate 2.3 converts 100 dollars using a custom exchange rate
PS > Get-Help -name ConvertTo-Euro -Parameter * -dollar the dollar amount. This parameter is mandatory. Required? Position? Default value Accept pipeline input? Accept wildcard characters?
true 1 false
-rate 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 [] Required? Position? Default value Accept pipeline input? Accept wildcard characters?
false named false
Creating Pipeline-Aware Functions Your function is not yet pipeline aware/ So, it will simply ignore the results delivered by the upstream cmdlet if you call it within a pipeline statement: 1..10 | ConvertTo-Euro Instead, you will receive exceptions complaining about PowerShell not being able to "bind" the input object. That's because PowerShell cannot know which parameter is supposed to receive the incoming pipeline values. If you want your function to be pipeline aware, you can fix it by choosing the parameter that is to receive the pipeline input. Here is the enhanced param block:
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: 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.
Playing With Prompt Functions PowerShell already contains some pre-defined functions. You can enumerate the special drive function if you would like to see all available functions: Dir function: Many of these pre-defined functions perform important tasks in PowerShell. The most important place for customization is the function prompt, which is executed automatically once a command is done. It is responsible for displaying the PowerShell prompt. You can change your PowerShell prompt by overriding the function prompt. This will get you a colored prompt: function prompt { Write-Host ("PS " + $(get-location) +">") -nonewline -foregroundcolor Magenta " " }
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.
Execution Policy - Allowing Scripts to Run If you launched your script from outside PowerShell, using an explicit call to powershell.exe, your scripts always ran (unless you mistyped something). That's because here, you submitted the parameter executionpolicy and turned the restriction off for the particular call. To enable PowerShell scripts, you need to change the ExecutionPolicy. There are actually five different execution policies which you can list with this command: PS > Get-ExecutionPolicy -List Scope ExecutionPolicy ------------
-------
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.
Invoking Scripts like Commands To actually invoke scripts just as easily as normal commands—without having to specify relative or absolute paths and the ".ps1" file extension—pick or create a folder to store your scripts in. Next, add this folder to your "Path" environment variable. Done. md $env:appdata\PSScripts copy-item $env:temp\myscript.ps1 $env:appdata\PSScripts\myscript.ps1 $env:path += ";$env:appdata\PSScripts " myscript
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.
Parameters: Passing Arguments to Scripts Scripts can receive additional arguments from the caller in much the same way as functions do (see Chapter 9). Simply add the param() block defining the parameters to the top of your script. You learned about param() blocks in the previous chapter. For example, to add parameters to your event log monitoring script, try this: @' Param( $hours = 24, [Switch] $show ) $cutoff = (Get-Date) - (New-Timespan -hours $hours) $filename = "$env:temp\report.txt" Get-EventLog -LogName System -EntryType Error,Warning -After $cutoff | Format-Table -AutoSize | Out-File $filename -width 10000 If ($Show) { Invoke-Item $filename } else { Write-Warning "The report has been generated here: $filename" } '@ > $env:temp\myscript.ps1
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.
Scopes: Variable Visibility Any variable or function you define in a script by default is scoped "local". The variable or function is visible from subscopes (like functions or nested functions or scripts called from your script). It is not visible from superscopes (like the one calling the script) unless the script was called dot-sourced. So by default, any function or variable you define can be accessed from any other function defined at the same scope or in a subscope: function A { "Here is A" } function B { "Here is B" } function C { A; B } C
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.
Profile Scripts: Automatic Scripts Most changes and adjustments you make to PowerShell are only temporary, and once you close and reopen PowerShell, they are lost. To make changes and adjustments persistent, use profile scripts. These scripts get executed automatically whenever PowerShell starts (unless you specify the -noprofile paramater).
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).
Signing Scripts with Digital Signatures To guarantee that a script comes from a safe source and wasn't manipulated, scripts can be signed with a digital signature. This signature requires a so called "Codesigning Certificate" which is a digital certificate with a private key and the explicit purpose of validating code. You can get such a certificate from your corporate IT (if they run a PKI infrastructure), or you can buy it from certificate authorities like Verisign or Thawte. You can even create your own "self-signed" certificates which are the least trustworthy alternative.
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
Creating/Loading a New Certificate If there is no certificate in your certificate store, you cannot sign scripts. You can then either request/purchase a codesigning certificate and install it into your personal certificate store by doubleclicking it, or you can temporarily load a certificate file into memory using Get-PfxCertificate.
Creating Self-Signed Certificates The key to making self-signed certificates is the Microsoft tool makecert.exe. Unfortunately, this tool can't be downloaded separately and it may not be spread widely. You have to download it as part of a free "Software Development Kit" (SDK). Makecert.exe is in the .NET framework SDK which you can find at http://msdn2.microsoft.com/en-us/netframework/aa731542.aspx. After the SDK is installed, you'll find makecert.exe on your computer and be able to issue a new codesigning certificate with a name you specify by typing the following lines: $name = "PowerShellTestCert" pushd Cd "$env:programfiles\Microsoft Visual Studio 8\SDK\v2.0\Bin" .\makecert.exe -pe -r -n "CN=$name" -eku 1.3.6.1.5.5.7.3.3 -ss "my"
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"}
Making a Certificate "Trustworthy" Certificates you purchased from trusted certificate authorities or your own enterprise IT are considered trustworthy by default. That's because their root is listed in the "trusted root certification authorities container. You can examine these settings like this: Certmgr.msc Self-signed certificates are not trustworthy by default because anyone can create them. To make them trustworthy, you need to copy them into the list of trusted root certification authorities and Trusted Publishers.
Signing PowerShell Scripts PowerShell script signatures require only two things: a valid code-signing certificate and the script that you want to sign. The cmdlet Set-AuthenticodeSignature takes care of the rest. The following code grabs the first available codesigning certificate and then signs a script: $certificate = @(Dir cert:CurrentUser\My -codeSigningCert -recurse)[0] Set-AuthenticodeSignature c:\scripts\test.ps1 $certificate
Likewise, to sign all PowerShell scripts on a drive, use this approach: Dir C:\ -filter *.ps1 -recurse -erroraction SilentlyContinue | Set-AuthenticodeSignature -cert $certificate When you look at the signed scripts, you'll see a new comment block at the end of a script. Attention: You cannot sign script files that are smaller than 4 Bytes, or that are saved with Big Endian Unicode. Unfortunately, the builtin script editor ISE uses just that encoding scheme to save scripts, so you may not be able to sign scripts created with ISE unless you save the scripts with a different encoding.
Checking Scripts To check all of your scripts manually and find out whether someone has tampered with them, use GetAuthenticodeSignature:
Dir C:\ -filter *.ps1 -recurse -erroraction SilentlyContinue | GetAuthenticodeSignature If you want to find only scripts that are potentially malicious, whose contents have been tampered with since they were signed (HashMismatch), or whose signature comes from an untrusted certificate (UnknownError), use Where-Object to filter your results: dir c:\ -filter *.ps1 -recurse -erroraction silentlycontinue | GetAuthenticodeSignature | Where-Object { 'HashMismatch', 'NotSigned', 'UnknownError' -contains $_.Status } Status Message Description Since the file has no digital The file "xyz" is not digitally signed. The script will signature, you must use SetNotSigned not execute on the system. Please see "get-help AuthenticodeSignature to sign the about_signing" for more details. file. The used certificate is unknown. The file "xyz" cannot be loaded. A certificate chain Add the certificate publisher to the UnknownError processed, but ended in a root certificate which is not trusted root certificates authorities trusted by the trust provider. store. File XXX check this cannot be loaded. The contents of file "…" may have been tampered because the hash The file contents were changed. If HashMismatch of the file does not match the hash stored in the digital you changed the contents yourself, signature. The script will not execute on the system. resign the file. Please see "get-help about_signing" for more details. The file contents match the Valid Signature was validated. signature and the signature is valid. Table 10.3: Status reports of signature validation and their causes
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) } }
|
This time, the warning is a lot more explicit: WARNING: Failed to access "nonexistent" : The RPC server is unavailable. (Exception from HRESULT: 0x800706BA) in "C:\Users\w7pc9\AppData\Local\Temp\Untitled3.ps1" Here, two procedures were needed: first of all, the current computer name processed by Foreach-Object needed to be stored in a new variable because the standard $_ variable is reused inside the catch-block and refers to the current error. So it can no longer be used to read the current computer name. That's why the example stored the content of $_ in $currentcomputer before an error could occur. This way, the script code became more legible as well. Second, inside the catch-block, $_ resembles the current error. This variable contains a complex object which contains all details about the error. Information about the cause can be found in the property Exception whereas information about the place the error occured are found in InvocationInfo. To examine the object stored in $_, you can save it in a global variable. This way, the object remains accessible (else it would be discarded once the catch-block is processed). So when an error was handled, you can examine your test variable using Get-Member. This is how you would adjust the catch-block:
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 }
Handling Native Commands Most errors in your PowerShell code can be handled in the way described above. The only command type that does not fit into this error handling scheme are native commands. Since these commands were not developed specifically for PowerShell, and since they do not necessarily use the .NET framework, they cannot directly participate in PowerShells error handling. Console-based applications return their error messages through another mechanism: they emit error messages using the console ErrOut channel. PowerShell can monitor this channel and treat outputs that come from this channel as regular exceptions. To make this work, you need to do two things: first of all, you need to set $ErrorActionPreference to Stop, and second, you need to redirect the ErrOut channel to the StdOut channel because only this channel is processed by PowerShell. Here is an example: When you run the following native command, you will receive an error, but the error is not red nor does it look like the usual PowerShell error messages because it comes as plain text directly from the application you ran: PS> net user willibald The user name could not be found. More help is available by typing NET HELPMSG 2221.
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
+ net &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.
Handling Particular Exceptions The code set by Trap is by default executed for any (visible) exception. If you'd prefer to use one or several groups of different error handlers, write several Trap (or Catch) statements and specify for each the type of exception it should handle: function Test { trap [System.DivideByZeroException] { "Divided by null!"; continue } trap [System.Management.Automation.ParameterBindingException] { "Incorrect parameter!"; continue } 1/$null Dir -MacGuffin } Test Divided by null! Incorrect parameter!
Throwing Your Own Exceptions If you develop functions or scripts and handle errors, you are free to output error information any way you want. You could output it as plain text, use a warning or write error information to a log file. With any of these, you take away the opportunity for the caller to respond to errors - because the caller has no longer a way of detecting the error. That's why you can also throw your own exceptions. They can be caught by the caller using a trap or a try/catch-block. function TextOutput([string]$text) { if ($text -eq "") { Throw "You must enter some text." } else { "OUTPUT: $text" } } # An error message will be thrown if no text is entered: TextOutput You have to enter some text. At line:5 char:10 + Throw dir DEBUG: 1+ > 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 "@.
Communicating with the User Maybe you don't want to hard-code text information in your script at all but instead provide a way for the user to enter information. To accept plain text input use Read-Host: $text = Read-Host "Enter some text" Enter some text: Hello world! $text Hello world!
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
Composing Text with "-f" The –f format operator is the most important PowerShell string operator. You'll soon be using it to format numeric values for easier reading: "{0:0} diskettes per CD" -f (720mb/1.44mb) 500 diskettes per CD
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 > "@ >> $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.
Removing White Space
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
Finding and Removing Doubled Words How is it possible to find and remove doubled words in text? Here, you can use back referencing again. The pattern could be described as follows: "\b(\w+)(\s+\1){1,}\b" The pattern searched for is a word (anchor: "\b"). It consists of one word (the character "\w" and quantifier "+"). A blank character follows (the character "\s" and quantifier "?"). This pattern, the blank character and the repeated word, must occur at least once (at least one and any number of iterations of the word, quantifier "{1,}"). The entire pattern is then replaced with the first back reference, that is, the first located word. # Find and remove doubled words in a text: "This this this is a test" -replace "\b(\w+)(\s+\1){1,}\b", '$1' This is a test
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 today‘s 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:
Taking a Look At XML Structure Loading and Processing XML Files o Accessing Single Nodes and Modifying Data o Using SelectNodes() to Choose Nodes o Accessing Attributes o Adding New Nodes Exloring the Extended Type System o The XML Data of the Extended Type System o Finding Pre-Defined Views
Taking a Look At XML Structure XML uses tags to uniquely identify pieces of information. A tag is a pair of angle brackets like the ones used for HTML documents. Typically, a piece of information is delimited by a start and end tag. The end tag is preceded by "/"; the result is called a "node", and in the next example, the node is called "Name": Tobias Weltner Nodes can be decorated with attributes. Attributes are stored in the start tag of the node like this: ... If a node has no particular content, its start and end tags can be combined, and the ending symbol "/" drifts toward the end of the tag. If the branch office in Hanover doesn't have any staff currently working in the field, the tag could look like this: The following XML structure describes two staff members of the Hanover branch office who are working in the sales department. Tobias Weltner management 39 Cofi Heidecke security 4
The XML data is wrapped in an XML node which is the top node of the document:
This particular header contains a version attribute which declares that the XML structure conforms to the specifications of XML version 1.0. There can be additional attributes in the XML header. Often you find a reference to a "schema", which is a formal description of the structure of that XML file. The schema could, for example, specify that there must always be a node called "staff" as part of staff information, which in turn could include as many sub-nodes named "staff" as required. The schema would also specify that information relating to name and function must also be defined for each staff member. Because XML files consist of plain text, you can easily create them using any editor or directly from within PowerShell. Let's save the previous staff list as an xml file: $xml = @' Tobias Weltner management 39 Cofi Heidecke security 4 '@ | Out-File $env:temp\employee.xml
XML is case-sensitive!
Loading and Processing XML Files To read and evaluate XML, you can either convert the text to the XML data type, or you can instantiate a blank XML object and load the XML from a file or a URL in the Internet. This line would read the content from a file $env:temp\employee.xml and convert it to XML: $xmldata = [xml](Get-Content $env:temp\employee.xml) A faster approach uses a blank XML object and its Load() method: $xmldata = New-Object XML $xmldata.Load("$env:temp\employee.xml")
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
Accessing Single Nodes and Modifying Data To pick out a specific node from a set of nodes, you can use the PowerShell pipeline and Where-Object. This would pick out a particular employee from the list of staff. As you will see, you can not only read data but also change it. $xmldata.staff.employee | Where-Object { $_.Name -match "Tobias Weltner" } Name function Age -----------Tobias Weltner management 39 $employee = $xmldata.staff.employee | Where-Object { $_.Name -match "Tobias Weltner" } $employee.function = "vacation" $xmldata.staff.employee Name function Age -----------Tobias Weltner vacation 39 Cofi Heidecke security 4
If you want to save changes you applied to XML data, call the Save() method: $xmldata.Save("$env:temp\updateddata.xml")
Using SelectNodes() to Choose Nodes Another way of picking nodes is to use the method SelectNode() and its so-called XPath query language. So, to get to the employee data below the staff node, use this approach: $xmldata.SelectNodes('staff/employee') Name Age -------Tobias Weltner 39
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[age1]')
Alternatively, you can also use an XpathNavigator: # Create navigator for XML: $xpath = [System.XML.XPath.XPathDocument][System.IO.TextReader][System.IO.StringReader ]` (Get-Content $env:temp\employee.xml | Out-String) $navigator = $xpath.CreateNavigator() # Output the last employee name of the Hanover branch office: $query = "/staff[@branch='Hanover']/employee[last()]/Name" $navigator.Select($query) | Format-Table Value Value ----Cofi Heidecke # Output all employees of the Hanover branch office except for Tobias Weltner: $query = "/staff[@branch='Hanover']/employee[Name!='Tobias Weltner']" $navigator.Select($query) | Format-Table Value Value
----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
Use GetAttribute() if you'd like to query a particular attribute: $xmldata.staff.GetAttribute("branch") Hanover
Use SetAttribute() to specify new attributes or modify (overwrite) existing ones: $xmldata.staff.SetAttribute("branch", "New York") $xmldata.staff.GetAttribute("branch") New York
Adding New Nodes If you'd like to add new employees to your XML, use CreateElement() to create an employee element and then fill in the data. Finally, add the element to the XML: # Create new node: $newemployee = $xmldata.CreateElement("employee") $newemployee.InnerXML = 'Bernd Seilerexpert' # Write nodes in XML: $xmldata.staff.AppendChild($newemployee) # Check result: $xmldata.staff.employee Name Age -------Tobias Weltner 39 Cofi Heidecke 4 Bernd Seiler
function ----management security expert
# Output plain text: $xmldata.get_InnerXml() Tobias Weltnermanagement39 Cofi Heideckesecurity 4Bernd Seiler expert
Exploring the Extended Type System The PowerShell Extended Type System (ETS) is XML-based, too. The ETS is responsible for turning objects into readable text. PowerShell comes with a set of xml files that all carry the extension ".ps1xml". There are format-files and type-files. Format-files control which object properties are shown and how the object structure is represented. Type-format files control which additional properties and methods should be added to objects. With the basic knowledge about XML that you gained so far, you can start exploring the ETS XML files and learn more about the inner workings of PowerShell.
The XML Data of the Extended Type System Whenever PowerShell needs to convert an object into text, it searches through its internal "database" to find information about how to best format and display the object. This database really is a collection of XML files in the PowerShell root folder $pshome: Dir $pshome\*.format.ps1xml All these files define a multitude of Views, which you can examine using PowerShell XML support. [xml]$file = Get-Content "$pshome\dotnettypes.format.ps1xml" $file.Configuration.ViewDefinitions.View Name ViewSelectedBy TableControl --------------------System.Reflection.Assembly ViewSelectedBy TableControl System.Reflection.AssemblyName ViewSelectedBy TableControl System.Globalization.CultureInfo ViewSelectedBy TableControl System.Diagnostics.FileVersionInfo ViewSelectedBy TableControl System.Diagnostics.EventLogEntry ViewSelectedBy TableControl System.Diagnostics.EventLog ViewSelectedBy TableControl System.Version ViewSelectedBy TableControl System.Drawing.Printing.PrintDo... ViewSelectedBy TableControl
-------
Dictionary TableControl ProcessModule TableControl process TableControl PSSnapInInfo PSSnapInInfo TableControl Priority TableControl StartTime TableControl service TableControl (...)
ViewSelectedBy ViewSelectedBy ViewSelectedBy ViewSelectedBy ViewSelectedBy ViewSelectedBy ViewSelectedBy ViewSelectedBy
Finding Pre-Defined Views Pre-defined views are interesting because you can use the -View parameter to change the way PowerShell presents results with the cmdlets Format-Table or Format-List. Get-Process | Format-Table -View Priority Get-Process | Format-Table -View StartTime
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 PowerShell‘s 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:
Getting to Know Your Tools Accessing Files and Directories o Listing Folder Contents o Choosing the Right Parameters o Getting File and Directory Items o Passing Files to Cmdlets, Functions, or Scripts o Selecting Files or Folders Only Navigating the File System o Relative and Absolute Paths o Converting Relative Paths into Absolute Paths o Pushing and Popping Directory Locations o Special Directories and System Paths o Constructing Paths Working with Files and Directories o Creating New Directories o Creating New Files o Reading the Contents of Text Files o Processing Comma-Separated Lists o Moving and Copying Files and Directories o Renaming Files and Directories o Bulk Renames o Deleting Files and Directories o Deleting Directory Contents o Deleting Directories Plus Content
Getting to Know Your Tools One of the best ways to get to know your set of file system-related PowerShell cmdlets is to simply list all aliases that point to cmdlets with the keyword "item" in their noun part. That is so because PowerShell calls everything "item" that lives on a drive. 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
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 ---------... ... ... ... ...
Accessing Files and Directories Use Get-ChildItem to list the contents of a folder. There are two historic aliases: Dir and ls. Get-ChildItem handles a number of important file system-related tasks:
Searching the file system recursively and finding files Listing hidden files Accessing files and directory objects Passing files to other cmdlets, functions, or scripts
Listing Folder Contents If you don't specify a path, Get-ChildItem lists the contents of the current directory. Since the current directory can vary, it is risky to use Get-Childitem in scripts without specifying a path. Omit a path only when you use PowerShell interactively and know where your current location actually is. Time to put Get-ChildItem to work: to get a list of all PowerShell script files stored in your profile folder, try this: PS> Get-ChildItem -Path $home -Filter *.ps1 Most likely, this will not return anything because, typically, your own files are not stored in the root of your profile folder. To find script files recursively (searching through all child folders), add the switch parameter -Recurse: PS> Get-ChildItem -Path $home -Filter *.ps1 -Recurse This may take much longer. If you still get no result, then maybe you did not create any PowerShell script file yet. Try searching for other file types. This line will get all Microsoft Word documents in your profile:
PS> Get-ChildItem -Path $home -Filter *.doc* -Recurse When searching folders recursively, you may run into situations where you do not have access to a particular subfolder. Get-ChildItem then raises an exception but continues its search. To hide such error messages, add the common parameter -Erroraction SilentlyContinue which is present in all cmdlets, or use its short form -ea 0: PS> Get-ChildItem -Path $home -Filter *.doc* -Recurse -ea 0 The -Path parameter accepts multiple comma-separated values, so you could search multiple drives or folders in one line. This would find all .log-files on drives C:\ and D:\ (and takes a long time because of the vast number of folders it searches): PS> Get-ChildItem c:\, d:\ -Filter *.log -Recurse -ea 0 If you just need the names of items in one directory, use the parameter -Name: PS> Get-ChildItem -Path $env:windir -Name To list only the full path of files, use a pipeline and send the results to Select-Object to only select the content of the FullName property: PS> Get-ChildItem -Path $env:windir | Select-Object -ExpandProperty FullName Some characters have special meaning to PowerShell, such as square brackets or wildcards such as '*'. If you want PowerShell to ignore special characters in path names and instead take the path literally, use the -LiteralPath parameter instead of -Path.
Choosing the Right Parameters In addition to -Filter, there is a parameter that seems to work very similar: -Include: PS> Get-ChildItem $home -Include *.ps1 -Recurse You'll see some dramatic speed differences, though: -Filter works significantly faster than -Include. PS> (Measure-Command {Get-ChildItem $home -Filter *.ps1 Recurse}).TotalSeconds 4,6830099 PS> (Measure-Command {Get-ChildItem $home -Include *.ps1 Recurse}).TotalSeconds 28,1017376
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
Getting File and Directory Items Everything on a drive is called "Item", so to get the properties of an individual file or folder, use Get-Item: PS> Get-Item $env:windir\explorer.exe | Select-Object * PSPath : Microsoft.PowerShell.Core\FileSystem::C:\Windows\explorer.e xe PSParentPath : Microsoft.PowerShell.Core\FileSystem::C:\Windows PSChildName : explorer.exe
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?
Passing Files to Cmdlets, Functions, or Scripts Because Get-ChildItem returns individual file and folder objects, Get-ChildItem can pass these objects to other cmdlets or to your own functions and scripts. This makes Get-ChildItem an important selection command which you can use to recursively find all the files you may be looking for, across multiple folders or even drives.
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
2008.1108.641... C:\Program Files\Common Files\Microsoft
Selecting Files or Folders Only Because Get-ChildItem does not differentiate between files and folders, it may be important to limit the result of Get-ChildItem to only files or only folders. There are several ways to accomplish this. You can check the type of returned object, check the PowerShell PSIsContainer property, or examine the mode property: # List directories only: PS> Get-ChildItem | Where-Object { $_ -is [System.IO.DirectoryInfo] } PS> Get-ChildItem | Where-Object { $_.PSIsContainer } PS> Get-ChildItem | Where-Object { $_.Mode -like 'd*' } # List files only: PS> Get-ChildItem | Where-Object { $_ -is [System.IO.FileInfo] } PS> Get-ChildItem | Where-Object { $_.PSIsContainer -eq $false} PS> Get-ChildItem | Where-Object { $_.Mode -notlike 'd*' }
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) }
Navigating the File System Unless you changed your prompt (see Chapter 9), the current directory is part of your input prompt. You can find out the current location by calling Get-Location: PS> Get-Location Path ---C:\Users\Tobias
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
Relative and Absolute Paths Paths can either be relative or absolute. Relative path specifications depend on the current directory, so .\test.txt always refers to the test.txt file in the current directory. Likewise, ..\test.txt refers to the test.txt file in the parent directory. Relative path specifications are useful, for example, when you want to use library scripts that are located in the same directory as your work script. Your work script will then be able to locate library scripts under relative paths—no matter what the directory is called. Absolute paths are always unique and are independent of your current directory. Character . .. \ ~
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
Converting Relative Paths into Absolute Paths Whenever you use relative paths, PowerShell must convert these relative paths into absolute paths. That occurs automatically when you submit a relative path to a cmdlet. You can resolve relative paths manually, too, by using Resolve-Path. PS> Resolve-Path .\test.txt Path ---C:\Users\Tobias Weltner\test.txt
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
Pushing and Popping Directory Locations The current directory can be ―pushed‖ onto a ―stack‖ by using Push-Location. Each Push-Location adds a new directory to the top of the stack. Use Pop-Location to get it back again. So, to perform a task that forces you to temporarily leave your current directory, first type Push-Location to store your current location. Then, you can complete your task and when use Pop-Location to return to where you were before. Cd $home will always take you back to your home directory. Also, both Push-Location and Pop-Location support the -Stack parameter. This enables you to create as many stacks as you want, such as one for each task. PushLocation -Stack job1 puts the current directory not on the standard stack, but on the stack called ―job1‖; you can use Pop-Location -Stack job1 to restore the initial directory from this stack.
Special Directories and System Paths There are many standard folders in Windows, for example the Windows folder itself, your user profile, or your desktop. Since the exact location of these paths can vary depending on your installation setup, it is bad practice to
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 user‘s 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
You can check content of your desktop: PS> Get-ChildItem desktop: And if you'd like to see all the drives accessible to you, run this command: PS> Get-PSDrive
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
Or, you can use .NET framework methods: PS> $path = [System.IO.Path]::Combine([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()
Description Changes the file extension Combines path strings; Combine() corresponds to Join-Path Returns the directory; GetDirectoryName() corresponds to Split-Path -parent GetExtension() Returns the file extension Returns the file name; GetFileName() corresponds to Split-Path -leaf Returns the file name without the GetFileNameWithoutExtension() file extension
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("c:\test\file.txt")
GetFullPath() GetInvalidFileNameChars() GetInvalidPathChars() GetPathRoot() GetRandomFileName() GetTempFileName() GetTempPath() HasExtension() IsPathRooted()
Returns the absolute path Lists all characters that are not allowed in a file name Lists all characters that are not allowed in a path Gets the root directory; corresponds to Split-Path qualifier Returns a random file name Returns a temporary file name in the Temp directory Returns the path of the directory for temporary files True, if the path includes a file extension True, if the path is absolute; corresponds to Split-Path isAbsolute
GetFullPath(".\test.txt") GetInvalidFileNameChars() GetInvalidPathChars() GetPathRoot("c:\test\file.txt") GetRandomFileName() GetTempFileName() GetTempPath() HasExtension("c:\test\file.txt") IsPathRooted("c:\test\file.txt")
Table 15.4: Methods for constructing paths
Working with Files and Directories The cmdlets Get-ChildItem and Get-Item can get you file and directory items that already exist. In addition, you can create new files and directories, rename them, fill them with content, copy them, move them, and, of course, delete them.
Creating New Directories The easiest way to create new directories is to use the Md function, which invokes the cmdlet New-Item internally and specifies as -ItemType parameter the Directory value: # "md" is the predefined function and creates new directories: PS> md Test1 Directory: Microsoft.PowerShell.Core\FileSystem::C:\users\Tobias Weltner Mode ---d----
LastWriteTime ------------12.10.2011 17:14
Length Name ------ ---Test1
# "New-Item" can do that, too, but takes more effort: PS> New-Item Test2 -ItemType Directory Directory: Microsoft.PowerShell.Core\FileSystem::C:\users\Tobias Weltner Mode ---d----
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.
Creating New Files You can use New-Item to also create new files. Use -Value if you want to specify text to put into the new file, or else you create an empty file: PS> New-Item "new file.txt" -ItemType File Directory: Microsoft.PowerShell.Core\FileSystem::C:\users\Tobias Weltner Mode ----a---
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 PowerShell‘s 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>
Get-ChildItem | ConvertTo-HTML | Out-File report1.hta .\report1.hta Get-ChildItem | ConvertTo-HTML | Set-Content report2.hta .\report2.htm
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.
Reading the Contents of Text Files Use Get-Content to retrieve the contents of a text-based file: PS> Get-Content $env:windir\windowsupdate.log There is a shortcut that uses variable notation if you know the absolute path of the file: PS> ${c:\windows\windowsupdate.log} However, this shortcut usually isn‘t very practical because it doesn‘t allow any variables inside curly brackets. You would have to hardcode the exact path to the file into your scripts. Get-Content reads the contents of a file line by line and passes on every line of text through the pipeline. You can add Select-Object if you want to read only the first 10 lines of a very long file: PS> Get-Content $env:windir\windowsupdate.log | Select-Object -First 10 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
Accessing Files and Directories Use Import-Csv if you want to process information from comma-separated lists in PowerShell. For example, you could export an Excel spreadsheet as CSV-file and then import the data into PowerShell. When you use Get-Content to read a CSV-file, you'd see the plain text. A much better way is to use Import-CSV. It honors the delimiter and returns objects. Each column header turns into an object property. To successfully import CSV files, make sure to use the parameter -UseCulture or -Delimiter if the list is not commaseparated. Depending on your culture, Excel may have picked a different delimiter than the comma, and UseCulture automatically uses the delimiter that Excel used.
Moving and Copying Files and Directories Move-Item and Copy-Item perform moving and copying operations. You may use wildcard characters with them. The following line copies all PowerShell scripts from your home directory to the Desktop: PS> Copy-Item $home\*.ps1 ([Environment]::GetFolderPath("Desktop")) Use Get-Childitem to copy recursively. Let it find the PowerShell scripts for you, and then pass the result on to Copy-Item: Before you run this line you should be aware that there may be hundreds of scripts, and unless you want to completely clutter your desktop, you may want to first create a folder on your desktop and then copy the files into that folder. PS> Get-ChildItem -Filter *.ps1 -Recurse | >> Copy-Item -Destination ([Environment]::GetFolderPath("Desktop"))}
Renaming Files and Directories Use Rename-Item if you want to rename files or folders. Renaming files or folders can be dangerous, so do not rename system files or else Windows may stall. PS> Set-Content $env:temp\testfile.txt "Hello,this,is,an,enumeration" # file opens in notepad: PS> Invoke-Item $env:temp\testfile.txt # file opens in Excel now: PS> Rename-Item $env:temp\testfile.txt testfile.csv PS> Invoke-Item $env:temp\testfile.csv
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.
Deleting Files and Directories Use Remove-Item or the Del alias to remove files and folders. If a file is write-protected, or if a folder contains data, you'll have to confirm the operation or use the -Force parameter. # Create an example file: PS> $file = New-Item testfile.txt -ItemType file # There is no write protection:
PS> $file.isReadOnly False # Activate write protection: PS> $file.isReadOnly = $true PS> $file.isReadOnly True # Write-protected file may be deleted only by using the –Force parameter: PS> del testfile.txt Remove-Item : Cannot remove item C:\Users\Tobias Weltner\testfile.txt: Not enough permission to perform operation. At line:1 char:4 + del Remove-Item $recents\*.* -WhatIf
You can as well put this in one line, too: PS> Get-Childitem ([Environment]::GetFolderPath('Recent')) | Remove-Item WhatIf This however would also delete subfolders contained in your Recent folder because Get-ChildItem lists both files and folders. If you are convinced that your command is correct, and that it will delete the correct files, repeat the statement without -WhatIf. Or, you could use -Confirm instead to manually approve or deny each delete operation.
Deleting Directories Plus Content PowerShell requests confirmation whenever you attempt to delete a folder that is not empty. Only the deletion of empty folders does not require confirmation: # Create a test directory: md testdirectory Directory: Microsoft.PowerShell.Core\FileSystem::C:\Users\Tobias Weltner\Sources\docs Mode
LastWriteTime
Length Name
---d----
------------13.10.2011 13:31
------ ---testdirectory
# Create a file in the directory: PS> Set-Content .\testdirectory\testfile.txt "Hello" # Delete directory: PS> del testdirectory Confirm The item at "C:\Users\Tobias Weltner\Sources\docs\testdirectory" 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 [K] No to All [H] Suspend [?] Help (default is "Y"):
To delete folders without confirmation, add the parameter -Recurse: PS> Remove-Item testdirectory -Recurse Alias Description ac Adds the contents of a file cls, clear Clears the console window cli Clears file of its contents, but not the file itself copy, cp, cpi Copies file or directory Dir, ls, gci Lists directory contents type, cat, gc Reads contents of text-based file gi Accesses specific file or directory gp
Reads property of a file or directory
ii
Invokes file or directory using allocated Windows program Joins two parts of a path into one path, for example, a drive and a file name Moves files and directories Creates new file or new directory
mi, mv, move ni ri, rm, rmdir, del, erase, rd rni, ren rvpa sp Cd, chdir, sl -
Cmdlet Add-Content Clear-Host Clear-Item Copy-Item Get-Childitem Get-Content Get-Item GetItemProperty Invoke-Item Join-Path Move-Item New-Item
Deletes empty directory or file
Remove-Item
Renames file or directory Resolves relative path or path including wildcard characters Sets property of file or directory Changes to specified directory Extracts a specific part of a path like the parent path, drive, or file name Returns True if the specified path exists
Rename-Item Resolve-Path Set-ItemProperty Set-Location Split-Path Test-Path
Table 15.1: Overview of the most important file system commands Thanks to PowerShells universal "Provider" concept, you can navigate the Windows Registry just as you would the file system. In this chapter, you will learn how to read and write Registry keys and Registry values.
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 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 Rename-ItemProperty Set-Item Set-ItemProperty
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... Microsoft.PowerSh... Microsoft.PowerSh... Microsoft.PowerSh...
Definition ---------... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ...
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
Capabilities -----------ShouldProcess ShouldProcess filter, ShouldProcess
Drives -----{Alias} {Env} {C, E, S, D}
function Registry Variable Certificate
ShouldProcess ShouldProcess ShouldProcess ShouldProcess
{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 Certificate
Example Dir Alias: $alias:Dir Dir env: $env:windir Dir function: $function:tabexpansion
Dir c: $(c:\autoexec.bat) Dir HKCU: Provides access to branches of the Windows registry. Dir HKLM: Manages all the variables that are defined in the PowerShell console. Dir variable: Variables are covered in Chapter 3. $variable:pshome Provides access to the certificate store with all its digital certificates. These are Dir cert: examined in detail in Chapter 10. Dir cert: -recurse Provides access to drives, directories and files.
Table 16.2: Default providers
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.
Using Provider Names Directly Actually, you do not need PowerShell drives at all to access the Registry. In many scenarios, it can be much easier to work with original Registry paths. To make this work, prepend the paths with the provider names like in the example below: Dir Dir Dir Dir
HKLM:\Software Registry::HKEY_LOCAL_MACHINE\Software Registry::HKEY_USERS Registry::HKEY_CLASSES_ROOT\.ps1
With this technique, you can even list all the Registry hives:
Dir Registry::
Searching for Keys Get-ChildItem can list all subkeys of a key, and it can of course use recursion to search the entire Registry for keys with specific keywords. The registry provider doesn't support filters, though, so you cannot use the parameter -Filter when you search the registry. Instead, use -Include and -Exclude. For example, if you wanted to find all Registry keys that include the word ―PowerShell‖, you could search using: PS> Get-ChildItem HKCU:, HKLM: -Recurse -Include *PowerShell* -ErrorAction SilentlyContinue | >> Select-Object -ExpandProperty Name >> HKEY_CURRENT_USER\Console\%SystemRoot%_System32_WindowsPowerShell_v1.0_powers hell.exe HKEY_CURRENT_USER\Software\Microsoft\PowerShell HKEY_CURRENT_USER\Software\Microsoft\PowerShell\1\ShellIds\Microsoft.PowerShe ll
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.
Searching for Values Since Registry values are not interpreted as separate items but rather are added to keys as so-called ItemProperties, you cannot use Get-ChildItem to search for Registry values. You can search for values indirectly, though. Here is some code that finds all Registry keys that have at least one value with the keyword "PowerShell": PS> Get-ChildItem HKCU:, HKLM: -Recurse -ea 0 | Where-Object { $_.GetValueNames() | >> Where-Object { $_ -like '*PowerShell*' } } If you want to find all keys that have a value with the keyword in its data, try this: PS> Get-ChildItem HKCU:, HKLM: -Recurse -ea 0 | Where-Object { $key = $_; $_.GetValueNames() | >> ForEach-Object { $key.GetValue($_) } | Where-Object { $_ -like '*PowerShell*' } }
Reading One Registry Value If you need to read a specific Registry value in a Registry key, use Get-ItemProperty. This example reads the registered owner: PS> Get-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion' -Name RegisteredOwner RegisteredOwner : Tim Telbert
PSPath PSParentPath PSChildName PSDrive PSProvider
: Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion : Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT : CurrentVersion : HKLM : Registry
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
Reading Multiple Registry Values Maybe you'd like to read more than one Registry value. Registry keys can hold an unlimited number of values. The code is not much different from before. Simply replace the single Registry value name with a comma-separated list, and again use Select-Object to focus only on those. Since this time you are reading multiple properties, use Property instead of –ExpandProperty parameter. PS> Get-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion' ` >> -Name ProductName, EditionID, CSDVersion, RegisteredOwner | >> Select-Object -Property ProductName, EditionID, CSDVersion, RegisteredOwner >> ProductName ----------Windows 7 Ultimate
EditionID --------Ultimate
CSDVersion ---------Service Pack 1
RegisteredOwner --------------Tim Telbert
Or, a little simpler: PS> Get-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion' | >> Select-Object -Property ProductName, EditionID, CSDVersion, RegisteredOwner >> ProductName ----------Windows 7 Ultimate
EditionID --------Ultimate
CSDVersion ---------Service Pack 1
RegisteredOwner --------------Tim Telbert
Reading Multiple Keys and Values 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 }
Creating Registry Keys Since Registry keys are treated like files or folders in the file system, you can create and delete them accordingly. To create new keys, either use historic aliases like md or mkdir, or use the underlying cmdlet directly: PS> New-Item HKCU:\Software\NewKey1 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
Deleting Registry Keys To delete a key, use the historic aliases from the file system that you would use to delete a folder, or use the underlying cmdlet Remove-Item directly: PS> Remove-Item HKCU:\Software\Test1 Del HKCU:\Software\Test2 Del HKCU:\Software\Test3
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: Del "HKCU:\Software\First key" -Recurse
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
: : : : : : : : : :
-Name Name -Value 'Smith' -Name ID -Value 12 -Type DWORD -Name Path -Value '%WINDIR%' -Name Notes -Value 'First
-Name DigitalInfo -Value
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
Table 16.4: Permitted ItemTypes in the registry 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
Securing Registry Keys Registry keys (and its values) can be secured with Access Control Lists (ACLs) in pretty much the same way the NTFS file system manages access permissions to files and folders. Likewise, you can use Get-Acl to show current permissions of a key: md HKCU:\Software\Testkey4 Get-Acl HKCU:\Software\Testkey Path Owner -------Microsoft.PowerShell.Core\Registr... TobiasWeltne-PC\Tobias Weltner TobiasWeltne-PC\Tobias Weltner A...
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): $acl = Get-Acl HKCU:\Software\Testkey $acl.Owner
scriptinternals\TobiasWeltner $me = [System.Security.Principal.NTAccount]"$env:userdomain\$env:username" $acl.SetOwner($me)
Setting New Access Permissions The next step is to assign new permissions to the key. Let's exclude the group ―Everyone‖ from making changes to this key: $acl = Get-Acl HKCU:\Software\Testkey $person = [System.Security.Principal.NTAccount]"Everyone" $access = [System.Security.AccessControl.RegistryRights]"WriteKey" $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.AddAccessRule($rule) Set-Acl HKCU:\Software\Testkey $acl
The modifications immediately take effect.Try creating new subkeys in the Registry editor or from within PowerShell, and you‘ll 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 Get-Process -FileVersionInfo -ErrorAction SilentlyContinue PS> Get-Process -FileVersionInfo -ea 0
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 ---------------
Console Window Host Microsoft Corpor... DataCardMonitor ... Huawei Technolog... DataCardMonitor Dropbox Dropbox, Inc. Desktop Window M... Microsoft Corpor...
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
storage1 - Remot... mstsc
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
Accessing Process Objects
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" }
Launching New Processes (Applications) Launching applications from PowerShell is pretty straight-forward: simply enter the name of the program you want to run, and press ENTER: PS> notepad PS> regedit PS> ipconfig
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:
PS> Start-Process iexplore.exe Second, Start-Process supports a number of parameters that allow you to control window size, synchronous or asynchronous execution or even the user context an application is using to run. For example, if you wanted PowerShell to wait for a window-based application so a script could execute applications in a strict order, use -Wait parameter: PS> Start-Process notepad -Wait You'll notice that PowerShell now waits for the Notepad to close again before it accepts new commands. Start-Process has just one limitation: it cannot return the results of console-based applications back to you. Check this out: PS> $result = ipconfig This will store the result of ipconfig in a variable. The same done with Start-Process yields nothing: PS> $result = Start-Process ipconfig That's because Start-Process by default runs every command synchronously, so ipconfig runs in its own new console window which is visible for a split-second if you look carefully. But even if you ask Start-Process to run the command in no new console, results are never returned: PS> $result = Start-Process ipconfig -NoNewWindow Instead, they are always output to the console. So if you want to read information from console-based applications, do not use Start-Process.
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 shouldn‘t 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
Table 17.1: Cmdlets for managing services
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
Starting, Stopping, Suspending, and Resuming Services To start, stop, temporarily suspend, or restart a service, use the corresponding cmdlets. You can also use Get-Service to select the services first, and then pipe them to one of the other cmdlets. Just note that you may need local administrator privileges to change service properties. PS> Stop-Service Spooler If a service has dependent services, it cannot be stopped unless you also specify -Force. PS> Stop-Service Spooler -Force Note that you can use WMI to find out more information about services, and also manage services on remote machines: PS> Get-WmiObject Win32_Service -ComputerName Storage1 -Credential Administrator Since WMI includes more information that Get-Service, you could filter for all services set to start automatically that are not running. By examining the service ExitCode property, you'd find services that did initialization tasks and finished ok (exit code is 0) or that crashed (exit code other than 0): PS> Get-WmiObject Win32_Service -Filter 'StartMode="Auto" and Started=False' | >> Select-Object DisplayName, ExitCode >> DisplayName
ExitCode
-----------------Microsoft .NET Framework NGEN v4.0.30319_X86 0 Microsoft .NET Framework NGEN v4.0.30319_X64 0 Google Update Service (gupdate) 0 Net Driver HPZ12 0 Pml Driver HPZ12 0 Software Protection 0 Windows Image Acquisition (WIA) 0
Reading and Writing Event Logs Event logs are the central place where applications as well as the operating system log all kinds of information grouped into the categories Information, Warning, and Error. PowerShell can read and write these log files. To find out which log files exist on your machine, use Get-EventLog with the parameter -List: PS> Get-EventLog -List To list the content of one of the listed event logs, use -LogName instead. This lists all events from the System event log: PS> Get-EventLog -LogName System Dumping all events is not a good idea, though, because this is just too much information. In order to filter the information and focus on what you want to know, take a look at the column headers. If you want to filter by the content of a specific column, look for a parameter that matches the column name. This line gets you the latest 20 errors from the System event log: PS> Get-EventLog -LogName System -EntryType Error -Newest 20 And this line gets you all error and warning entries that have the keyword "Time" in its message: PS> Get-EventLog -LogName System -EntryType Error, Warning -Message *Time* | >> Select-Object TimeWritten, Message
Writing Entries to the Event Log To write your own entries to one of the event logs, you first need to register an event source (which acts like your "sender" address). To register an event source, you need local administrator privileges. Make sure you open PowerShell with full administrator rights and use this line to add a new event source called "PowerShellScript" to the Application log: PS> New-EventLog -LogName Application -Source PowerShellScript Note that an event source must be unique and may not exist already in any other event log. To remove an event source, use Remove-EventLog with the same parameters as above, but be extremely careful. This cmdlet can wipe out entire event logs. Once you have registered your event source, you are ready to log things to an event log. Logging (writing) event entries no longer necessarily requires administrative privileges. Since we added the event source to the Application
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.
WMI Quick Start Retrieving Information o Exploring WMI Classes o Swallowing The Red Pill o Filtering WMI Results o Direct WMI Object Classes Changing System Configuration o Modifying Properties o Invoking WMI Methods o Static Methods o Using WMI Auto-Documentation WMI Events Using WMI Remotely o Accessing WMI Objects on Another Computer WMI Background Information o Converting the WMI Date Format
WMI Quick Start To work with WMI you need to know just a little bit of theory. Let's check out what the terms "class" and "object" stand for.
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
Exploring WMI Classes As you can see, working with WMI does not require much knowledge. It does require though that you know the name of a WMI class that represents what you are after. Fortunately, Get-WmiObject can also work like a dictionary and look up WMI class names for you. This will get you all WMI class names that have the keyword "print" in them: PS> Get-WmiObject -List Win32_*Print* NameSpace: ROOT\cimv2 Name ---Win32_PrinterConfiguration Captio... Win32_PrinterSetting Win32_PrintJob Da... Win32_Printer Availa... Win32_PrinterDriver ConfigFil... Win32_TCPIPPrinterPort Caption...
Methods ------{}
Properties ---------{BitsPerPel,
{} {Pause, Resume}
{Element, Setting} {Caption, Color,
{SetPowerState, R... {Attributes, {StartService, St... {Caption, {}
{ByteCount,
Win32_PrinterShare Depend... Win32_PrinterDriverDll Depend... Win32_PrinterController Antec...
{}
{Antecedent,
{}
{Antecedent,
{}
{AccessState,
Swallowing The Red Pill By default, PowerShell limits the information WMI returns to you so you don't get carried away. It's pretty much like in the movie "The Matrix": you need to decide whether you want to swallow the blue pill and live in a simple world, or whether you dare to swallow the red pill and see the real world. By default, you live in the blue-pill-world with only limited information. PS> Get-WmiObject -Class Win32_BIOS SMBIOSBIOSVersion Manufacturer Name SerialNumber Version
: : : : :
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
Filtering WMI Results Often, there are more instances of a class than you need. For example, when you query for Win32_NetworkAdapter, you get all kinds of network adapters, including virtual adapters and miniports. PowerShell can filter WMI results client-side using Where-Object. So, to get only objects that have a MACAddress, you could use this line: PS> Get-WmiObject Win32_NetworkAdapter | Where-Object { $_.MACAddress -ne $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
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:
PS> Get-WmiObject Win32_NetworkAdapterConfiguration | Where-Object { $_.IPAddress -ne $null } | >> Select-Object Caption, IPAddress, MACAddress >> Caption IPAddress MACAddress -----------------------[00000011] Intel(R) WiF... {192.168.2.109, fe80::a... 00:22:FA:D9:E1:50
If you translated this to a server-side WMI filter, it fails: PS> Get-WmiObject Win32_NetworkAdapterConfiguration -Filter 'IPAddress != NULL' | >> Select-Object Caption, IPAddress, MACAddress >> Get-WmiObject : Invalid query "select * from Win32_NetworkAdapterConfiguration where IPAddress != NULL"
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 PowerShell‘s 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
Windows Media Player Ne... Running
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
Direct WMI Object Access Every WMI instance has its own unique path. This path is important if you want to access a particular instance directly. The path of a WMI object is located in the __PATH property. First use a "traditional" query to list this property and find out what it looks like: Get-WmiObject Win32_Service | ForEach-Object { $_.__PATH } \\JSMITH-PC\root\cimv2:Win32_Service.Name="AeLookupSvc" \\JSMITH-PC\root\cimv2:Win32_Service.Name="AgereModemAudio" \\JSMITH-PC\root\cimv2:Win32_Service.Name="ALG" \\JSMITH-PC\root\cimv2:Win32_Service.Name="Appinfo" \\JSMITH-PC\root\cimv2:Win32_Service.Name="AppMgmt" \\JSMITH-PC\root\cimv2:Win32_Service.Name="Ati External Event Utility" \\JSMITH-PC\root\cimv2:Win32_Service.Name="AudioEndpointBuilder" \\JSMITH-PC\root\cimv2:Win32_Service.Name="Audiosrv" \\JSMITH-PC\root\cimv2:Win32_Service.Name="Automatic LiveUpdate - Scheduler" \\JSMITH-PC\root\cimv2:Win32_Service.Name="BFE" \\JSMITH-PC\root\cimv2:Win32_Service.Name="BITS" \\JSMITH-PC\root\cimv2:Win32_Service.Name="Browser" (...)
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 don‘t necessarily need to specify the name of the key property as long as you at least specify its value. This way, you‘ll 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
Changing System Configuration WMIs primary purpose is to read information about the current system configuration but it can also be used to make changes to a system. Most WMI object properties are read-only, but some are writeable, too. In addition, a number of WMI objects contain methods that you can call to make changes. Note that WMI objects returned by PowerShell Remoting always are read-only. They cannot be used to change the remote system. If you want to change a remote system using WMI objects, you must connect to the remote system using the -ComputerName parameter provided by Get-WmiObject.
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
Three conditions must be met before you can modify a property:
The property must be writeable. Most properties are read-only. You require the proper permissions for modifications. The drive description applies to all users of a computer so only administrators may modify them. You must use Put() to save the modification. Without Put(), the modification will not be written back to the system.
Invoking WMI Methods WMI objects derived from the Win32_Process class have a Terminate() method you can use to terminate a process. Of course it is much easier to terminate a process with Stop-Process, so why would you use WMI? Because WMI supports remote connections. Stop-Process can only stop processes on your local machine. This line would kill all instances of the Windows Editor "notepad.exe" on your local machine: Get-WmiObject Win32_Process -Filter "name='notepad.exe'" | ForEach-Object { $_.Terminate().ReturnValue } Add the parameter -ComputerName to Get-WmiObject, and you'd be able to kill notepads on one or more remote machines - provided you have Administrator privileges on the remote machine. For every instance that Terminate() closes, it returns an object with a number of properties. Only the property ReturnValue is useful, though, because it tells you whether the call succeeded. That's why it is generally a good idea to add ".ReturnValue" to all calls of a WMI method. A return value of 0 generally indicates success, any other code failure. To find out what the error codes mean you would have to surf to an Internet search engine and enter the WMI class name (like "Win32_Process"). One of the first links will guide you to the Microsoft MSDN documentation page for that class. It lists all codes and clear text translations for all properties and method calls. If you already know the process ID of a process, you can work on the process directly just as you did in the last section because the process ID is the key property of processes. For example, you could terminate the process with the ID 1234 like this: ([wmi]"Win32_Process='1234'").Terminate()
If you‘d 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
Get-Member will tell you which methods a WMI object supports: PS> Get-WmiObject Win32_Process | Get-Member -MemberType Method TypeName: System.Management.ManagementObject#root\cimv2\Win32_Process Name MemberType Definition ------------- ---------AttachDebugger Method System.Management.ManagementBaseObject AttachDebugger() GetOwner Method System.Management.ManagementBaseObject GetOwner() GetOwnerSid Method System.Management.ManagementBaseObject GetOwnerSid() SetPriority Method System.Management.ManagementBaseObject SetPriority(System.Int32 Priority) Terminate Method System.Management.ManagementBaseObject Terminate(System.UInt32 Reason)
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 you‘d 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. It‘s best to limit your search to the Microsoft MSDN pages: Win32_NetworkAdapterConfiguration RenewDHCPLeaseAll site:msdn2.microsoft.com.
Using WMI Auto-Documentation Nearly every WMI class has a built-in description that explains its purpose. You can view this description only if you first set a hidden option called UseAmendedQualifiers to $true. Once that‘s done, the WMI class will readily supply information about its function: $class = [wmiclass]'Win32_LogicalDisk' $class.psbase.Options.UseAmendedQualifiers = $true ($class.psbase.qualifiers["description"]).Value The Win32_LogicalDisk class represents a data source that resolves to an actual local storage device on a Win32 system. The class returns both local as well as mapped logical disks. However, the recommended approach is to use this class for obtaining information on local disks and to use the Win32_MappedLogicalDisk for information on mapped logical disk.
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()
Using WMI Remotely
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.
Accessing WMI Objects on Another Computer Use the -ComputerName parameter of Get-WmiObject to access another computer system using WMI. Then specify the name of the computer after it: Get-WmiObject -ComputerName pc023 Win32_Process You can also specify a comma-separated list of a number of computers and return information from all of them. The parameter -ComputerName accepts an array of computer names. Anything that returns an array of computer names or IP addresses can be valid input. This line, for example, would read computer names from a file: Get-WmiObject Win32_Process -ComputerName (Get-Content c:\serverlist.txt) If you want to log on to the target system using another user account, use the –Credential parameter to specify additional log on data as in this example: $credential = Get-Credential Get-WmiObject -ComputerName pc023 -Credential $credential Win32_Process
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.
WMI Background Information WMI has a hierarchical structure much like a file system does. Up to now, all the classes that you have used have come from the WMI ―directory‖ root\cimv2. Third-party vendors can create additional WMI directories, known as Namespaces, and put in them their own classes, which you can use to control software, like Microsoft Office or hardware like switches and other equipment. Because the topmost directory in WMI is always named root, from its location you can inspect existing namespaces. Get a display first of the namespaces on this level: Get-WmiObject -Namespace root __Namespace | Format-Wide Name subscription DEFAULT MicrosoftDfs CIMV2 Cli nap SECURITY RSOP Infineon WMI
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
Converting the WMI Date Format WMI uses special date formats. For example, look at Win32_OperatingSystem objects: Get-WmiObject Win32_OperatingSystem | Format-List *time* CurrentTimeZone : 120 LastBootUpTime : 20111016085609.375199+120 LocalDateTime : 20111016153922.498000+120
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 isn‘t 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 won‘t 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.
Logging On Under Other User Names [ADSI] is a shortcut to the DirectoryServices.DirectoryEntry .NET type. That‘s why you could have set up the previous connection this way as well: $domain = [DirectoryServices.DirectoryEntry]"" $domain distinguishedName ----------------{DC=scriptinternals,DC=technet}
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 isn‘t 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 doesn‘t 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 isn‘t your log-on domain. More likely than not, you‘ll have to log on to the new domain with an identity known to it.
Logging onto a domain that isn‘t 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. That‘s 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 don‘t 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 won‘t cause any errors because when the connection fails, there isn‘t 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 {
Write-Host -Fore Green "Connection established." } Logon failure: unknown user name or bad password.
By the way, the error code -2147352570 means that although the connection was established, Bind() didn’t find an object to which it could bind itself. That‘s OK because you didn‘t 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
Listing Container Contents
At some point, you‘d 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} (...)
Accessing Individual Users or Groups There are various ways to access individual users or groups. For example, you can filter the contents of a container. You can also specifically select individual items from a container or access them directly through their LDAP path. And you can search for items across directories.
Using Filters and the Pipeline Children gets back fully structured objects that, as shown in Chapter 5, you can process further in the PowerShell pipeline. Among other things, if you want to list only users, not groups, you could query the sAMAccountType property and use it as a filter criterion: $ldap = "CN=Users" $domain = [ADSI]"" $dn = $domain.distinguishedName $users = [ADSI]"LDAP://$ldap,$dn" $users.PSBase.Children | Where-Object { $_.sAMAccountType -eq 805306368 }
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} (...)
Directly Accessing Elements If you know the ADSI path to a particular object, you don‘t have to resort to a circuitous approach but can access it directly through the pipeline filter. You can find the path of an object in the distinguishedName property: $users.PSBase.Children | Format-Table sAMAccountName, distinguishedName -wrap sAMAccountName distinguishedName -----------------------------{admin} {CN=admin,CN=Users,DC=scriptinternals,DC=technet} {Administrator} {CN=Administrator,CN=Users,DC=scriptinternals,DC=technet} {All} {CN=All,CN=Users,DC=scriptinternals,DC=technet} {ASPNET} {CN=ASPNET,CN=Users,DC=scriptinternals,DC=technet} {Belle} {CN=Belle,CN=Users,DC=scriptinternals,DC=technet} {consultation2} {CN=consultation2,CN=Users,DC=scriptinternals,DC=technet} {consultation3} {CN=consultation3,CN=Users,DC=scriptinternals,DC=technet} (...)
For example, if you want to access the Guest account directly, specify its distinguishedName. If you‘re a domain member, you don‘t 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.
Obtaining Elements from a Container You already know what to use to read out all the elements in a container: PSBase.Children. However, by using PSBase.Find() you can also retrieve individual elements from a container: $domain = [ADSI]"" $users = $domain.psbase.Children.Find("CN=Users") $useraccount = $users.psbase.Children.Find("CN=Administrator") $useraccount.Description Predefined account for managing the computer or domain.
Searching for Elements You‘ve had to know exactly where in the hierarchy of domain a particular element is stored to access it. In larger domains, it can be really difficult to relocate a particular user account or group. That‘s why a domain can be accessed and searched like a database.
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 haven‘t 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 they‘re 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 you‘re 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 pipeline—just 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
Table 19.1: Examples of LDAP queries
Accessing Elements Using GUID Elements in a domain are subject to change. The only thing that is really constant is the so-called GUID of an account. A GUID is assigned just one single time, namely when the object is created, after which it always remains the same. You can find out the GUID of an element by accessing the account. For example, use the practical GetLDAPUser function above: $searchuser = Get-LDAPUser "Guest" $useraccount = $searchuser.GetDirectoryEntry() $useraccount.psbase.NativeGUID f0ffa8b401ce5549b318c0a4641cdd4a
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 won‘t 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://" $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 = "" $acccount = new-object DirectoryServices.DirectoryEntry("LDAP://10.10.10.1/$guid","domain\user", ` "secret") distinguishedName ----------------{CN=Guest,CN=Users,DC=scriptinternals,DC=technet}
Reading and Modifying Properties In the last section, you learned how to access individual elements inside a domain: either directly through the ADSI path, the GUID, searching through directory contents, or launching a search across domains. The elements you get this way are full-fledged objects. You use the methods and properties of these elements to control them. Basically, everything applies that you read about in Chapter 6. In the case of ADSI, there are some additional special features:
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 won‘t 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.
Just What Properties Are There? There are theoretical and a practical approaches to establishing which properties any ADSI object contains.
Practical Approach: Look The practical approach is the simplest one: if you output the object to the console, PowerShell will convert all the properties it contains into text so that you not only see the properties, but also right away which values are assigned to the properties. In the following example, the user object is the result of an ADSI search, to be precise, of the above-mentioned Get-LDAPUser function: $useraccount = Get-LDAPUser Guest $useraccount | Format-List * Path : LDAP://10.10.10.1/CN=Guest,CN=Users,DC=scriptinternals,DC=technet Properties : {samaccounttype, lastlogon, objectsid, whencreated...}
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 you‘ll 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}
In addition, further properties are available in the underlying base object: $useraccount.PSBase | Format-List * AuthenticationType : Secure Children : {} Guid : b4a8fff0-ce01-4955-b318-c0a4641cdd4a ObjectSecurity : System.DirectoryServices.ActiveDirectorySecurity Name : CN=Guest NativeGuid : f0ffa8b401ce5549b318c0a4641cdd4a NativeObject : {} Parent : System.DirectoryServices.DirectoryEntry Password : Path : LDAP://10.10.10.1/CN=Guest,CN=Users,DC=scriptinternals,DC=technet Properties : {objectClass, cn, description, distinguishedName...} SchemaClassName : user SchemaEntry : System.DirectoryServices.DirectoryEntry UsePropertyCache : True Username : scriptinternals\Administrator Options : System.DirectoryServices.DirectoryEntryConfiguration Site : Container :
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).
Theoretical Approach: Much More Thorough The practical approach we just saw is quick and returns a lot of information, but it is also incomplete. PowerShell shows only those properties in the output that actually do include a value right then (even if it is an empty value). In reality, many more properties are available so the tool you need to list them is Get-Member: $useraccount | Get-Member -memberType *Property Name MemberType Definition ------------- ---------accountExpires Property System.DirectoryServices.PropertyValueCollection badPasswordTime Property System.DirectoryServices.PropertyValueCollection badPwdCount Property System.DirectoryServices.PropertyValueCollection cn Property System.DirectoryServices.PropertyValueCollection codePage Property System.DirectoryServices.PropertyValueCollection countryCode Property System.DirectoryServices.PropertyValueCollection description Property System.DirectoryServices.PropertyValueCollection
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 won‘t 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(
View more...
Comments