Onto event 3 http://blogs.technet.com/b/heyscriptingguy/archive/2012/04/04/2012-scripting-games-beginner-event-3-create-a-file-in-a-folder.aspx where we have to create a file in a folder. Simple – well yes but not with the twists the Scripting Guy throws at you.
The requirements:
- save file in c:\2012sg\event3
- file contains process name and id running on local machine
- file must be called Process3.txt
- if folder doesn’t exist must create it
- if don’t have permissions to root of c: create it somewhere where you do have permissions
- script should not generate errors that are ignored
- script doesn’t need command line parameters but get extra points for including them
- if include command line parameters should include comment based help and sample of using script
Lets start with the file contents. We can find the process information like this
Get-Process
Use a select to restrict the data to the two properties we want
Get-Process | select Name, Id
That looks ugly so lets bring the two columns together so that it’s easier on the eye.
Get-Process | Format-Table Name, Id -a
The PowerShell object police will be screaming at this point because I’m not outputting objects BUT the data is going to end up in a file so it doesn’t matter.
Now we need to get the data into the file
PS> Get-Process | Format-Table Name, Id -a | Out-File -FilePath Process3.txt
PS> Get-Content Process3.txt
Name Id
—- —
armsvc 1764
BingApp 3216
BingBar 3764
bingsurrogate 2692
etc
Even kept the case on the file name!
So we can create a file with the data we need. What about the folder?
It has to be a specific path and if the path doesn’t exist we have to create it. Need to test for existence and create folder if it doesn’t exist.
The trick here is to test for the whole folder path. Ignore the file we are going to create that anywhere. Don’t test for each element in the path – if c:\2012sg\event3 exists then c:\2012 MUST exist. We don’t care about the file because it will be over written if it exists.
There are a number of cmdlets in PowerShell for working with paths
PS> Get-Command *path
CommandType Name
———– —-
Cmdlet Convert-Path
Cmdlet Join-Path
Cmdlet Resolve-Path
Cmdlet Split-Path
Cmdlet Test-Path
We want test-path. If the path DOESN’T exist we need to create it. To get the neatest code we test on the negative of its existence and create the folder
$path = "C:\2012sg\event3"
if (-not (Test-Path -Path $path)){
New-Item -Path "C:\2012sg\event3" -ItemType Directory
}
Test-Path -Path $path
This will return True when we test the second time. The folder information is written to screen
Directory: C:\2012sg
Mode LastWriteTime Length Name
—- ————- —— —-
d—- 13/04/2012 19:47 event3
We can suppress this by adding Out-Null onto the New-Item line
New-Item -Path "C:\2012sg\event3" -ItemType Directory | Out-Null
At this time we have the basis of our entry
if (Test-Path -Path C:\2012sg){ Remove-Item -Path "C:\2012sg" -Recurse -Force } $path = "C:\2012sg\event3" $file = "Process3.txt" if (-not (Test-Path -Path $path)){ New-Item -Path "C:\2012sg\event3" -ItemType Directory | Out-Null } Get-Process | Format-Table Name, Id -a | Out-File -FilePath (Join-Path -Path $path -ChildPath $file)
The first test-path deletes the folder AND contents if it exists. This is useful when testing to ensure you always end up testing the script properly but don’t forget to remove it at some stage so you can test that the script works if the folder exists!
I used Join-Path to create the full path to the file – could have done “$path\$file” or even worse $path + “\” + $file
That has dealt with the first four objectives. Now what if we don’t have permissions to the root of C:
I’m not messing with the permissions on my C: drive but I’ve created a test folder and denied the permissions to create folders and files – typical permissions
This is what happens
PS> New-Item -Path c:\testperm -Name Myfolder -ItemType Directory
New-Item : Access to the path ‘Myfolder’ is denied.
At line:1 char:9
+ New-Item <<<< -Path c:\testperm -Name Myfolder -ItemType Directory
+ CategoryInfo : PermissionDenied: (C:\testperm\Myfolder:String) [New-Item], UnauthorizedAccessException
+ FullyQualifiedErrorId : CreateDirectoryUnauthorizedAccessError,Microsoft.PowerShell.Commands.NewItemCommand
Now we could use Get-Acl to test permissions
PS> Get-Acl -Path c:\testperm | select -ExpandProperty Access |
Format-Table IdentityReference, FileSystemRights, AccessControlType -AutoSize
IdentityReference FileSystemRights AccessControlType
—————– —————- —————–
BUILTIN\Administrators CreateFiles, AppendData Deny
BUILTIN\Administrators FullControl Allow
BUILTIN\Administrators 268435456 Allow
NT AUTHORITY\SYSTEM FullControl Allow
NT AUTHORITY\SYSTEM 268435456 Allow
BUILTIN\Users ReadAndExecute, Synchronize Allow
NT AUTHORITY\Authenticated Users Modify, Synchronize Allow
NT AUTHORITY\Authenticated Users -536805376 Allow
But in reality we don’t know what permissions have been set so potentially we would have to test a lot of permissions. Not easy to get right and we don’t want to generate errors that are ignored.
We also need somewhere to put the file if we can’t get to C:\
You might think that this would work
path = "C:\Testperm\2012sg\event3"
$file = "Process3.txt"
if (-not (Test-Path -Path $path)){
try {
New-Item -Path $path -ItemType Directory |
Out-Null
}
catch {
write-host "oops"
}
}
Get-Process |
Format-Table Name, Id -a |
Out-File -FilePath (Join-Path -Path $path -ChildPath $file)
But it doesn’t because the error message thrown by New-Item isn’t a .NET exception. Its a message to say that the task hasn’t completed
Lets try this
$path = "C:\Testperm\2012sg\event3"
if (-not (Test-Path -Path $path)){
$rt = New-Item -Path $path -ItemType Directory
}
$rt
$path = "C:\2012sg\event3"
if (-not (Test-Path -Path $path)){
$rt = New-Item -Path $path -ItemType Directory
}
$rt
Which captures the two return messages
New-Item : Access to the path ‘event3’ is denied.
At line:4 char:18
+ $rt = New-Item <<<< -Path $path -ItemType Directory
+ CategoryInfo : PermissionDenied: (C:\Testperm\2012sg\event3:String) [New-Item], UnauthorizedAccessException
+ FullyQualifiedErrorId : CreateDirectoryUnauthorizedAccessError,Microsoft.PowerShell.Commands.NewItemCommand
Directory: C:\2012sg
Mode LastWriteTime Length Name
—- ————- —— —-
d—- 13/04/2012 20:29 event3
Working with either of these isn’t going to be easy BUT if the directory doesn’t create we get a error which we can access through the $error automatic variable
$path = "C:\2012sg\event3" $file = "Process3.txt" if (-not (Test-Path -Path $path)){ $rt = New-Item -Path $path -ItemType Directory -ErrorAction SilentlyContinue if (Select-String -InputObject $error[0] -Pattern "event3" -SimpleMatch){ $path = (Join-Path -Path $env:HOMEDRIVE -ChildPath $env:HOMEPATH) + (Split-Path -Path $path -NoQualifier) New-Item -Path $path -ItemType Directory | Out-Null } } Get-Process | Format-Table Name, Id -a | Out-File -FilePath (Join-Path -Path $path -ChildPath $file)
Set the folder creation to silently continue and test the last error message for part of the folder path – if its there something has gone wrong so we swing to creating the folder where we will have permissions – our home drive. A bit of manipulation with the path cmdlets and we have a new path where we can create the folder
get-process can then be used as before
That takes care of the folder creation if no permissions and the error messages being unhandled
At the moment the paths are hard coded but if we want to change then using command line parameters is the best bet. Keep the current as defaults.
param ( [string]$path = "C:\2012sg\event3", [string]$file = "Process3.txt" ) if (-not (Test-Path -Path $path -IsValid)){ Throw "Invalid path $path" } if (-not (Test-Path -Path $path)){ $rt = New-Item -Path $path -ItemType Directory -ErrorAction SilentlyContinue if (Select-String -InputObject $error[0] -Pattern "event3" -SimpleMatch){ $path = (Join-Path -Path $env:HOMEDRIVE -ChildPath $env:HOMEPATH) + (Split-Path -Path $path -NoQualifier) New-Item -Path $path -ItemType Directory | Out-Null } } Get-Process | Format-Table Name, Id -a | Out-File -FilePath (Join-Path -Path $path -ChildPath $file)
We can use it like this to use the defaults
.\beg3.ps1
or like this to put the file somewhere else
.\beg3.ps1 -path "c:\testbeg3\event3" -file "testP3.txt"
Lets add some comments – don’t use # – use write-debug and write-verbose
[CmdletBinding()] param ( [string]$path = "C:\2012sg\event3", [string]$file = "Process3.txt" ) Write-Verbose "Test path is valid" if (-not (Test-Path -Path $path -IsValid)){ Throw "Invalid path $path" } if (-not (Test-Path -Path $path)){ Write-Verbose "Create Folder" $rt = New-Item -Path $path -ItemType Directory -ErrorAction SilentlyContinue if (Select-String -InputObject $error[0] -Pattern "event3" -SimpleMatch){ $path = (Join-Path -Path $env:HOMEDRIVE -ChildPath $env:HOMEPATH) + (Split-Path -Path $path -NoQualifier) Write-Debug $path New-Item -Path $path -ItemType Directory | Out-Null } } Write-Verbose "Write Process data" Get-Process | Format-Table Name, Id -a | Out-File -FilePath (Join-Path -Path $path -ChildPath $file)
Which means we can do this – all for adding [CmdletBinding()] to the top of the script
PS> .\beg3.ps1 -Verbose
VERBOSE: Test path is valid
VERBOSE: Write Process data
last job is add the help
[CmdletBinding()] param ( [string]$path = "C:\2012sg\event3", [string]$file = "Process3.txt" ) Write-Verbose "Test path is valid" if (-not (Test-Path -Path $path -IsValid)){ Throw "Invalid path $path" } if (-not (Test-Path -Path $path)){ Write-Verbose "Create Folder" $rt = New-Item -Path $path -ItemType Directory -ErrorAction SilentlyContinue if (Select-String -InputObject $error[0] -Pattern "event3" -SimpleMatch){ $path = (Join-Path -Path $env:HOMEDRIVE -ChildPath $env:HOMEPATH) + (Split-Path -Path $path -NoQualifier) Write-Debug $path New-Item -Path $path -ItemType Directory | Out-Null } } Write-Verbose "Write Process data" Get-Process | Format-Table Name, Id -a | Out-File -FilePath (Join-Path -Path $path -ChildPath $file) <# .SYNOPSIS Creates a folder to hold a file containing process data .DESCRIPTION A folder - by default "C:\2012sg\event3" - is created and a file called "Process3.txt" by default is created holding process data. The process name and id is used to fill the file. Any existing file is over written If the folder can't be created in the root of C: it is created in $env:HOMEDRIVE\$env:HOMEPATH i.e. the users home drive .PARAMETER path Path of the folder we want to create Default is "C:\2012sg\event3" .PARAMETER file File name of file to hold process data Default is "Process3.txt" .EXAMPLE .\beg3.ps1 Runs script with default values .EXAMPLE .\beg3.ps1 -path "c:\testbeg3\event3" -file "testP3.txt" Runs script with values for path and file supplied .EXAMPLE .\beg3.ps1 -Verbose Runs script with verbose output #>
I always add help at the end. check about_comment_based help for further details