Breaking out of a foreach loop - powershell

Currently using the following code to search a directory and copy any .msi that is found to another directory. I don't want to copy a .msi if it is in use by another program. I saw other questions on StackExchange that showed other ways to check if a file was in use but using a try/catch block around a Rename-Item command seemed like a simpler solution. If the script can't rename the item I want it go to the next $file in $listoffiles. The only way that I could get it to work was by using the code listed below. However I can't seem to format the resulting error message the way I want and it seems like -ErrorAction Stop shouldn't be necessary. If an error occurs in the try block shouldn't the error just go to the catch block? Also, only 2 of the 3 variables in my error message are written out to the log file.
Current code:
$listofFiles=(Get-ChildItem -Path $outputPath -Filter "*.msi" | where {$_.Name -notlike "*.Context.msi" -and $_.LastAccessTime -gt (Get-Date).AddMinutes(-15)})
foreach ($file in $listofFiles){
$y = ($file -split ("\\"))
$msiFolder = $y[4]
#Tests to see if $file currently in use
try{
Rename-Item -Path $file -NewName $file -ErrorVariable renameError -ErrorAction Stop
}
catch{
"$logTime ERROR Could not copy $msiFolder: $renameError.Exception.Message()" >> $copyLog
continue
}
#Code that copies files that are not in use.
Currently the error is displayed like so:
02/18/13 13:43:25 ERROR Could not copy System.Management.Automation.ActionPreferenceStopException: Command execution stopped because the preference variable "ErrorActionPreference" or common parameter is set to Stop: The process cannot access the file because it is being used by another process.
at System.Management.Automation.StatementListNode.ExecuteStatement(ParseTreeNode statement, Array input, Pipe outputPipe, ArrayList& resultList, ExecutionContext context)
at System.Management.Automation.StatementListNode.Execute(Array input, Pipe outputPipe, ArrayList& resultList, ExecutionContext context)
at System.Management.Automation.TryStatementNode.Execute(Array input, Pipe outputPipe, ArrayList& resultList, ExecutionContext context).Exception.Message()
I would like for it to look like:
02/18/13 13:34:34 ERROR Could not copy ABC_CLIENT: The process cannot access the file because it is being used by another process.
Questions
Why does $msiFolder not appear in the log message?
How do I get simplify the error message?
Is there a better way to use a try/catch block to break out of an iteration of the foreach loop?
Update 1:
I cleared $error and ran:
Rename-Item -Path $file -NewName $file -ErrorAction SilentlyContinue
if (!$?){"$logTime ERROR Could not copy $file: $error[0].Exception.Message()" >> $copyLog}
The result was:
02/18/13 14:52:59 ERROR Could not copy The process cannot access the file because it is being used by another process.[0].Exception.Message()
Why does $logTime get printed but the other variables do not?
Update 2:
Final code that I used:
Rename-Item -Path $file -NewName $file -ErrorAction SilentlyContinue
if (!$?){
"$logTime ERROR Could not copy ${msiFolder}: $($error[0].Exception.Message)" >> $copyLog
continue
}

If you want to use try/catch you need to convert the non-terminating error you get from Rename-Item to a terminating error. That is why you need to use -ErrorAction Stop to make your catch block get invoked. However, you can do this another way e.g.:
foreach ($file in $listofFiles){
$y = ($file -split ("\\"))
$msiFolder = $y[4]
#Tests to see if $file currently in use
Rename-Item -Path $file -NewName $file -ErrorVariable renameError -ErrorAction SilentlyContinue
if (!$?) {
"$logTime ERROR Could not copy ${msiFolder}: $($error[0].Exception.Message)" >> $copyLog
continue
}
#Code that copies files that are not in use.
}
The automatic variable $? indicates whether the last command succeeded or not.
As for why $msiFolder isn't getting output, I would check the split operation. Check that $y.count is >= 5.

Related

Suppress Error output Where-Object on a cmd command

I'm trying to suppress an error output from a Where-Object of a cmd command (gpresult /r), first I saved the output of the gpresult to a variable, then I filtered the variable with the Where-Object and added two filters, to find two AD groups that the user should be member of.
The problem comes when the user is not in any of those groups (that it could happen because not everyone uses the programs related to those groups), the console prints a ugly error that we don't want the user to see... I tried adding -ErrorAction SilentlyContinue to the Where-Object with no avail, the error is still popping up.
Do you guys have any clue on this?
Here's the code, so you can understand better what I'm trying to suppress:
$gpresult = gpresult /r
$userGroups = ($gpresult | Where-Object -FilterScript {($_ -match 'Group1_*') -or ($_ -match 'Group2_*')} -ErrorAction SilentlyContinue).Trim()
Thanks in advance!
Edit:
Here's the error I get (with 2>$null):
I tried adding -ErrorAction SilentlyContinue to the Where-Object to no avail, the error is still popping up.
The unwanted error happens during the (...).Trim() method call, not in the Where-Object pipeline:
If the pipeline produces no output, the statement is equivalent to $null.Trim(), which predictably causes the following statement-terminating error:
You cannot call a method on a null-valued expression.
Therefore, to avoid this error, you must avoid the .Trim() call if the pipeline produces no output:
$userGroups =
$gpresult |
Where-Object -FilterScript {($_ -match 'Group1_*') -or ($_ -match 'Group2_*')} |
ForEach-Object Trim
Note: The above uses simplified syntax to call .Trim() on each input object from the pipeline; if there is no input object, no call is made, which avoids the error.
The non-simplified equivalent of ForEach-Object Trim is ForEach-Object { $_.Trim() }
You could alternatively use a try { ... } catch { ... } statement to suppress the error (a simplified example: try { $null.Trim() } catch { }), but note that catching statement-terminating errors (exceptions) is comparatively slower than the above approach.
I am not completely sure I understand what you are trying to do but you can separate standard out and standard error streams
For example redirecting stderr to null will completely remove it. If you add this to the end of your command.
2>$null
2 is error stream
If you want to separate them later you should be able to. Because data from stdout will be strings and data from stderr System.Management.Automation.ErrorRecord objects.
$gpresult = gpresult /r
$stderr = $gpresult | ?{ $_ -is [System.Management.Automation.ErrorRecord] }
$stdout = $gpresult | ?{ $_ -isnot [System.Management.Automation.ErrorRecord] }

Is there a way to Expand-Archive without overwriting files?

Is there a way in PowerShell to use Expand-Archive so that files are written where they don't exist, but are not overwritten when they do exist? I can achieve this with -ErrorAction SilentlyContinue, but that ignores things that might be actual errors.
To silence only "file already exists" error messages of Expand-Archive, you can redirect the error stream to the success stream and process error records using ForEach-Object:
Expand-Archive -Path Test.zip -DestinationPath . -EA Continue 2>&1 | ForEach-Object {
if( $_ -is [System.Management.Automation.ErrorRecord] ) {
if( $_.FullyQualifiedErrorId -split ',' -notcontains 'ExpandArchiveFileExists' ) {
Write-Error $_ # output error that is not "file exists"
}
}
else {
$_ # pass success stream through
}
}
-EA Continue (-ErrorAction) overrides the preference variable $ErrorActionPreference to make sure errors are not turned into exceptions (in which case the first error would interrupt the extraction).
2>&1 redirects (merges) the error stream (#2) to the success stream (#1), so both can be processed using ForEach-Object.
$_ -is [System.Management.Automation.ErrorRecord] tests if the current pipeline element is an error record.
When this is the case, we test what kind of error we have, by checking the FullyQualifiedErrorId property of the ErrorRecord (the exception type System.IO.IOException would be too general to test for)
Otherwise it is a message from the success stream, which will be simply passed through.
In case you are wondering how I came up with that FullyQualifiedErrorId thing, I just run Expand-Archive without redirection and called Get-Error afterwards. This outputs all information of the last error record, so I could look up the information to detect the error condition.
An alternative solution, similar to the one suggested by Abraham Zinala, is to unconditionally silence all errors and use -ErrorVariable to collect the errors and shown the relevant ones after the call to Expand-Archive has returned:
$oldErrorActionPreference = $ErrorActionPreference
$ErrorActionPreference = 'SilentlyContinue'
$archiveErrors = $null
Expand-Archive -Path Test.zip -DestinationPath . -ErrorVariable archiveErrors
$ErrorActionPreference = $oldErrorActionPreference
$archiveErrors | Sort-Object { $_ | Out-String } -Unique | ForEach-Object {
if( $_ -is [System.Management.Automation.ErrorRecord] ) {
if( $_.FullyQualifiedErrorId -split ',' -notcontains 'ExpandArchiveFileExists' ) {
$_ # output error that is not "file exists"
}
}
}
The errors of Expand-Archive cannot be completely silenced through the -ErrorAction parameter, because some errors (like input file doesn't exist) are detected as part of parameter validation. To really silence all errors, the $ErrorActionPreference variable must be used.
It is important to set the error variable to $null before calling Expand-Archive because the command doesn't reset the variable, when there is no error.
The name of the variable passed to -ErrorVariable must be specified without $.
The Sort-Object -Unique command makes sure we don't show duplicate errors.

using a delimiter to split files names in powershell

I use a simple function to download files and return the path to me when updating computers for simplicity.
I was stuck on why it was not working then realized that the proxy is appending a random number to the filename so instead of it being 12345.zip it is actually 8493830_12345.zip.
I have tried to find the file using the "_" as a split but while there are no errors, the file is not being returned and I have checked it is there manually.
function FileCheck {
$fileName.Split("_")[1]
$fileName = "{0}.zip" -f 12345
Download -ZipFileName $($fileName) -OutputDirectory $env:temp
$SleepTime = 300
$sleepElapsed = 0
$sleepInterval = 20
Start-Sleep $sleepInterval
$file = Get-ChildItem -Path $env:temp -Filter "$fileName*"
if ($file -ne $null) {
return $file[0].FullName
}
Start-Sleep($sleepInterval)
$sleepElapsed += $sleepInterval
if (($SleepTime) -le $sleepElapsed){
# Check for file with given prefix
$file = Get-ChildItem -Path $env:temp -Filter "$fileName*"
if ($file -eq $null) {
Write-Error 'file not found'
return $null
}
return $file[0].FullName
}
}
I am guessing the split is not working but googling and moving the filename.split has not worked for me. Any help is appreciated
Well, your split is doing nothing at all. You haven't defined $filename, but if you had, and it had an underscore, then $filename.split('_') would return two or more strings, depending on how many underscores were in the original string, but you never capture the result. I think the real problem here is the filter you are applying to Get-ChildItem later in your function.
$file = Get-ChildItem -Path $env:temp -Filter "$fileName*"
That will look for files beginning with $fileName, which you define on line 4 to be "12345.zip". That is exactly the opposite of what you want to be looking for. You need to move the asterisk to before $fileName, so it looks like this:
$file = Get-ChildItem -Path $env:temp -Filter "*$fileName"
That will return all files that end with "12345.zip", which would include things like:
myfuzzyhippo12345.zip
learn-to-count-12345.zip
8493830_12345.zip
Basically anything that ends in 12345.zip. Also, it appears that you are under the impression that executing a return $file[0].fullname or return $null will stop the function. That's a mistake. A function runs to completion unless exited early by something like a break command. Also, everything not explicitly captured or redirected will be passed back from the function, so reading through your function people are likely to get the output of your $filename.split('_') line, then possibly $null or $filename[0].fullname.
Lastly, it appears that you're trying to look for the file, if you don't find it to wait a bit, and try again, until $sleepElapsed is greater than $sleepTime. What you want here is a While or a Do/While loop. Here's what I'd do...
function FileCheck {
Param(
$fileName = '12345.zip',
$SleepTime = 300,
$sleepElapsed = 0,
$sleepInterval = 20
)
Download -ZipFileName $($fileName) -OutputDirectory $env:temp
Do{
Start-Sleep $sleepInterval
$sleepElapsed = $sleepElapsed + $sleepInterval
$file = Get-ChildItem -Path $env:temp -Filter "*$fileName"|Select -First 1
}While(!$file -and $sleepElapsed -le $sleepTime)
$file.FullName
}
That lets you define things like sleep settings at runtime if you want, or just let it default to what you were using, same with the file name. Then it downloads the file, and looks for it, pausing between attempts, until either it finds the file, or it runs out of time. Then it returns $file.FullName which is either the path to the file if it found one, or nothing if it didn't find a file.
Personally I'd have it return the file object, and just utilize the .FullName property if that's all I wanted later. Usually (not always, but usually) more info returned from a function is better than less info. Like what if the download fails and it's a zero byte file? Just returning only the path doesn't tell you that.

Powershell. Write event logs

I have a script which moves some files from a folder to the temp folder, archives them and cleans the temp folder at the end.
I want my script to also write information about it in the win-event log.
Here is my script:
Get-ChildItem C:\Users\Administrator\Desktop\test1\ | Where-Object {$_.LastWriteTime -lt "09/24/2018 09:00 PM"} | Move-Item -Destination C:\Users\Administrator\Desktop\data\
Compress-Archive -path C:\Users\Administrator\Desktop\data\ -CompressionLevel Optimal -DestinationPath C:\Users\Administrator\Desktop\data1\test.zip
Remove-Item C:\Users\Administrator\Desktop\data\*
I want to add code which will write an event for any error into the win-event log.
Per the comments, you can use Write-EventLog to write to the Windows Event Logs. If you want to write any errors that occur during those commands, then you probably want to use a Try..Catch to catch any errors and handle them:
Try {
$PrevEAP = $ErrorActionPreference
$ErrorActionPreference = 'Stop'
Get-ChildItem C:\Users\Administrator\Desktop\test1\ | Where-Object {$_.LastWriteTime -lt "09/24/2018 09:00 PM"} | Move-Item -Destination C:\Users\Administrator\Desktop\data\
Compress-Archive -path C:\Users\Administrator\Desktop\data\ -CompressionLevel Optimal -DestinationPath C:\Users\Administrator\Desktop\data1\test.zip
Remove-Item C:\Users\Administrator\Desktop\data\*
Catch {
Write-Error $_
$ErrorEvent = #{
LogName = 'Application'
Source = 'YourScript'
EventID = 123
EntryType = 'Information'
Message = $_
}
Write-EventLog #ErrorEvent
}
Finally {
$ErrorActionPreference = $PrevEAP
}
In order for an exception (error) to trigger a Try..Catch the exception needs to be terminating (vs non-terminating). You can force cmdlets to do terminating errors by setting the cmdlets -ErrorAction to Stop, or you can do this globally via the $ErrorActionPreference variable.
In the catch block, the error is held in the special variable: $_. So we can use Write-Error to still write it out to the console (if you want to) and then we're using Write-EventLog to write it into the Event Log.
Customise LogName, Source, EventID, Information etc. as per your needs. Note LogName needs to be one of the existing Logs and Entry Type needs to be one of the valid entry types (Information, Warning, Error).

PowerShell: try-catch not working

I have a PowerShell script that gets a list of file names from a file, searches a folder for the file names, archives them, and then does other stuff.
#make non-terminating errors behave like terminating errors (at script level)
$ErrorActionPreference = "Stop"
#set the folder that has the list and the files
$some_path = "D:\some_folder\"
$archive = "D:\archive\"
#set the list file name
$file_list = $some_path + "file_list.txt"
#get the files that I'm searching for from this list file
$files_to_retrieve = Select String -Path $file_list -Pattern "something" | Select-Object Line
#get the number of files for this search string
$n = $file_list.Length - 1
#seed the while loop counter
$i = 0
#while loop to archive and modify the files
While ($i -le $n)
{
#set the current file name
$current_file = $path + $files_to_retrieve[$i].Line
try
{
Copy-Item -Path $current_file -Destination $archive_path
}
catch
{
Write-Host ("file " + $files_to_retrieve[$i].Line + " not found")
}
$data = Get-Content $current_file
#do modifications here
}
The try-catch isn't working as expected. I have a file name in the file list that is not present in $some_path. I was expecting try-catch to stop the execution and do the Write-Host. Instead, it doesn't run the Write-Host and continues to the $data = Get-Content $current_file step, which is throwing a terminating error because the path doesn't exist for the missing file. How can I fix this?
Your first problem is the try/catch as you know. Taking a brief look at the documentation for about_Try_Catch_Finally you will see that..
Use Try, Catch, and Finally blocks to respond to or handle terminating
errors in scripts.
Your Copy-Item line is not throwing a terminating error. We fix that with the common parameter -ErrorAction
Copy-Item -Path $current_file -Destination $archive_path -ErrorAction Stop
So if there is a problem then the Catch block should be called. Assuming that was the real issue there.
You have another issue I think as well that might just be a typo. I see the following snippet more than once.
$file_list[$i].Line
Earlier you have declared $file_list as "D:\some_folder\file_list.txt" which is a string. I think what you meant to have is below. The above code would be null since string dont have a line property. But the return from Select-String can!
$files_to_retrieve[$i].Line