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

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.

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] }

Powershell Throw Causing Variables to Clear?

My PowerShell script just checks multiple servers to make sure the input* and output* directories are clear of any files.
I'm simply trying to output to console the results of a GCI call prior to throwing an error message. However, when I uncomment the "throw" line, the $inputFiles and $outputFiles no longer output to the console. Below is the code:
$allServers = #(
"server1.com",
"server2.com")
foreach ($server in $allServers) {
$inputFiles = Get-ChildItem -Path "\\$server\C$\jobs\statements\input*\" -Recurse | Where-Object {! $_.PSIsContainer } | Select FullName
$outputFiles = Get-ChildItem -Path "\\$server\C$\jobs\statements\output*\" -Recurse | Where-Object {! $_.PSIsContainer } | Select FullName
if ($inputFiles -eq $NULL -and $outputFiles -eq $NULL) {
Write-Host "Environment is ready for statement processing."
}
else {
Write-Host "Environment is NOT ready for statement processing."
Write-Host "The following files exist in input/output: `n"
$inputFiles
$outputFiles
#Throw "Files exist in input/output. See above for details."
}
}
Below is the console output:
Environment is NOT ready for statement processing.
The following files exist in input/output:
Environment is NOT ready for statement processing.
The following files exist in input/output:
FullName
--------
\\server1.com\C$\jobs\statements\input\asdasd.txt
\\server1.com\C$\jobs\statements\input_254\asdasd.txt
\\server1.com\C$\jobs\statements\input_test\asdasd.txt
\\server2.com\C$\jobs\statements\input\CUSSTAT10302021.245
\\server2.com\C$\jobs\statements\input\CUSSTAT11312021
\\server2.com\C$\jobs\statements\input\CUSSTAT11312021.zip
And below is the console output when I uncomment the "throw" line:
Environment is NOT ready for statement processing.
The following files exist in input/output:
Files exist in input/output. See above for details.
At C:\jobs\statements\bin\Statements-EnvironmentCheck.ps1:47 char:9
+ Throw "Files exist in input/output. See above for details."
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : OperationStopped: (Files exist in ...ve for details.:String) [], RuntimeException
+ FullyQualifiedErrorId : Files exist in input/output. See above for details.
I know I have some error output cleanup to perform in order to include all the servers that might have files present, but please ignore that for now.
What you're experiencing is explained in this answer and this answer, basically you need to implement Out-Host \ Out-Default:
$inputFiles, $outputFiles | Out-Host # Should fix the problem
# possibly `throw` might require this too
throw "Files exist in input/output. See above for details." | Out-Host
However, I feel is worth showing you a better way to approach your code, returning a unified array of objects which you can filter, sort and export.
$allServers = #(
"server1.com"
"server2.com"
)
$result = foreach ($server in $allServers) {
# use `-File` instead of `! $_.PSIsContainer`
$out = #{
in = Get-ChildItem "\\$server\C$\jobs\statements\input*\" -Recurse -File
out = Get-ChildItem "\\$server\C$\jobs\statements\output*\" -Recurse -File
}
# if $out['in'] and $out['out'] are `$null`, Ready is `$true`
[pscustomobject]#{
Ready = -not($out['in'] -or $out['out'])
Server = $server
Files = $out
}
}
Now, if you want to see which servers are Ready (no files in input and output):
$result.where{ $_.Ready }
And if you want to see which servers are not Ready, and have a list of the files:
$result.where{ -not $_.Ready }.foreach{
foreach($file in $_.Files.PSBase.Values.FullName) {
[pscustomobject]#{
Server = $_.Server
Files = $file
}
}
}

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) Log Exceptions and Continue (Active Directory Module)

I'm using New-ADUser and Add-ADGroupMember
If the user already exists or is already in the group then the functions throw exceptions (which are expected and not a problem).
How do I log the exceptions to file and keep going?
Redirection is not working - the exceptions always go to the
console.
-ErrorAction is not working - exceptions still go to the console
Try / Catch works, but execution stops and the rest of the commands don't run
I can do a Try / Catch for every single statment, but that seems
ridiculous
You can combine -ErrorAction SilentlyContinue with -ErrorVariable:
$e = $null
New-ADUser iExist -ErrorAction SilentlyContinue -ErrorVariable e
$e # contains the error
You can also use the built in $Error variable, which is a circular buffer holding all the errors.
$ErrorPreference = SilentlyContinue # I don't like this personally
New-ADUser iExist
Add-ADGroupMember iExist iForgotTheParameters
$Error[0] # The Add-ADGroupMember error
$Error[1] # The New-ADUser error
So you could set your $ErrorPreference, do a bunch of commands, and at the end of it all, do something like $Error | Out-File -Path errors.txt.
Have a look at PowerShell Error Handling and Why You Should Care for more ideas.
The simplest way to accomplish this is probably using the trap construct:
function Test-Trap{
trap {
$_ | Out-String | Out-File C:\path\to\errors.txt -Append
}
Get-ADUser -NoSuchParam "argument"
Write-Host "Show must go on"
nonexistingcommand
Write-Host "Still executing"
}
When you call Test-Trap, you'll see that after the error has been written to the console, the trap is executed, and the rest of the execution flow is resumed:
And the error record output as it would normally appear on screen (courtesy of Out-String) has been saved to the file:
You could add cool features like timestamps and stack traces to your trap:
function Test-Trap{
trap {
$LogPath = "C:\path\to\errors.txt"
$ErrorCount = $ErrorCount + 1
$("[Error {0} trapped {1}]:" -f $ErrorCount,(Get-Date -Format "dd/MM/yyyy HH:mm:ss.fff")) | Out-File $LogPath -Append
$_ | Out-String | Out-File $LogPath -Append
if(($st = $_.Exception.Stacktrace)){ $st |Out-File $LogPath -Append }
$("[Error {0} logged]" -f $ErrorCount)| Out-File $LogPath -Append
}
Provoke-Error -NoSuchParam muhahaha
}

Breaking out of a foreach loop

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.