header image

Scripting games 2012 comments: #6 Beginners event 3

Posted by: | April 13, 2012 | No Comment |

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

under: PowerShell Basics, Scripting Games