The file system has special importance within the PowerShell console. One obvious reason is that administrators perform many tasks that involve the file system. Another is that the file system is the prototype of a hierarchically structured information system. In coming chapters, you'll see that PowerShell controls other hierarchical information systems on this basis. You can easily apply what you have learned about drives, directories, and files in PowerShell to other areas, including the registry or Microsoft Exchange.
Topics Covered:
A number of cmdlets in Table 15.1 do the main work as they are rarely accessed under their real names. Aliases are much more useful and the aliases of cmdlets come from both the Windows and the UNIX worlds. This makes it easy for new learners to find the right cmdlets quickly.
| Alias |
Description |
Cmdlet |
| ac |
Adds the contents of a file |
Add-Content |
| cls, clear |
Clears the console window |
Clear-Host |
| cli |
Clears file of its contents, but not the file itself |
Clear-Item |
| copy, cp, cpi |
Copies file or directory |
Copy-Item |
| Dir, ls, gci |
Lists directory contents |
Get-Childitem |
| type, cat, gc |
Reads contents of text-based file |
Get-Content |
| gi |
Accesses specific file or directory |
Get-Item |
| gp |
Reads property of a file or directory |
Get-ItemProperty |
| ii |
Invokes file or directory using allocated Windows program |
Invoke-Item |
| - |
Joins two parts of a path into one path, for example, a drive and a file name |
Join-Path |
| mi, mv, move |
Moves files and directories |
Move-Item |
| ni |
Creates new file or new directory |
New-Item |
| ri, rm, rmdir, del, erase, rd |
Deletes empty directory or file |
Remove-Item |
| rni, ren |
Renames file or directory |
Rename-Item |
| rvpa |
Resolves relative path or path including wildcard characters |
Resolve-Path |
| sp |
Sets property of file or directory |
Set-ItemProperty |
| Cd, chdir, sl |
Changes to specified directory |
Set-Location |
| - |
Extracts a specific part of a path like the parent path, drive, or file name |
Split-Path |
| - |
Returns True if the specified path exists |
Test-Path |
Table 15.1: Overview of the most important file system commands
Accessing Files and Directories
Use Get-ChildItem to list the contents of a directory. The predefined aliases for this are Dir and ls. Get-ChildItem perform a number of important tasks:
- Making directory contents visible
- Searching through the file system recursively and finding certain files
- Getting files and directory objects
- Passing files to other cmdlets, functions, or scripts
Because Windows administrators use the alias Dir in practice, not the cmdlet Get-ChildItem by its name, Dir is used in the following examples. Dir can also be replaced by ls (UNIX) or Get-ChildItem in all examples.
Listing Directory Contents
In rudimentary cases, you may simply want to know which files are in a certain directory. If you don't specify another one, Dir lists the contents of the current directory. If you specify a directory after Dir, its contents will be listed. Also, if you use the -recurse parameter, Dir will list the contents of all subdirectories. Wildcard characters are also allowed.
For example, if you want to get a list of all PowerShell script files stored in the current directory, type this command:
Dir *.ps1
Dir even accepts arrays, which allow you to list different drives at the same time. The following instruction lists all the PowerShell scripts in the PowerShell root directory, as well as all log files in the Windows directory:
Dir $pshome\*.ps1, $env:windir\*.log
If you're interested only in the names of items in one directory, use the parameter -name. Dir will not retrieve objects (files and directories), but just their names in plain text.
Dir -name
Some characters have a special meaning in PowerShell, such as square brackets. Square brackets always designate array elements (see Chapter 4). That's why using file names can cause confusion. All special characters will be evaluated as path segments and won't be interpreted by PowerShell if you use the -literalPath parameter to specify file names.
Recursively Searching the Entire File System
Use the -recurse parameter if you want your search to include every subdirectory. However, note the failure of the following invocation:
Dir *.ps1 -recurse
You need to know a few more details about how -recurse works to understand why this happens,. Dir always retrieves directory contents as file and directory objects. If you set the -recurse switch, Dir will invoke directory objects recursively. Because you instructed Dir in the last example to retrieve only those files that had the .ps1 extension, Dir found no directories that -recurse could have stepped through. The concept may be hard to get used to at first, but it explains why in the following example you get a recursive directory listing, even when you use wildcards:
Dir $home\d* -recurse
Here, Dir retrieves all the items from your root directory that begin with the letter "D". The directories are searched recursively as well because directories are among them.
Filter and Exclusion Criterion
But let's return to our initial problem: how to get a recursive listing of all files of one type, such as PowerShell scripts. The answer is to instruct Dir to list the directory contents completely and to specify a filter additionally. Dir then filters the files you want out of all the files:
Dir $home -filter *.ps1 -recurse
In addition to -filter, there is a parameter that at first glance works in a very similar way: -include:
Dir $home -include *.ps1 -recurse
You'll see some dramatic speed differences: -filter is much quicker than -include.
(Measure-Command {Dir $home -filter *.ps1 -recurse}).TotalSeconds
4,6830099
(Measure-Command {Dir $home -include *.ps1 -recurse}).TotalSeconds
28,1017376
The reason is that -include supports regular expressions, which are fundamentally more complicated, while -filter only understands simple wildcard characters. That's why you could use -include to make even more complex filters than the following ones, which find all script files beginning with one of the letters from "A" to "F". That's beyond the capacity of -filter:
Dir $home -filter [a-f]*.ps1 -recurse
Dir $home -include [a-f]*.ps1 -recurse
Directory: Microsoft.PowerShell.Core\FileSystem::C:\Users\Tobias Weltner\Documents
Mode LastWriteTime Length Name
---- ------------- ------ ----
-a--- 28.09.2007 23:59 1442 finddouble3.ps1
Directory: Microsoft.PowerShell.Core\FileSystem::C:\Users\Tobias Weltner\Downloads\PowerShell
CX-24134\Branches\Developer\rlehrbaum\Src\Pscx\Profile
Mode LastWriteTime Length Name
---- ------------- ------ ----
-a--- 30.07.2007 08:40 6225 Cd.ps1
-a--- 30.07.2007 08:40 2083 Debug.ps1
-a--- 30.07.2007 08:40 1930 Dir.ps1
-a--- 30.07.2007 08:40 2279 Environment.ps1
-a--- 30.07.2007 08:40 2898 Environment.VisualStudio2005.ps1
-a--- 30.07.2007 08:40 1588 EyeCandy.Jachym.ps1
-a--- 30.07.2007 08:40 2096 EyeCandy.Keith.ps1
-a--- 30.07.2007 08:40 2254 EyeCandy.ps1
-a--- 30.07.2007 08:40 591 FileSystem.ps1
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 enables you to get a list of all image files in your profile:
Dir $home -recurse -include *.bmp,*.png,*.jpg, *.gif
Avoid just one thing: don't combine -filter and -include. Choose one of the two parameters. Specifically you should use -filter when you don't need any regular expressions or arrays because of its enormous speed advantage.
You can't use Dir to list files that have a certain size because with its filters, Dir can apply restrictions only at the level of file and directory names. If you want to filter results returned by Dir using other criteria, use Where-Object (Chapter 5).
The next example retrieves the biggest memory hogs in your user profile, specifically files that are at least 100 MB large:
Dir $home -recurse | Where-Object { $_.length -gt 100MB }
If you want to know just how many items Dir found, instruct Dir to retrieve its result as an array and set its Count property. The next instruction will tell you how many images are stored in your user profile (an operation that can take a long time):
@(Dir $home -recurse -include *.bmp,*.png,*.jpg, *.gif).Count
6386
Getting File and Directory Contents
You can use Dir to directly access individual files because Dir returns the contents of a directory in the form of file and directory objects. This enables you to obtain the FileInfo object of each file:
$file = Dir c:\autoexec.bat
$file | Format-List *
PSPath : Microsoft.PowerShell.Core\FileSystem::C:\autoexec.bat
PSParentPath : Microsoft.PowerShell.Core\FileSystem::C:\
PSChildName : autoexec.bat
PSDrive : C
PSProvider : Microsoft.PowerShell.Core\FileSystem
PSIsContainer : False
Mode : -a---
Name : autoexec.bat
Length : 24
DirectoryName : C:\: C:\
IsReadOnly : False
Exists : True
FullName : C:\autoexec.bat
Extension : .bat
CreationTime : 11.02.2006 11:23:09
CreationTimeUtc : 11.02.2006 10:23:09
LastAccessTime : 11.02.2006 11:23:09
LastAccessTimeUtc : 11.02.2006 10:23:09
LastWriteTime : 09.18.2006 23:43:36
LastWriteTimeUtc : 09.18.2006 21:43:36
Attributes : Archive
This is how you could read the properties of single files as well as modify them if their properties allow modification:
$file.Attributes
Archive
$file.Mode
-a---
Get-Item uses another approach to access the file object. All three commands return the same result, which is the file object of the specified file.
$file = Dir c:\autoexec.bat
$file = Get-Childitem c:\autoexec.bat
$file = Get-Item c:\autoexec.bat
However, Get-Childitem and Get-Item act very differently when accessing directories instead of files:
$directory = Dir c:\windows
$directory = Get-Childitem c:\windows
$directory
Directory: Microsoft.PowerShell.Core\FileSystem::C:\windows
Mode LastWriteTime Length Name
---- ------------- ------ ----
d---- 11.02.2006 13:35 addins
d---- 10.11.2007 03:18 AppPatch
d-r-s 08.31.2007 13:42 assembly
(...)
$directory = Get-Item c:\windows
$directory
Directory: Microsoft.PowerShell.Core\FileSystem::C:\
Mode LastWriteTime Length Name
---- ------------- ------ ----
d---- 11.10.2007 03:07 windows
$directory | Format-List *
PSPath : Microsoft.PowerShell.Core\FileSystem::C:\windows
PSParentPath : Microsoft.PowerShell.Core\FileSystem::C:\
PSChildName : windows
PSDrive : C
PSProvider : Microsoft.PowerShell.Core\FileSystem
PSIsContainer : True
Mode : d----
Name : windows
Parent :
Exists : True
Root : C:\
FullName : C:\windows
Extension :
CreationTime : 02.11.2006 12:18:34
CreationTimeUtc : 02.11.2006 11:18:34
LastAccessTime : 11.10.2007 03:07:30
LastAccessTimeUtc : 11.10.2007 01:07:30
LastWriteTime : 11.10.2007 03:07:30
LastWriteTimeUtc : 11.10.2007 01:07:30
Attributes : 65552
Passing Files to Cmdlets, Functions, or Scripts
Because Dir returns individual file and directory objects in its result, Dir can pass these objects directly to other cmdlets or to your own functions and scripts. This makes Dir an important selection command, which you can very conveniently use to recursively find all files having the type you're looking for on the entire hard disk drive or even on several drives.
To do so, process the result of Dir in the pipeline either by using Where-Object and then ForEach-Object (Chapter 5), or write your own pipeline filter (Chapter 9).
You can also combine the results of several separate Dir commands. In the following example, two separate Dir 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:
$list1 = Dir $env:windir\system32\*.dll
$list2 = Dir $env:programfiles -recurse -filter *.dll
$totallist = $list1 + $list2
$totallist | ForEach-Object {
$info = [system.diagnostics.fileversioninfo]::GetVersionInfo($_.FullName);
"{0,-30} {1,15} {2,-20}" -f $_.Name, `
$info.ProductVersion, $info.FileDescription
}
aaclient.dll 6.0.6000.16386 Anywhere access client
accessibilitycpl.dll 6.0.6000.16386 Ease of access control panel
acctres.dll 6.0.6000.16386 Microsoft Internet Account...
acledit.dll 6.0.6000.16386 Access Control List Editor
aclui.dll 6.0.6000.16386 Security Descriptor Editor
(...)
Because Dir retrieves directories as well as files, it can sometimes be important to limit the result of Dir only to files or only to directories. There are several ways to do this. You can either validate the attribute of the returned object, the PowerShell PSIsContainer property, or the object type:
Dir | Where-Object { $_ -is [System.IO.DirectoryInfo] }
Dir | Where-Object { $_.PSIsContainer }
Dir | Where-Object { $_.Mode.Substring(0,1) -eq "d" }
Dir | Where-Object { $_ -is [System.IO.FileInfo] }
Dir | Where-Object { $_.PSIsContainer -eq $false}
Dir | Where-Object { $_.Mode.Substring(0,1) -ne "d" }
The first variant (controlling object types) is the fastest by far while the latter (text comparison) is more complex and slower as a result of it complexity.
Where-Object can filter files according to other criteron as well.
For example, use the following pipeline filter if you'd like to locate only files that were created after May 12, 2007:
Dir | Where-Object { $_.CreationTime -gt [datetime]::Parse("May 12, 2007") }
You can use relative data if all you want to see are files that have been changed in the last two weeks:
Dir | Where-Object { $_.CreationTime -gt (Get-Date).AddDays(-14) }
Navigating the File System
Unless you changed your prompt in the way described in Chapter 9, the current directory in which you are working inside the PowerShell console is named at the command line prompt. You can find out what the current directory is by using Get-Location:
Get-Location
Path
----
C:\Users\Tobias Weltner\Sources
If you want to navigate to another location in the file system, use Set-Location or the Cd alias:
Cd ..
Cd \
Cd c:\windows
Cd $env:windir
Cd $home
Relative and Absolute Paths
Path specifications can be either relative or absolute. In the last example you used both types.
Relative path specifications depend on the current directory, and the .\test.txt specification always refers to the test.txt file in the current directory while ..\test.txt refers to the test.txt file in the parent directory. Relative path specifications are useful, for example, if 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 |
Example |
Result |
| . |
Current directory |
ii . |
Opens the current directory in Windows Explorer |
| .. |
Parent directory |
Cd .. |
Changes to the parent directory |
| \ |
Root directory |
Cd \ |
Changes to the topmost directory of a drive |
| ~ |
Home directory |
Cd ~ |
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 invoke a file or a command using relative paths. You can resolve them yourself by using Resolve-Path.
Resolve-Path .\test.txt
Path
----
C:\Users\Tobias Weltner\test.txt
Resolve-Path, however, only works for files that actually exist. If there is no file in your current directory that's called test.txt, Resolve-Path will report an error.
Resolve-Path can have more than one result if the path that you specify includes wildcard characters. The following invocation will retrieve the names of all the ps1xml files in the PowerShell home directory:
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
Like Dir, Resolve-Path can act as a selection filter for a downstream function. The following example shows opening a file in Notepad for processing. The command calls Notepad to open a file by using Resolve-Path.
Figure 15.1: Using Resolve-Path to select several files and opening them by querying
If there are no files at all that conform to the criterion, Resolve-Path will throw an error, which will be noted in the $? variable (Chapter 11). The expression !$? is always satisfied when an error occurs, and in such a case the function reports that no file was found.
The result is an array if Resolve-Path finds more than one file. In this case, the function lists the files that were found so not too many files will be unexpectedly opened in the event of a faulty entry. The function uses the internal PowerShell function PromptForChoice() that we saw in Chapter 6 to request the user for confirmation.
The Call operator we saw in Chapter 12 launches the file(s). Of course, this will only work if an application is allocated to the respective file type.
function edit-file([string]$path=$(Throw "Specify a relative path!"))
{
$files = Resolve-Path $path -ea SilentlyContinue
if (!$?)
{
"No file met your criterion."; break
}
if ($files -is [array])
{
Write-Host -foregroundColor "Red" -backgroundColor "White" `
"Do you want to open these files?"
foreach ($file in $files)
{
"- " + $file.Path
}
$yes = ([System.Management.Automation.Host.ChoiceDescription]"&yes")
$no = ([System.Management.Automation.Host.ChoiceDescription]"&no")
$choices = [System.Management.Automation.Host.ChoiceDescription[]]($yes,$no)
$result = $host.ui.PromptForChoice('Open files','Open these files?',$choices,1)
if ($result -eq 0)
{
foreach ($file in $files)
{
& $file
}
}
}
else
{
& $files
}
}
Saving Directory Locations
The current directory where you are working can be "pushed" to the top of a list of locations, called 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 leave your current directory, first type Push-Location to store your current location. Then, you can complete your task and when ready, use Pop-Location to retrieve your stored location.
Cd $home will always take you back to your home directory. Moreover, 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. Push-Location -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.
Finding Special Directories
Windows uses a number of special directories which, depending on installation, may be found at different locations. The paths of the most important directories are located in the Windows environment variables so that PowerShell can clearly allocate these special directories. You can find many other special directories through the Environment class of the .NET framework.
| Special directory |
Description |
Access |
| Application data |
Application data locally stored on the machine |
$env:localappdata |
| User profile |
User directory |
$env:userprofile |
| Data used in common |
Directory for data used by all programs |
$env:commonprogramfiles |
| Public directory |
Common directory of all local users |
$env:public |
| Program directory |
Directory in which programs are installed |
$env:programfiles |
| Roaming Profiles |
Application data for roaming profiles |
$env:appdata |
| Temporary files (private) |
Directory for temporary files of the user |
$env:tmp |
| Temporary files |
Directory for temporary files |
$env:temp |
| Windows directory |
Directory in which Windows is installed |
$env:windir |
Table 15.3: Important Windows directories that are stored in environment variables
Environment variables return only a few, and by far not all, of the paths of special directories. For example, if you'd like to put a file directly on a user's Desktop, you'll need the path to the Desktop that the environment variables can't retrieve for you. However, The GetFolderPath() method of the environment class of the .NET framework (Chapter 6) can do that. The following shows how you could put a link on the Desktop.
[Environment]::GetFolderPath("Desktop")
C:\Users\Tobias Weltner\Desktop
$path = [Environment]::GetFolderPath("Desktop") + "\EditorStart.lnk"
$comobject = New-Object -comObject WScript.Shell
$link = $comobject.CreateShortcut($path)
$link.targetpath = "notepad.exe"
$link.IconLocation = "notepad.exe,0"
$link.Save()
The types of directories that GetFolderPath() can find are noted in the SpecialFolder enumeration. You should use the following line to view its contents:
[System.Environment+SpecialFolder] | Get-Member -static -memberType Property
TypeName: System.Environment+SpecialFolder
Name MemberType Definition
---- ---------- ----------
ApplicationData Property static System.Environment+SpecialFolder ApplicationData {get;}
CommonApplicationData Property static System.Environment+SpecialFolder CommonApplicationData ...
CommonProgramFiles Property static System.Environment+SpecialFolder CommonProgramFiles {get;}
Cookies Property static System.Environment+SpecialFolder Cookies {get;}
Desktop Property static System.Environment+SpecialFolder Desktop {get;}
DesktopDirectory Property static System.Environment+SpecialFolder DesktopDirectory {get;}
Favorites Property static System.Environment+SpecialFolder Favorites {get;}
History Property static System.Environment+SpecialFolder History {get;}
InternetCache Property static System.Environment+SpecialFolder InternetCache {get;}
LocalApplicationData Property static System.Environment+SpecialFolder LocalApplicationData {...
MyComputer Property static System.Environment+SpecialFolder MyComputer {get;}
MyDocuments Property static System.Environment+SpecialFolder MyDocuments {get;}
MyMusic Property static System.Environment+SpecialFolder MyMusic {get;}
MyPictures Property static System.Environment+SpecialFolder MyPictures {get;}
Personal Property static System.Environment+SpecialFolder Personal {get;}
ProgramFiles Property static System.Environment+SpecialFolder ProgramFiles {get;}
Programs Property static System.Environment+SpecialFolder Programs {get;}
Recent Property static System.Environment+SpecialFolder Recent {get;}
SendTo Property static System.Environment+SpecialFolder SendTo {get;}
StartMenu Property static System.Environment+SpecialFolder StartMenu {get;}
Startup Property static System.Environment+SpecialFolder Startup {get;}
System Property static System.Environment+SpecialFolder System {get;}
Templates Property static System.Environment+SpecialFolder Templates {get;}
If you want an overview of all the directories that GetFolderPath() can locate, you can retrieve that as follows:
[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
Constructing Paths
Path names consist of text so you can construct them however you like. You saw above how you can construct the path of a file that is intended to be on a user's Desktop:
$path = [Environment]::GetFolderPath("Desktop") + "\file.txt"
$path
C:\Users\Tobias Weltner\Desktop\file.txt
Be absolutely sure that you put the right number of backward slashes in your path. That's why a backward slash was specified in front of the file name in the last example. A more reliable way would be to put together paths using Join-Path or the methods of the Path .NET class:
$path = Join-Path ([Environment]::GetFolderPath("Desktop")) "test.txt"
$path
C:\Users\Tobias Weltner\Desktop\test.txt
$path = [System.IO.Path]::Combine([Environment]::`
GetFolderPath("Desktop"), "test.txt")
$path
C:\Users\Tobias Weltner\Desktop\test.txt
The Path class includes a number of additionally useful methods that you can use to put together paths or extract information from paths. Just insert [System.IO.Path]:: in front of the methods listed in Table 15.4, for example:
[System.IO.Path]::ChangeExtension("test.txt", "ps1")
test.ps1
| Method |
Description |
Example |
| ChangeExtension() |
Changes the file extension |
ChangeExtension("test.txt", "ps1") |
| Combine() |
Combines path strings; corresponds to Join-Path |
Combine("C:\test", "test.txt") |
| GetDirectoryName() |
Returns the directory; corresponds to Split-Path -parent |
GetDirectoryName("c:\test\file.txt") |
| GetExtension() |
Returns the file extension |
GetExtension("c:\test\file.txt") |
| GetFileName() |
Returns the file name; corresponds to Split-Path -leaf |
GetFileName("c:\test\file.txt") |
| GetFileNameWithoutExtension() |
Returns the file name without the file extension |
GetFileNameWithoutExtension("c:\test\file.txt") |
| GetFullPath() |
Returns the absolute path |
GetFullPath(".\test.txt") |
| GetInvalidFileNameChars() |
Lists all characters that are not allowed in a file name |
GetInvalidFileNameChars() |
| GetInvalidPathChars() |
Lists all characters that are not allowed in a path |
GetInvalidPathChars() |
| GetPathRoot() |
Gets the root directory; corresponds to Split-Path -qualifier |
GetPathRoot("c:\test\file.txt") |
| GetRandomFileName() |
Returns a random file name |
GetRandomFileName() |
| GetTempFileName() |
Returns a temporary file name in the Temp directory |
GetTempFileName() |
| GetTempPath() |
Returns the path of the directory for temporary files |
GetTempPath() |
| HasExtension() |
True, if the path includes a file extension |
HasExtension("c:\test\file.txt") |
| IsPathRooted() |
True, if the path is absolute; corresponds to Split-Path -isAbsolute |
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. You can also create your own 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 -type parameter the Directory value:
md Test1
Directory: Microsoft.PowerShell.Core\FileSystem::C:\users\Tobias Weltner
Mode LastWriteTime Length Name
---- ------------- ------ ----
d---- 12.10.2007 17:14 Test1
New-Item Test2 -type Directory
Directory: Microsoft.PowerShell.Core\FileSystem::C:\users\Tobias Weltner
Mode LastWriteTime Length Name
---- ------------- ------ ----
d---- 12.10.2007 17:14 Test2
You can also create several subdirectories in one step as PowerShell automatically creates all the directories that don't exist yet in the specified path:
md test\subdirectory\somethingelse
Three subdirectories will be created as long as the directories Test and Subdirectory are not in the current directory.
Creating New Files
You could also use New-Item to create new files, but they would be completely empty:
New-Item "new file.txt" -type File
Directory: Microsoft.PowerShell.Core\FileSystem::C:\users\Tobias Weltner
Mode LastWriteTime Length Name
---- ------------- ------ ----
-a--- 10.12.2007 17:16 0 new file.txt
Files are usually automatically created when you save data results because empty files are not particularly useful,. Redirection and the cmdlets Out-File and Set-Content can help you:
Dir > info1.txt
.\info1.txt
Dir | Out-File info2.txt
.\info2.txt
Dir | Set-Content info3.txt
.\info3.txt
Set-Content info4.txt (Get-Date)
.\info4.txt
As it turns out, redirection and Out-File are very similar in operation: when PowerShell converts pipeline results, file contents look just like they would if you output the information in the console. Set-Content works differently as it only returns directory listing names because when you use Set-Content PowerShell doesn't turn objects into text automatically. Instead, Set-Content takes out a standard property from an object. In this case, the property is Name.
Normally, you would use Set-Content to write any text to a file. This last line shows how you could write a date to a file. For example, if you manually convert the pipeline result by using ConvertTo-HTML, Out-File and Set-Content will behave alike.
Dir | ConvertTo-HTML | Out-File report1.htm
.\report1.htm
Dir | ConvertTo-HTML | Set-Content report2.htm
.\report2.htm
If you want to determine which object properties are displayed on a HTML page, use Select-Object, which was discussed in Chapter 5, to filter out the properties before conversion into HTML:
Dir | Select-Object name, length, LastWriteTime |
ConvertTo-HTML | Out-File report.htm
.\report.htm
During redirection, the encoding of the console is used automatically to specify how special characters will be displayed in text. You can manually set encoding for Out-File.by using the -encoding parameter.
If you prefer to export the result as a comma-separated list, use the Export-Csv cmdlet instead of Out-File.
You can use either double redirection or Add-Content if you want to attach information to a text file:
Set-Content info.txt "First line"
"Second line" >> info.txt
Add-Content info.txt "Third line"
Get-Content info.txt
First Line
S e c o n d L i n e
Third line
The result may surprise you: the double redirection arrow worked, but text was displayed in spaced characters. The redirection operations basically use the console encoding , and this can lead to unexpected results if you happen to mix together the ANSI and Unicode character sets. Instead, use the cmdlets Set-Content, Add-Content, and Out-File without the redirections to avoid this risk. All three commands support the -encoding parameter, which you can use to select a character set.
Creating New Drives
It may surprise you that PowerShell allows you to create new drives. You're not limited to network drives only. You can also use drives as convenient shortcuts to other important locations in your file system and even beyond your file system.
Use New-PSDrive to create new drives. To set up a network drive, proceed as follows:
New-PSDrive -name network -psProvider FileSystem -root \\127.0.0.1\c$
Name Provider Root
---- -------- ----
network FileSystem \\127.0.0.1\c$
You can now access the network drive through the new virtual drive network: like this:
Dir network:
It's also easy to create convenient shortcuts in working locations. The next lines create the drives desktop: and docs:, which represent your Desktop and the Windows folder "My Documents":
New-PSDrive desktop FileSystem `
([Environment]::GetFolderPath("Desktop")) | out-null
New-PSDrive docs FileSystem `
([Environment]::GetFolderPath("MyDocuments")) | out-null
If you want to change to your Desktop later on, type:
Cd desktop:
Use Remove-PSDrive to remove a virtual drive you have created. You can't remove the drive if the drive is in use. Note that drive letters for New-PSDrive and Remove-PSDrive are specified without colons. On the other hand, when working with drives using customary file system commands, you do have to specify a colon.
Remove-PSDrive desktop
Reading the Contents of Text Files
Use Get-Content to retrieve the contents of a text-based file:
Get-Content $env:windir\windowsupdate.log
There is a shortcut that uses variable notation if you know the absolute path of the file:
${c:\windows\windowsupdate.log}
However, this notation usually isn't very practical because it doesn't allow any variables inside braces. In most cases, the file path can't be accessed through the same absolute path on all computer systems.
Get-Content reads the contents of a file line by line and passes on every line of text through the pipeline. So, you should add Select-Object if you wanted to read only the first 10 lines of a very long file:
Get-Content $env:windir\windowsupdate.log | Select-Object -first 10
Use Select-String to filter out the information you want from text files. The next line gets only those lines from the windowsupdate.log file that contain the phrase "added update":
Get-Content $env:windir\windowsupdate.log | Select-String "Added update"
Processing Comma-Separated Lists
You should use Import-Csv if you want to process information from comma-separated lists in PowerShell. For test purposes, first create a comma-separated list:
Set-Content user.txt "Username,Function,Passwordage"
Add-Content user.txt "Tobias,Normal,10"
Add-Content user.txt "Martina,Normal,15"
Add-Content user.txt "Cofi,Administrator,-1"
Get-Content user.txt
Username,Function,Passwordage
Tobias,Normal,10
Martina,Normal,15
Cofi,Administrator,-1
Now, use Import-Csv to input this comma-separated list:
Import-Csv user.txt
Username Function Passwordage
------------ ----- -------------
Tobias Normal 10
Martina Normal 15
Cofi Administrator -1
As you see, Import-Csv understands the comma format and displays the data column by column. Save yourself the substantial effort usually involved in parsing a comma-separated value file: Import-Csv will do it for you. The first line is read as a column heading. You could then conveniently use the data in the comma-separated value as an input, such as to create user accounts.
Import-Csv user.txt | ForEach-Object { $_.Username }
Tobias
Martina
Cofi
Instead of a ForEach-Object loop, you can use a scriptblock in braces. The scriptblock is invoked inside the pipeline for every pipeline object and must be bound to a cmdlet parameter. In the following example, every user name in a comma-separated file is returned by the parameter -InputObject to echo and output.
Import-Csv user.txt | echo -InputObject {$_.Username }
Parsing Text Contents and Extracting Information
One frequent task is parsing raw data, such as log files , to get a structured readout of all the data. An example of such a log file is the windowsupdate.log file, which keeps a record of all Windows updates details (and which was misused as a guinea pig in previous examples). This file contains numerous data that at first glance seems scarcely readable. Initial analysis shows that the file stores information line by line and separates each piece of data by tab characters.
Regular expressions offer the easiest way to describe such a text format, which you already used in Chapter 13. You could use regular expressions to correctly describe the windowsupdate.log file contents as follows:
$pattern = "(.*)\t(.*)\t(.*)\t(.*)\t(.*)\t(.*)" (Enter)
$text = Get-Content $env:windir\windowsupdate.log (Enter)
$text[20] -match $pattern
True
$matches
Name Value
---- -----
6 * Added update {C14637DF-43D9-4201-9C0F-615D43943635}.101 to search result
5 Agent
4 2400
3 1248
2 09:18:02:087
1 2007-05-19
0 2007-05-19 09:18:02:087 1248 2400 Agent * Added update...
$matches returns a hit here for every expression in parentheses so you could address each text array on every line through the index numbers. For example, if you are only interested in the date and the description in a line y, then format it as follows:
"On {0} this took place: {1}" -f $matches[1], $matches[6]
On 2007-05-19 this took place:
* Added update {C14637DF-43D9-4201-9C0F-615D43943635}.101
to search result
Here, it is recommended that you give every subexpression its own name, which you can use later to query the result:
$pattern = "(?<Datum>.*)\t(?<time>.*)\t(?<Code1>.*)" + `
"\t(?<Code2>.*)\t(?<Program>.*)\t(?<Text>.*)"
$text = Get-Content $env:windir\windowsupdate.log
$text[20] -match $pattern
True
$matches.time + $matches.text
09:18:02:087 * Added update {C14637DF-43D9-4201
-9C0F-615D43943635}.101 to search result
You could now read in the entire log file, line by line, by using Get-Content, and then parse every line just the way it was above. This means that you could collect all the information you need, even from a gigantic log file, quickly and relatively efficiently. The next example does exactly that by listing only those lines in whose description is the phrase "woken up". This helps you find out whether a computer was woken up from the standby or sleep mode by automatic updates:
Get-Content $env:windir\windowsupdate.log |
ForEach-Object { if ($_ -match "woken up") { $_ } }
2007-05-24 03:00:34:609 1276 1490 AU The machine was woken up by
Windows Update
2007-05-24 03:00:34:609 1276 1490 AU The system was woken up by
Windows Update, but found to be
running on battery power. Skip
the forcedinstall.
2007-06-28 03:00:11:563 1272 fe0 AU The machine was woken up by
Windows Update
If the loop is successful, it will output the entire line that was stored in $_. You now know how you could use a further regular expression to split up this line into arrays to output from it only certain pieces of information.
However, there is a second, and a much more sophisticated, way to select individual text lines of the file: Switch. Merely tell this statement which file you want to examine and how the pattern looks that you're looking for. Switch will do the rest. The next statement gets all log entries showing installed automatic updates, and it does so considerably faster than if you had used Get-Content and ForEach-Object. Just remember that in regular expressions ".*" can represent any number of any characters.
Switch -regex -file $env:windir\wu1.log {
'START.*Agent: Install.*AutomaticUpdates' { $_ }}
2007-05-19 09:22:04:113 1248 1d0c Agent **START**
Agent: Installing updates [CallerId = AutomaticUpdates]
2007-05-24 22:31:51:046 1276 c38 Agent **START**
Agent: Installing updates [CallerId = AutomaticUpdates]
2007-06-13 12:05:44:366 1252 228c Agent **START**
Agent: Installing updates [CallerId = AutomaticUpdates]
(...)
Just substitute "SMS" or "Defender" for "automatic updates" in your regular expression if you'd like to find out when other updating programs, such as SMS or Defender, have installed updates. In fact, Switch can look for more than one pattern, and depending on the pattern it finds, carry out the instructions in the braces that follow it. This means you need only a few lines of code to find out how many updates you received and from which service:
result = @{Defender=0; AutoUpdate=0; SMS=0}
Switch -regex -file $env:windir\wu1.log
{
'START.*Agent: Install.*Defender' { $result.Defender += 1 };
'START.*Agent: Install.*AutomaticUpdates' { $result.AutoUpdate +=1 };
'START.*Agent: Install.*SMS' { $result.SMS += 1}
}
$result
Name Value
---- -----
SMS 0
Defender 1
AutoUpdate 8
Reading Binary Contents
Not all files contain text. Sometimes, it's necessary to read information from binary files. Normally, the visible file extension of a file plays the greatest role because it determines which program Windows uses to open the file. However, in many binary files, a header is also tightly integrated with the file. The header includes an internal type designation that provides information about what sort of file it is. Get-Content can obtain these "magic bytes" with the help of the parameters -readCount and -totalCount. The parameter -readCount indicates how many bytes are read in one step; -totalCount determines the number of bytes that you want to read from the file. In this case, the bytes you're looking for are the first four bytes of the file:
function Get-MagicNumber ($path)
{
Resolve-Path $path | ForEach-Object {
$magicnumber = Get-Content -encoding byte $_ -read 4 -total 4
$hex1 = ("{0:x}" -f ($magicnumber[0] * `
256 + $magicnumber[1])).PadLeft(4, "0")
$hex2 = ("{0:x}" -f ($magicnumber[2] * `
256 + $magicnumber[3])).PadLeft(4, "0")
[string] $chars = $magicnumber| %{ if ([char]::IsLetterOrDigit($_))
{ [char] $_ } else { "." }}
"{0} {1} '{2}'" -f $hex1, $hex2, $chars
}
}
Get-MagicNumber "$env:windir\explorer.exe"
4d5a 9000 'M Z . .'
The first four bytes of the Explorer are 4d, 5a, 90, and 00—or are given as the text MZ. Those are the initials of Mark Zbikowski, one of the developers of Microsoft DOS. The tag MZ represents executable programs. The tag looks different for graphics:
Get-MagicNumber "$env:windir\web\wallpaper\*.*"
ffd8 ffe0 'ÿ Ø ÿ à'
ffd8 ffe0 'ÿ Ø ÿ à'
ffd8 ffe0 'ÿ Ø ÿ à'
ffd8 ffe0 'ÿ Ø ÿ à'
ffd8 ffe0 'ÿ Ø ÿ à'
ffd8 ffe0 'ÿ Ø ÿ à'
(...)
You've seen that Get-Content can also read binary files, one byte at a time. Specify in the -readCount parameter how many bytes should be read for each step. -totalCount determines the total number of bytes that you want to read. If you assign the parameter -1, the file will be read to the end. You can assemble a little viewer for yourself that outputs data in hexadecimal form because binary data doesn't particularly look good as text:
function Get-HexDump($path,$width=10, $bytes=-1)
{
$OFS=""
Get-Content -encoding byte $path -readCount $width `
-totalCount $bytes | ForEach-Object {
$characters = $_
if (($characters -eq 0).count -ne $width)
{
$hex = $characters | ForEach-Object {
" " + ("{0:x}" -f $_).PadLeft(2,"0")}
$char = $characters | ForEach-Object {
if ([char]::IsLetterOrDigit($_))
{ [char] $_ } else { "." }}
"$hex $char"
}
}
}
Get-HexDump $env:windir\explorer.exe -width 15 -bytes 150
4d 5a 90 00 03 00 00 00 04 00 00 00 ff ff 00 MZ..........ÿÿ.
00 b8 00 00 00 00 00 00 00 40 00 00 00 00 00 ...............
d8 00 00 00 0e 1f ba 0e 00 b4 09 Cd 21 b8 01 Ø.....º....Í...
4c Cd 21 54 68 69 73 20 70 72 6f 67 72 61 6d LÍ.This.program
20 63 61 6e 6e 6f 74 20 62 65 20 72 75 6e 20 .cannot.be.run.
69 6e 20 44 4f 53 20 6d 6f 64 65 2e 0d 0d 0a in.DOS.mode....
24 00 00 00 00 00 00 00 ec 53 20 a3 a8 32 4e ........ìS...2N
f0 a8 32 4e f0 a8 32 4e f0 8f f4 33 f0 ae 32 ð.2Nð.2Nð.ô3ð.2
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 statement copies all PowerShell scripts from your home directory to the Desktop:
Copy-Item $home\*.ps1 ([Environment]::GetFolderPath("Desktop"))
However, only those scripts were copied that are directly available in the $home directory. While Copy-Item is familiar with the -recurse parameter, the parameter, similar to Dir, won't work if your initial path no longer contains any directories.
Copy-Item -recurse $home\*.ps1 ([Environment]::GetFolderPath("Desktop"))
Use Dir to copy all the PowerShell scripts to your Desktop anyway. Let it find the PowerShell scripts for you, and then pass the result on to Copy-Item:
Dir -filter *.ps1 -recurse | ForEach-Object {
Copy-Item $_.FullName ([Environment]::GetFolderPath("Desktop")) }
You might be tempted to reduce this line because every file object has an integrated CopyTo() method:
Dir -filter *.ps1 -recurse | ForEach-Object {
$_.CopyTo([Environment]::GetFolderPath("Desktop")) }
But the result would be an error. CopyTo() is a low-level function and needs the destination path for the file that is to be copied. Because you just want to copy all the files to the Desktop, you specified the path of the destination directory. CopyTo() will try to copy the file under precisely this name, which naturally cannot succeed because the Desktop already exists as a directory. Copy-Item is smarter: the file will be copied to this directory if the destination is a directory.
Because by now your Desktop is probably teeming with PowerShell scripts, it would be better to store them in their own subdirectory. You should create a new subdirectory on the Desktop, and move all your PowerShell scripts on the Desktop into this subdirectory:
$desktop = [Environment]::GetFolderPath("Desktop")
md ($desktop + "\PS Scripts")
Move-Item ($desktop + "\*.ps1") ($desktop + "\PS Scripts")
Your Desktop is now tidy again, and all your scripts are safely stored in a common directory on your Desktop.
Renaming Files and Directories
Use Rename-Item if you want to give a file or a directory another name. But be careful when you do this because Windows could be ruined if you rename system directories or files. Even if you rename the file extensions of files, you may not be able to open these files and display them properly any more.
Set-Content testfile.txt "Hello,this,is,an,enumeration"
.\testfile.txt
Rename-Item testfile.txt testfile.csv
.\testfile.csv
Numerous Renames
Because Rename-Item can be used as a building block in the pipeline, it provides surprisingly simple solutions to complex tasks. For example, if you want to remove the term "x86" from a directory and all its subdirectories, as well as all the included files, this instruction will suffice:
Dir | ForEach-Object {
Rename-Item $_.Name $_.Name.replace("-x86", "") }
However, this command will now actually attempt to rename all the files and directories, even if the term you're looking for isn't even in the file name. That generates errors and is very time-consuming. To greatly speed things up, sort out in advance all the files and directories that are in question by using Where-Object, which can increase speed by a factor of 50:
Dir | Where-Object { $_.Name -contains "-x86" } | ForEach-Object {
Rename-Item $_.Name $_.Name.replace("-x86", "") }
Changing File Extensions
If you want to change the file extension, be aware first of the consequences: the file will subsequently be recognized as another file type, and possibly be opened by the wrong application program or perhaps not be able to be opened by any application at all. The next instruction renames all PowerShell scripts in the current directory and changes the file extension from ".ps1" to ".bak".
Dir *.ps1 | ForEach-Object { Rename-Item $_.Name `
([System.IO.Path]::GetFileNameWithoutExtension($_.FullName) + `
".bak") -whatIf }
What if: Performing operation "Rename file" on Target
"Element: C:\Users\Tobias Weltner\tabexpansion.ps1
Destination: C:\Users\Tobias Weltner\tabexpansion.bak".
Because of the -whatIf parameter, initially the statement only indicates which renaming operation you could carry out.
Sorting Out File Names
Data collections often grow over time. If you want to sort out a directory, you could give all the files it contains uniform names and sequential numbers, or you could synthesize file names from some specific properties of files. Remember that PowerShell script folder that we just created on your Desktop? Let's properly number the PowerShell scripts in the folder in sequence:
$directory = [Environment]::GetFolderPath("Desktop") + "\PS Scripts"
Dir $directory\*.ps1
Directory: Microsoft.PowerShell.Core\FileSystem::C:\Users\Tobias Weltner\Desktop\PS Scripts
Mode LastWriteTime Length Name
---- ------------- ------ ----
-a--- 02.08.2007 17:21 46 a.ps1
-a--- 02.08.2007 17:32 146 b.ps1
-a--- 20.06.2007 16:41 766 clearhost.ps1
-a--- 20.06.2007 14:47 768 clearhost2.PS1
-a--- 02.08.2007 18:51 46 d.PS1
-a--- 18.06.2007 13:32 869 findCommandName.ps1
-a--- 27.04.2007 23:39 200 getdlls.ps1
-a--- 10.05.2007 14:53 1138 installfont.ps1
-a--- 02.08.2007 18:53 15 k.PS1
-a--- 27.04.2007 13:19 264 myinvoke.ps1
-a--- 20.06.2007 12:08 27 junk.PS1
-a--- 21.06.2007 08:15 2742 prereqs.ps1
-a--- 27.06.2007 14:11 495 profile.ps1
-a--- 26.04.2007 21:59 250 progress.ps1
-a--- 15.06.2007 15:44 4366 tabexpansion.ps1
-a--- 08.06.2007 12:56 176 test - Copy (2).ps1
-a--- 08.06.2007 12:56 176 test - Copy (3).ps1
-a--- 08.06.2007 12:56 176 test - Copy (4).ps1
-a--- 08.06.2007 12:56 176 test - Copy (5).ps1
-a--- 08.06.2007 12:56 176 test - Copy.ps1
-a--- 08.06.2007 12:56 176 test.ps1
-a--- 27.04.2007 20:42 106 test2.ps1
-a--- 20.06.2007 14:42 766 Untitled.ps1
Dir $directory\*.ps1 | ForEach-Object {$x=0} {
Rename-Item $_ ("Script " + $x + ".ps1"); $x++ } {"Finished!"}
Dir $directory\*.ps1
Directory: Microsoft.PowerShell.Core\FileSystem::C:\Users\Tobias Weltner\Desktop\PS Scripts
Mode LastWriteTime Length Name
---- ------------- ------ ----
-a--- 08.02.2007 17:21 46 Script 0.ps1
-a--- 08.02.2007 17:32 146 Script 1.ps1
-a--- 06.08.2007 12:56 176 Script 10.ps1
-a--- 06.08.2007 12:56 176 Script 11.ps1
-a--- 06.20.2007 16:41 766 Script 12.ps1
-a--- 06.08.2007 12:56 176 Script 13.ps1
-a--- 04.27.2007 20:42 106 Script 14.ps1
-a--- 06.20.2007 14:42 766 Script 15.ps1
-a--- 06.20.2007 14:47 768 Script 16.ps1
-a--- 08.02.2007 18:51 46 Script 17.ps1
-a--- 06.18.2007 13:32 869 Script 18.ps1
-a--- 04.27.2007 23:39 200 Script 19.ps1
-a--- 06.20.2007 12:08 27 Script 2.ps1
-a--- 05.10.2007 14:53 1138 Script 20.ps1
-a--- 02.08.2007 18:53 15 Script 21.ps1
-a--- 04.27.2007 13:19 264 Script 22.ps1
-a--- 06.21.2007 08:15 2742 Script 3.ps1
-a--- 06.27.2007 14:11 495 Script 4.ps1
-a--- 04.26.2007 21:59 250 Script 5.ps1
-a--- 06.15.2007 15:44 4366 Script .ps1
-a--- 08.06.2007 12:56 176 Script 7.ps1
-a--- 08.06.2007 12:56 176 Script 8.ps1
-a--- 08.06.2007 12:56 176 Script 9.ps1
Deleting Files and Directories
Use Remove-Item or the Del alias to remove files and directories, which deletes files and directories irrevocably. If a file is write-protected, you'll have to specify the -force parameter.
$file = New-Item testfile.txt -type file
$file.isReadOnly
False
$file.isReadOnly = $true
$file.isReadOnly
True
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 <<<< testfile.txt
del testfile.txt -force
Deleting Directory Contents
Use wildcard characters if all you want to do is to delete the contents of a directory, but still keep the directory. The following line, for example, will delete the contents of the Recent directory, which corresponds to "My Recent Documents" on the start menu. Because deleting files and directories is not something to be taken lightly and can have serious consequences, you can just simulate their deletion first by using -whatIf to see what happens:
$recents = [Environment]::GetFolderPath("Recent")
del $recents\*.* -whatIf
If you are convinced that your command is correct, and that it will delete the correct files, repeat the statement without -whatIf. On the other hand, if you're still unsure, you can also use -confirm, which makes every deletion contingent on your approval.
Deleting Directories and Their Contents
If a directory is deleted, its entire contents will be lost. PowerShell requests confirmation whenever you attempt to erase a directory along with its contents to prevent you from unintentionally destroying large quantities of data. Only the deletion of empty directories does not require confirmation:
md testdirectory
Directory: Microsoft.PowerShell.Core\FileSystem::C:\Users\Tobias Weltner\Sources\docs
Mode LastWriteTime Length Name
---- ------------- ------ ----
d---- 13.10.2007 13:31 testdirectory
Set-Content .\testdirectory\testfile.txt "Hello"
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 |L| No to All |S| Suspend |?| Help (default is "Y"):
But if you had specified the -recurse parameter, PowerShell would have deleted the directory, including its contents, immediately and without asking for confirmation:
del testdirectory -recurse
Managing Access Permissions
For NTFS drives, access permissions determine which users may access files and directories. For each file and directory, security data is laid down in what is known as a "security descriptor" (SD). The security descriptor determines whether security settings are valid only for the current directory or whether they can be passed on other files and directories. The real access permissions are on access control lists (ACL). An access control entry (ACE) for every single access permission is on the ACL.
File and directory access permissions are equivalent to complex electronic locks. If used properly, you can make them into an effective security system. However if improperly used, you can just as easily lock yourself out, lose access to important data, or damage the Windows operating system (when you unintentionally block access to key system directories). As owner of a file or directory, you always have the option of correcting permissions, and as an administrator, you can always assume ownership of a file or directory. But that's a back door you can't rely on: you should change permissions only when you are fully aware of what will be the consequences. It's better to experiment initially with test directories and files.
PowerShell uses the cmdlets Get-Acl und Set-Acl to manage permissions. In addition, traditionally proven commands like cacls are available to you in the PowerShell console. Often, they can modify access permissions more quickly than PowerShell cmdlets, particularly when you're working with very many files and directories. Since Windows Vista was released, cacls has been regarded as outdated. If possible, use its successor, icacls.
cacls /?
NOTE: Cacls is now deprecated, please use Icacls.
Displays or modifies access control lists (ACLs) of files
CACLS Filename [/T] [/M] [/L] [/S[:SDDL]] [/E] [/C]
[/G user:perm] [/R user [...]]
[/P user:perm [...]]
[/D user [...]]
Filename Displays ACLs.
/T Changes ACLs of specified files in
the current directory and all subdirectories.
/L Performs the action on a symbolic link versus its destination.
/M Changes ACLs of volumes mounted to a directory.
/S Displays the SDDL string for the DACL.
/S:SDDL Replaces the ACLs with those specified in the SDDL string
(not valid with /E, /G, /R, /P, or /D).
/E Edit ACL instead of replacing it.
/C Continue on access denied errors.
/G user:perm Grant specified user access permissions.
Perm can be: R Read
W Write
C Change (write)
F Full control
/R user Revoke specified user's access permissions (only valid with /E).
/P user:perm Replace specified user's access permissions
Perm can be: N None
R Read
W Write
C Change (write)
F Full control
/D user Deny specified user access.
Wildcards can be used to specify more than one file in a command.
You can specify more than one user in a command.
Abbreviations:
CI - Container Inherit.
Checking Effective Security Settings
Effective security settings of directories and files are on the access control list, and PowerShell retrieves the contents of this list when you use Get-Acl. So, if you would like to find out who has access to a certain file or a certain directory, proceed as follows:
Get-Acl $env:windir
Directory: Microsoft.PowerShell.Core\FileSystem::C:\
Path Owner Access
---- ----- ------
Windows NT SERVICE\TrustedInstaller CREATOR OWNER Allow 268435...
Establishing the Identity of the Owner
The owner of a file or directory has special rights. For example, the owner can always get access and you can find the owner in the Owner property:
(Get-Acl $env:windir).Owner
NT SERVICE\TrustedInstaller
Listing Access Permissions
Actual access permissions—who may do what—are output in the Access property:
(Get-Acl $env:windir).Access | Format-Table -wrap
FileSystem Access IdentityReference IsInhe InheritanceFlags PropagationFlags
Rights Control rited
Type
---------- ------- ----------------- ------ ---------------- ----------------
268435456 Allow CREATOR OWNER False ContainerInherit, InheritOnly
ObjectInherit
268435456 Allow NT AUTHORITY\ False ContainerInherit, InheritOnly
SYSTEM ObjectInherit
Modify, Sync Allow NT AUTHORITY\ False None None
hronize SYSTEM
268435456 Allow BUILTIN\Admi False ContainerInherit, InheritOnly
nistrators ObjectInherit
Modify, Sync Allow BUILTIN\Admi False None None
hronize nistrators
-1610612736 Allow BUILTIN\Users False ContainerInherit, InheritOnly
ObjectInherit
ReadAndExecu Allow BUILTIN\Users False None None
te, Synchron
ize
268435456 Allow NT SERVICE\Truste False ContainerInherit InheritOnly
dInstaller
FullControl Allow NT SERVICE\Truste False None None
dInstaller
The IdentityReference column of this overview tells you who has special permission; the FileSystemRights column also tell you the type of permission. The AccessControlType column is particularly important because if it shows Deny instead of Allow, you will know who is restricted and who has access.
Creating New Permissions
The object returned by Get-Acl contains a number of methods that you can use to modify permissions or assume ownership. If you'd like to set permissions yourself, you don't necessarily have to delve deeply into the world of security descriptors. Often, it suffices either to read the security descriptor of an existing file and to transfer it to another or to specify the security information in the form of a text in the special SDDL language.
Whether you want to modify the structure of the security descriptor yourself or acquire a complete security descriptor: use Set-Acl to assign the security descriptor to a new object.
The below examples will acquaint you with all the usual procedures. Note two aspects: don't forget proven tools, like cacls, because you may be able to do your work more quickly with it than with PowerShell. Moreover, the Get-Acl and Set-Acl team work not only on the file level, but also everywhere where security descriptors control access, such as in the Windows registry (see next chapter).
"Cloning" Permissions
In a rudimentary case, you wouldn't create any new permissions but would "clone" permissions by transferring the access control list of an existing directory (or a file) to another. The advantage is that this enables you to use a graphic interface to set permissions, which are often complex.
Because the manual adjustment of security settings is a job for professionals, non-commercial Windows versions like Windows XP Home do not have this option. Nevertheless, you can use PowerShell to modify file and directory permissions used in these Windows versions as well.
To begin, create two directories as a test:
md Prototype | out-null
md Protected | out-null
Now, open Explorer, and change the security settings of the Prototype directory.
explorer .
In Explorer, right-click the Prototype directory and select Properties. Then, click the Security tab.
Figure 15.2: Modifying security settings of the directory using a dialog box
Click Modify and add additional people to change the security settings of the test directory. Set permissions for the new persons in the lower area of the dialog box.
You may also deny users permission by putting a check mark after permission in the Deny column. You have to be very careful when doing this since restrictions always have priority over permissions. For example, if you were to grant yourself full access but deny access for the Everyone group, you would shut yourself out of your own system. You also belong to the Everyone group, and since restrictions have priority, the restrictions also apply to you—even though you granted yourself full access.
After you've changed permissions, take a look at the permissions of the second test directory, Protected, in Explorer. This directory is still assigned default permissions. In the next step, the new permissions of the Prototype directory will be transferred to the Protected directory:
$acl = Get-Acl Prototype
Set-Acl Protected $acl
You need special rights to set permissions. If you're operating PowerShell using the Windows Vista operating system and User Account Control is active, you won't have these permissions and you'll get an error message. Run PowerShell as administrator to obtain the permissions.
That's all there is to it. The Protected directory is now just as secure as the Prototype directory, and when you check their security settings in Explorer, you'll see that all their settings are identical.
Using SDDL to Set Permissions
The previous example was very simple because all you did was transfer the security settings of an existing directory to another. In your daily work, you'll always require a Prototype directory, and that's often unwanted. But you can also summarize the security settings of a security descriptor as text. Each security setting is defined in the special Security Descriptor Description Language (SDDL). It enables you to read out the security information of the Prototype directory as text and use it later without having to resort to the Prototype directory.
Let's delete the old Protected test directory, and then save the security information of the Prototype directory in the SDDL:
Del Protected
$acl = Get-Acl Prototype
$sddl = $acl.Sddl
$sddl
O:S-1-5-21-3347592486-2700198336-2512522042-1000G:S-1-5-21-3347592486-
2700198336-2512522042-513D:AI(A;OICI;0x1200a9;;;WD)(A;OICI;FA;;;LA)(A;ID;
FA;;;S-1-5-21-3347592486-2700198336-2512522042-1000)(A;OICIIOID;GA;;;S-1-
5-21-3347592486-2700198336-2512522042-1000)(A;ID;FA;;;SY)(A;OICIIOID;GA;;
;SY)(A;ID;FA;;;BA)(A;OICIIOID;GA;;;BA)
You could now include the SDDL text in a second script and assign its security settings to any directory .
Md Protected
$sddl = "O:S-1-5-21-3347592486-2700198336-2512522042-1000G:" + `
"S-1-5-21-3347592486-2700198336-2512522042-513D:" + `
"AI(A;OICI;0x1200a9;;;WD)(A;OICI;FA;;;LA)" + `
"(A;ID;FA;;;S-1-5-21-3347592486-2700198336-2512522042-1000)" + `
"(A;OICIIOID;GA;;;S-1-5-21-3347592486-2700198336-2512522042-1000)" + `
"(A;ID;FA;;;SY)(A;OICIIOID;GA;;;SY)(A;ID;FA;;;BA)(A;OICIIOID;GA;;;BA)"
$acl = Get-Acl Protected
$acl.SetSecurityDescriptorSddlForm($sddl)
Set-Acl Protected $acl
Your second script is completely independent of your Prototype directory. What you've done is use the Prototype directory only temporarily to generate the SDDL definition of your security settings with the help of the user interface.
However, the SDDL cannot be simply transferred to other computers. If you take a second look, you'll see that each authorized person is not identified by name, but by a security identifier (SID). This SID differs from person to person so even if there were accounts on several computers that have the same name, they would be different accounts, in reality, with different SIDs. However inside a domain, the SIDs of user accounts is the same on all computers because the domain centrally manages them. As a result, the SDDL solution is ideal for domain-based company networks. Nevertheless, if you're working in a small peer-to-peer network, SDDL can be useful. You just have to use "copy & paste" to replace the SIDs of respective accounts. It would be even simpler, though, to use the commands cacls or icacls in peer-to-peer networks.
Manually Creating New Permissions
Permissions can also be created manually. The advantage is that you specify authorized users by name so that this approach would work on any computer in the same way—even if there is no central domain.
But note that this involves extra effort because then you would have to create the security descriptor entirely on your own. The next example will show you how to do this. However in practice, this procedure is usually too time-consuming. It's simpler in this case to use commands like cacls or icacls. Now, let's delete the Protected test directory again and create a new one so that the directory is again assigned default access rights:
Del Protected
Md Protected
Ultimately, this directory should have general read access permission for the Everyone group and full access to the Administrator account. To accomplish this, use AddAccessRule() to add two new access rules to the security descriptor:
$acl = Get-Acl Protected
$person = [System.Security.Principal.NTAccount]"Administrator"
$access = [System.Security.AccessControl.FileSystemRights]"FullControl"
$inheritance = [System.Security.AccessControl.InheritanceFlags] `
"ObjectInherit,ContainerInherit"
$propagation = [System.Security.AccessControl.PropagationFlags]"None"
$type = [System.Security.AccessControl.AccessControlType]"Allow"
$rule = New-Object System.Security.AccessControl.FileSystemAccessRule( `
$person,$access,$inheritance,$propagation,$type)
$acl.AddAccessRule($rule)
$person = [System.Security.Principal.NTAccount]"Everyone"
$access = [System.Security.AccessControl.FileSystemRights]"ReadAndExecute"
$inheritance = [System.Security.AccessControl.InheritanceFlags] `
"ObjectInherit,ContainerInherit"
$propagation = [System.Security.AccessControl.PropagationFlags]"None"
$type = [System.Security.AccessControl.AccessControlType]"Allow"
$rule = New-Object System.Security.AccessControl.FileSystemAccessRule( `
$person,$access,$inheritance,$propagation,$type)
$acl.AddAccessRule($rule)
Set-Acl Protected $acl
Next, let's look at how each access rule is defined. Five details are required for each rule:
- Person: Here the person or the group is specified to which the rule is supposed to apply.
- Access: Here permissions are selected that the rule controls.
- Inheritance: Here the objects are selected to which the rule applies.The rule can, and normally also is, granted to child objects, so it applies automatically to files that are in a directory.
- Propagation: Determines whether permissions are passed to child objects (such as subdirectories and files). Normally, the setting is None and permissions are merely granted.
- Type: This enables you to set either a permission or restriction. If restriction, the permissions that were specified will expressly not be granted.
The next question is: which values are allowed for these specifications? The example shows that specifications are given in the form of special .NET objects (Chapter 6). You can list all the permitted values for access permissions by using the following trick:
[System.Enum]::GetNames([System.Security.AccessControl.FileSystemRights])
ListDirectory
ReadData
WriteData
CreateFiles
CreateDirectories
AppendData
ReadExtendedAttributes
WriteExtendedAttributes
Traverse
ExecuteFile
DeleteSubdirectoriesAndFiles
ReadAttributes
WriteAttributes
Write
Delete
ReadPermissions
Read
ReadAndExecute
Modify
ChangePermissions
TakeOwnership
Synchronize
FullControl
You would actually have to combine the relevant values from the list if you want to set access permissions, such as like this:
$access = [System.Security.AccessControl.FileSystemRights]::Read `
-bor [System.Security.AccessControl.FileSystemRights]::Write
$access
131209
The result is a number, the bitmask for permissions to read and write. In the above example, you achieved the same result more easily because you are allowed to specify wanted items, even if they are comma-separated items and enclosed in brackets, after a .NET enumeration:
$access = [System.Security.AccessControl.FileSystemRights]"Read,Write"
$access
Write, Read
[int]$access
131209
Because you didn't carry out any binary -bor calculations here, the result is readable text. But in this case the bitmask is at work here, as the conversion to the Integer data type proves. You can find out what the underlying value of a setting is at any time like this:
[int][System.Security.AccessControl.InheritanceFlags] `
"ObjectInherit,ContainerInherit"
3
The significance of this for you is that you can now examine the permitted values for the other .NET enumerations and convert these into numbers. While it won't make your commands more readable, they will be shorter because the following lines do the same thing as the lines in the preceding example:
Del Protected
Md Protected
$acl = Get-Acl Protected
$rule = New-Object System.Security.AccessControl.FileSystemAccessRule( `
"Administrator",2032127,3,0,0)
$acl.AddAccessRule($rule)
$rule = New-Object System.Security.AccessControl.FileSystemAccessRule( `
"Everyone",131241,3,0,0)
$acl.AddAccessRule($rule)
Set-Acl Protected $acl
Finally, let's look at how PowerShell specifies persons to whom permissions apply. In the above examples, you specified the names of users or of groups. Because permissions are not responsive to names, but to the unique SIDs of user accounts, names are changed internally to SIDs. You can also change names manually to see whether a specified user account in fact exists:
$Account = [System.Security.Principal.NTAccount]"Administrators"
$SID = $Account.translate([System.Security.Principal.Securityidentifier])
$SID
BinaryLength AccountDomainSid Value
------------ ---------------- -----
16 S-1-5-32-544
An NTAccount object describes a security principal, which is something to which permissions can be granted. In practice, this is users and groups. The NTAccount object can use Translate() to output the information it contains through the principal into its SID. However, this will only work if the specified account in fact exists. Otherwise, you will get an error, so you should use Translate() to validate the existence of the account.
The unique SID that Translate() retrieves is also useful. If you look closely, you'll discover that the SID of the Administrators group clearly differs from the SID of your own user account:
([System.Security.Principal.NTAccount]"$env:userdomain\$env:username").`
Translate([System.Security.Principal.Securityidentifier]).Value
S-1-5-21-3347592486-2700198336-2512522042-1000
([System.Security.Principal.NTAccount]"Administrators").`
Translate([System.Security.Principal.Securityidentifier]).Value
S-1-5-32-544
The SID of the Administrators group is not only much shorter, but also unique. For its integrated accounts, Windows uses so-called "well-known" SIDs, which are the same in all Windows systems. This is important because if you were to run your above script on a German system, it would fail since the Administrators group is called "Administratoren," and the "Everyone" group is called "Jeder" on systems localized for Germany. The SIDs of these groups are identical, and knowing this for integrated accounts, you should use SIDs instead of localized names. This is how you turn a SID into the name of a user account:
$sid = [System.Security.Principal.SecurityIdentifier]"S-1-1-0"
$sid.Translate([System.Security.Principal.NTAccount])
Value
-----
Everyone
And this is how your script could work flawlessly in international localizations:
Del Protected
Md Protected
$acl = Get-Acl Protected
$sid = [System.Security.Principal.SecurityIdentifier]"S-1-5-32-544"
$access = [System.Security.AccessControl.FileSystemRights]"FullControl"
$rule = New-Object System.Security.AccessControl.FileSystemAccessRule( `
$sid,$access,3,0,0)
$acl.AddAccessRule($rule)
$sid = [System.Security.Principal.SecurityIdentifier]"S-1-1-0"
$access = [System.Security.AccessControl.FileSystemRights]"ReadAndExecute"
$rule = New-Object System.Security.AccessControl.FileSystemAccessRule( `
$sid,$access,3,0,0)
$acl.AddAccessRule($rule)
Set-Acl Protected $acl
Posted
Mar 30 2009, 08:06 AM
by
ps1