I have been trying to write a script in powershell that renames a file if it does not get transferred correctly through WinSCP. The script looks something like this:
# Variables
$error = 0
$currentFile = "test"
# Functions
function upload-files {
param(
$WinScpSession,
$LocalDirectory,
$FileType,
$RemoteDirectory
)
get-childitem $LocalDirectory -filter $FileType |
foreach-object {
# $_ gets the current item of the foreach loop.
write-output "Sending $_..."
$currentFile = "$($LocalDirectory)$($_)"
upload-file -WinScpSession $session -LocalDirectory "$($LocalDirectory)$($_)" -RemoteDirectory "$RemoteDirectory"
}
}
try
{
# Upload files
upload-files -WinScpSession $Session -LocalDirectory [PathToLocalDirectory] -FileType [FileType] -RemoteDirectory [PathToRemoteDirectoy]
}
catch
{
Write-Host "Error: $($_.Exception.Message)"
write-output $currentFile
$errorMoveLocation = copy-item -path "$currentFile" -destination "$currentFile.err" -passthru
Write-Host "Error File has been saved as $errorMoveLocation"
$error = 1
}
I have removed the paths for ease of reading and some WinSCP lines that don't have anything to do with the issue.
When adding some script-breaking code after the upload-file function, it would go to the catch statement where I expected the $currentFile variable to be the $($LocalDirectory)$($_) since it was caught after setting the variable again. However, the actual value of the variable is the original "test" value that it was initiated with. I have tried changing the scope of $currentFile to both script, and global the same issue still happens. I am still relatively new at powershell and this is beyond my expertise. Any help would be greatly appreciated, thanks!
As written, there is indeed a variable scoping problem with your code:
function
s (and script files) run in a child scope of the caller, so $currentFile = "$($LocalDirectory)$($_)"
inside your upload-files
creates a local $currentFile
variable, that the caller doesn't know about, and which goes out of scope when existing the function.I have tried changing the scope of
$currentFile
to both$script:
, and$global:
the same issue still happens
Using these scopes should work, given that you're explicitly specifying the target scope (and in the top-level scope, you don't even need the $script:
scope specifier; also, $global:
variables are best avoided, as they affect the global session state and linger after your script exits; while $script:
is better than $global:
, it is best to avoid cross-scope variable modification altogether.)
The only explanation for $script:currentFile = ...
not modifying the value in the script / caller's scope would be if get-childitem $LocalDirectory -filter $FileType
produced no output, in which case the ForEach-Object
script block is never entered.
Also note that at least in your upload-files
function there is no code that would generate a terminating error, which is the prerequisite for being able to use a try {...} catch { ... }
statement.
try {...} catch { ... }
statement:Update:
upload-file
(which isn't shown in the question) can generate a (statement-)terminating error, as a result of an exception thrown by a (WinSCP) .NET method call. Such a statement-terminating error is caught by an enclosing try { ... } catch { ... }
statement, and control is instantly transferred to the catch
block.try { ... } catch { ... }
, enclose the .NET method call too in try {...} catch { ... }
, and translate the exception into a non-terminating error (try { ... } catch { $_ | Write-Error }
), and combine that with method (c) below. (Of course, if you've ensured that no terminating errors can occur at all, you may dispense with try { ... } catch { ... }
altogether.)Your upload-files
function only emits non-terminating errors, whereas try
/ catch
only catches terminating errors.
To ensure that upload-files
reports terminating errors, you have two options:
(a) Before calling it, (temporarily) set the $ErrorActionPreference
preference variable to 'Stop'
, which promotes non-terminating errors to terminating ones.
(b) Make your function an advanced (cmdlet-like) one, which makes it support the -ErrorAction
common parameter , allowing you to pass -ErrorAction Stop
on a per-call basis to achieve the same effect.
param(...)
block with a [CmdletBinding()]
attribute is enough to make it an advanced one, and so is using a parameter-individual [Parameter()]
attribute.Alternatively, (c) - assuming you've made your function an advanced one - you can use the common -ErrorVariable
parameter to capture all the non-terminating errors in a self-chosen variable (e.g., -ErrorVariable errs
). If you find this variable non-empty, you can then use throw
to emit a (script-)terminating error that catch
will handle (e.g., if ($errs) { throw $errs }
Note that:
(a) and (b) by design abort processing (and transfer control to the catch
block) once the first error occurs, whereas
(c) potentially runs the function call to completion, allowing potentially multiple, non-terminating errors to occur.
Implementation of (a):
$oldErrorActionPref = $ErrorActionPreference
try
{
# Treat all errors as terminating
$ErrorActionPreference = 'Stop'
upload-files -WinScpSession $Session -LocalDirectory [PathToLocalDirectory] -FileType [FileType] -RemoteDirectory [PathToRemoteDirectoy]
}
catch
{
Write-Host "Error: $($_.Exception.Message)"
# ...
}
finally {
$ErrorActionPreference = $oldErrorActionPref
}
Implementation of (b):
# ...
function upload-files {
[CmdletBinding()] # NOTE: This makes your function an *advanced* one.
param(
$WinScpSession,
$LocalDirectory,
$FileType,
$RemoteDirectory
)
# ...
}
try
{
# Pass -ErrorAction Stop to treat all errors as terminating
upload-files -ErrorAction Stop -WinScpSession $Session -LocalDirectory [PathToLocalDirectory] -FileType [FileType] -RemoteDirectory [PathToRemoteDirectoy]
}
catch
{
Write-Host "Error: $($_.Exception.Message)"
# ...
}
Implementation of (c):
# ...
function upload-files {
[CmdletBinding()] # NOTE: This makes your function an *advanced* one.
param(
$WinScpSession,
$LocalDirectory,
$FileType,
$RemoteDirectory
)
# ...
}
try
{
# Pass -ErrorVariable errs to collect all non-terminating errors
# that occur, if any.
upload-files -ErrorVariable errs -WinScpSession $Session -LocalDirectory [PathToLocalDirectory] -FileType [FileType] -RemoteDirectory [PathToRemoteDirectoy]
# If errors occurred, use `throw` to generate a (script-)terminating
# error that triggers the `catch` block
if ($errs) { throw $errs }
}
catch
{
Write-Host "Error: $($_.Exception.Message)"
# ...
}
See also:
The about_Try_Catch_Finally
help topic.
A description of the fundamental error types in the context of guidance for command authors on when to emit a terminating vs. a non-terminating error: this answer.
A comprehensive overview of PowerShell's surprisingly complex error handling: this GitHub docs issue.