Powershell: how get exception line number when using ScriptBlock::Invoke - powershell-4.0

How can I get the line number where the exception is thrown in this example? The example below only gives me the line number where I invoke the script block (i.e. $sb.Invoke()). I want the line number where I throw "Error in FooBar".
function Foobar {
throw "Error in FooBar"
}
function Bar {
FooBar
}
function Foo {
Bar
}
try {
$sb = ${function:Foo}
$sb.Invoke()
}
catch {
$ErrorMessage = $_.Exception.Message
$line = $_.InvocationInfo.ScriptLineNumber
$script_name = $_.InvocationInfo.ScriptName
write-host "<--Error: Occurred on line $line in script $script_name."
Write-host "<--Error: $ErrorMessage"
}
UPDATE:
If I change:
$sb = ${function:Foo}
$sb.Invoke()
to this:
$func_info = get-command Foo
Invoke-Command $func_info.ScriptBlock
it works.

This seems like a limitation of Invoke()'s error handling - it's passing on the error message but throwing away the invocation info.
One workaround is to catch and rethrow inside the scriptblock itself, but with more information, e.g.
$sb = {
try {
Foo
}
catch {
throw "Error: $($_.Exception.Message). Stack trace:`n$($_.ScriptStackTrace)"
}
}
$sb.Invoke()
This outputs:
<--Error: Occurred on line 22 in script .
<--Error: Exception calling "Invoke" with "0" argument(s): "Error: Error in FooBar. Stack trace:
at Foobar, <No file>: line 2
at Bar, <No file>: line 6
at Foo, <No file>: line 10
at <ScriptBlock>, <No file>: line 16
at <ScriptBlock>, <No file>: line 22"

Related

How to stop power shell script on error?

I have bellow script
$ErrorActionPreference = "Stop";
while($true) {
try {
Write-Host "Step 1";
Dir C:\arts #Error
Write-Host "Step 2";
exit 0
break;
}
catch {
"Error in " + $_.InvocationInfo.ScriptName + " at line: " + $_.InvocationInfo.ScriptLineNumber + ", offset: " + $_.InvocationInfo.OffsetInLine + ".";
$Error
exit 1
break;
}
}
It stops on Dir C:\arts line and that is good for me. As I understood it happens cos I have line $ErrorActionPreference = "Stop"; at the beginning.
I also have some docker params
Param(
[Parameter(Mandatory=$True,ParameterSetName="Compose")]
[switch]$Compose,
[Parameter(Mandatory=$True,ParameterSetName="ComposeForDebug")]
[switch]$ComposeForDebug,
[Parameter(Mandatory=$True,ParameterSetName="StartDebugging")]
[switch]$StartDebugging,
[Parameter(Mandatory=$True,ParameterSetName="Build")]
[switch]$Build,
[Parameter(Mandatory=$True,ParameterSetName="Clean")]
[switch]$Clean,
[parameter(ParameterSetName="Compose")]
[Parameter(ParameterSetName="ComposeForDebug")]
[parameter(ParameterSetName="Build")]
[parameter(ParameterSetName="Clean")]
[ValidateNotNullOrEmpty()]
[String]$Environment = "Debug"
)
If I put $ErrorActionPreference = "Stop" line before docker params I will have error Cannot convert value "System.String" to type "System.Management.Automation.SwitchParameter". Boolean parameters accept only Boolean values and numbers, such as $True, $False, 1 or 0.
In case if I put $ErrorActionPreference = "Stop"; line after docker params, script is continued to run and that is not that I want.
I do not know what I need to do here, so I will be grateful for any help
$ErrorActionPreference doesn't work with command line utilities like docker as they don't throw exceptions in PowerShell. You would have to use returncode/errorlevel or parse the output to handle those type of errors. Useful automatic variables:
$?
Contains the execution status of the last operation. It contains
TRUE if the last operation succeeded and FALSE if it failed.
$LastExitCode
Contains the exit code of the last Windows-based program that was run. Same as %errorlevel% in cmd.
If you detect an error, you can throw an exception to stop the script or use something like exit to stop the script. Example:
function Test-Error {
$ErrorActionPreference = "Stop"
Write-Host Before
ping -n 1 123.123.123.123
#If last command was not successfull.
#You can also have checked $lastexitcode, output etc.
if($? -eq $false) {
#Throw terminating error
#throw "Error"
#Or since we've chosen to stop on non-terminating errors, we could use:
Write-Error -ErrorId $LASTEXITCODE -Message "Ping failed"
}
Write-Host After
}
Test-Error
Output:
Before
Pinging 123.123.123.123 with 32 bytes of data:
Request timed out.
Ping statistics for 123.123.123.123:
Packets: Sent = 1, Received = 0, Lost = 1 (100% loss),
Test-Error : Ping failed
At line:22 char:1
+ Test-Error
+ ~~~~~~~~~~
+ CategoryInfo : NotSpecified: (:) [Write-Error], WriteErrorException
+ FullyQualifiedErrorId : 1,Test-Error
If you're creating a advanced function, you could set the default ErrorAction for the scope of the cmdlet like this:
function Test-Error {
[CmdLetBinding()]
param(
$Name = "World"
)
#If -ErrorAction is not specified by the user, use Stop for the scope of the function
if(-not $MyInvocation.BoundParameters.ContainsKey("ErrorAction")) { $ErrorActionPreference = "Stop" }
"Hello $Name ! My ErrorAction is: $ErrorActionPreference"
}
PS > $ErrorActionPreference
Continue
PS > Test-Error -ErrorAction Ignore
Hello World ! My ErrorAction is: Ignore
PS > Test-Error
Hello World ! My ErrorAction is: Stop

PowerShell stop script catch from another script

I have a problem... I have two ps1 PowerShell scripts:
The first one catches an error like this:
$F = "Hola2"
try {
if ($F -contains "Hola") { write-host "ok" }
else {
write-error "Word is not Hola"
return
}
}
catch {
throw
break
}
The other one calls the first one like this:
$F1 = "Hola2"
try {
.\sub1.ps1
if ($F1 -contains "G") { write-host "ok" }
else {
write-error "Word is not Hola2"
return
}
}
catch {
throw
#Write-Warning "Caught: $_"
}
But when I execute the second script the error of the shows up but it doesn't stop, continues and then shows me the second error too. I want to stop at the first script error.
Can you help me?
Thanks!
Calling the script is what you're doing. It runs in its own context. The outer script will never know about its exception.
Instead, run the second script in the context of the outer by dot sourcing it:
. .\sub1.ps1
That first dot . acts as an include, and its as if that file's code is embedded right at that line.

wsadmin.sh reading multiline commands from stdin

Piped wsadmin can't run scripts with flow control, because in that mode a newline separates commands.
Simple example from http://www-01.ibm.com/support/knowledgecenter/SSEQTP_7.0.0/com.ibm.websphere.base.iseries.doc/info/iseries/ae/cxml_jacl.html?lang=en :
set numbers {1 3 5 7 11 13}
foreach num $numbers {
puts $num
}
output:
[wasuser#oktest-prod-app-2 ~]$ ${WC_WSADMIN:?} -f test.jacl
WASX7209I: Connected to process "dmgr" on node EmProdDmgrNode using SOAP connector; The type of process is: DeploymentManager
1
3
5
7
11
13
[wasuser#oktest-prod-app-2 ~]$ ${WC_WSADMIN:?} <test.jacl
WASX7209I: Connected to process "dmgr" on node EmProdDmgrNode using SOAP connector; The type of process is: DeploymentManager
WASX7029I: For help, enter: "$Help help"
wsadmin>1 3 5 7 11 13
wsadmin>WASX7015E: Exception running command: "foreach num $numbers {"; exception information:
com.ibm.bsf.BSFException: error while eval'ing Jacl expression:
wsadmin>WASX7015E: Exception running command: "puts $num"; exception information:
com.ibm.bsf.BSFException: error while eval'ing Jacl expression:
can't read "num": no such variable
while executing
"puts $num"
wsadmin>WASX7015E: Exception running command: "}"; exception information:
com.ibm.bsf.BSFException: error while eval'ing Jacl expression:
invalid command name "}"
while executing
"}"
My script is generated. Beside storing it in a temporary file is there a workaround? I know it's possible to do this:
foreach num $numbers { puts $num }
but what if there must be more than one command in the block?
Use a semicolon:
foreach num $numbers { puts $num; puts $num }
But you're probably better off writing the script to a temporary file.

debug.log in CakePHP without stacktrace

I log values of variables into debug.log using:
$var = 'Hello World';
debugger::log($var);
in my /app/tmp/logs/debug.log there is the whole stacktrace for that log:
2014-03-24 20:47:42 Debug:
UserController::create() - APP\Controller\UserController.php, line 21
ReflectionMethod::invokeArgs() - [internal], line ??
Controller::invokeAction() - CORE\Cake\Controller\Controller.php, line 490
Dispatcher::_invoke() - CORE\Cake\Routing\Dispatcher.php, line 185
Dispatcher::dispatch() - CORE\Cake\Routing\Dispatcher.php, line 160
[main] - APP\webroot\index.php, line 108
'Hello World'
I donĀ“t need the stacktrace, only the value of my variable.
One of the solutions is to use CakeLog::write function
CakeLog::write(LOG_DEBUG, "your message");
You will get the output to debug.log file:
2014-07-21 16:08:25 Debug:
your message
It is impossible to remove stack trace from debugger::log($var). Why? Here is its CakePHP code from Debugger.php:
public static function log($var, $level = LOG_DEBUG, $depth = 3) {
$source = self::trace(array('start' => 1)) . "\n";
CakeLog::write($level, "\n" . $source . self::exportVar($var, $depth));
}
Or edit the CakePHP source :)

How to evaluate a tclsh script?

tclsh is a shell containing the TCL commands.
The TCL uplevel command evaluates the given TCL script, but it fails to evaluate a tclsh script (which can contain bash commands).
How can I obtain an analogue of uplevel for the tclsh script?
Consider this TCL script:
# file main.tcl
proc prompt { } \
{
puts -nonewline stdout "MyShell > "
flush stdout
}
proc process { } \
{
catch { uplevel #0 [gets stdin] } got
if { $got ne "" } {
puts stderr $got
flush stderr
}
prompt
}
fileevent stdin readable process
prompt
while { true } { update; after 100 }
This is a kind of TCL shell, so when you type tclsh main.tcl it shows a prompt MyShell > and it acts like you are in interactive tclsh session. However, you are in non-interactive tclsh session, and everything you type is evaluated by the uplevel command. So here you can't use bash commands like you can do it int interactive tclsh session. E.g. you can't open vim right from the shell, also exec vim will not work.
What I want is to make MyShell > act like interactive tclsh session. The reason why I can't just use tclsh is the loop at the last line of main.tcl: I have to have that loop and everything has to happen in that loop. I also have to do some stuff at each iteration of that loop, so can use vwait.
Here is the solution.
I have found no better solution then to overwrite the ::unknown function.
# file main.tcl
proc ::unknown { args } \
{
variable ::tcl::UnknownPending
global auto_noexec auto_noload env tcl_interactive
global myshell_evaluation
if { [info exists myshell_evaluation] && $myshell_evaluation } {
set level #0
} else {
set level 1
}
# If the command word has the form "namespace inscope ns cmd"
# then concatenate its arguments onto the end and evaluate it.
set cmd [lindex $args 0]
if {[regexp "^:*namespace\[ \t\n\]+inscope" $cmd] && [llength $cmd] == 4} {
#return -code error "You need an {*}"
set arglist [lrange $args 1 end]
set ret [catch {uplevel $level ::$cmd $arglist} result opts]
dict unset opts -errorinfo
dict incr opts -level
return -options $opts $result
}
catch {set savedErrorInfo $::errorInfo}
catch {set savedErrorCode $::errorCode}
set name $cmd
if {![info exists auto_noload]} {
#
# Make sure we're not trying to load the same proc twice.
#
if {[info exists UnknownPending($name)]} {
return -code error "self-referential recursion in \"unknown\" for command \"$name\"";
}
set UnknownPending($name) pending;
set ret [catch {
auto_load $name [uplevel $level {::namespace current}]
} msg opts]
unset UnknownPending($name);
if {$ret != 0} {
dict append opts -errorinfo "\n (autoloading \"$name\")"
return -options $opts $msg
}
if {![array size UnknownPending]} {
unset UnknownPending
}
if {$msg} {
if {[info exists savedErrorCode]} {
set ::errorCode $savedErrorCode
} else {
unset -nocomplain ::errorCode
}
if {[info exists savedErrorInfo]} {
set ::errorInfo $savedErrorInfo
} else {
unset -nocomplain ::errorInfo
}
set code [catch {uplevel $level $args} msg opts]
if {$code == 1} {
#
# Compute stack trace contribution from the [uplevel].
# Note the dependence on how Tcl_AddErrorInfo, etc.
# construct the stack trace.
#
set errorInfo [dict get $opts -errorinfo]
set errorCode [dict get $opts -errorcode]
set cinfo $args
if {[string bytelength $cinfo] > 150} {
set cinfo [string range $cinfo 0 150]
while {[string bytelength $cinfo] > 150} {
set cinfo [string range $cinfo 0 end-1]
}
append cinfo ...
}
append cinfo "\"\n (\"uplevel\" body line 1)"
append cinfo "\n invoked from within"
append cinfo "\n\"uplevel $level \$args\""
#
# Try each possible form of the stack trace
# and trim the extra contribution from the matching case
#
set expect "$msg\n while executing\n\"$cinfo"
if {$errorInfo eq $expect} {
#
# The stack has only the eval from the expanded command
# Do not generate any stack trace here.
#
dict unset opts -errorinfo
dict incr opts -level
return -options $opts $msg
}
#
# Stack trace is nested, trim off just the contribution
# from the extra "eval" of $args due to the "catch" above.
#
set expect "\n invoked from within\n\"$cinfo"
set exlen [string length $expect]
set eilen [string length $errorInfo]
set i [expr {$eilen - $exlen - 1}]
set einfo [string range $errorInfo 0 $i]
#
# For now verify that $errorInfo consists of what we are about
# to return plus what we expected to trim off.
#
if {$errorInfo ne "$einfo$expect"} {
error "Tcl bug: unexpected stack trace in \"unknown\"" {} [list CORE UNKNOWN BADTRACE $einfo $expect $errorInfo]
}
return -code error -errorcode $errorCode -errorinfo $einfo $msg
} else {
dict incr opts -level
return -options $opts $msg
}
}
}
if { ( [info exists myshell_evaluation] && $myshell_evaluation ) || (([info level] == 1) && ([info script] eq "") && [info exists tcl_interactive] && $tcl_interactive) } {
if {![info exists auto_noexec]} {
set new [auto_execok $name]
if {$new ne ""} {
set redir ""
if {[namespace which -command console] eq ""} {
set redir ">&#stdout <#stdin"
}
uplevel $level [list ::catch [concat exec $redir $new [lrange $args 1 end]] ::tcl::UnknownResult ::tcl::UnknownOptions]
dict incr ::tcl::UnknownOptions -level
return -options $::tcl::UnknownOptions $::tcl::UnknownResult
}
}
if {$name eq "!!"} {
set newcmd [history event]
} elseif {[regexp {^!(.+)$} $name -> event]} {
set newcmd [history event $event]
} elseif {[regexp {^\^([^^]*)\^([^^]*)\^?$} $name -> old new]} {
set newcmd [history event -1]
catch {regsub -all -- $old $newcmd $new newcmd}
}
if {[info exists newcmd]} {
tclLog $newcmd
history change $newcmd 0
uplevel $level [list ::catch $newcmd ::tcl::UnknownResult ::tcl::UnknownOptions]
dict incr ::tcl::UnknownOptions -level
return -options $::tcl::UnknownOptions $::tcl::UnknownResult
}
set ret [catch {set candidates [info commands $name*]} msg]
if {$name eq "::"} {
set name ""
}
if {$ret != 0} {
dict append opts -errorinfo "\n (expanding command prefix \"$name\" in unknown)"
return -options $opts $msg
}
# Filter out bogus matches when $name contained
# a glob-special char [Bug 946952]
if {$name eq ""} {
# Handle empty $name separately due to strangeness
# in [string first] (See RFE 1243354)
set cmds $candidates
} else {
set cmds [list]
foreach x $candidates {
if {[string first $name $x] == 0} {
lappend cmds $x
}
}
}
if {[llength $cmds] == 1} {
uplevel $level [list ::catch [lreplace $args 0 0 [lindex $cmds 0]] ::tcl::UnknownResult ::tcl::UnknownOptions]
dict incr ::tcl::UnknownOptions -level
return -options $::tcl::UnknownOptions $::tcl::UnknownResult
}
if {[llength $cmds]} {
return -code error "ambiguous command name \"$name\": [lsort $cmds]"
}
}
return -code error "invalid command name \"$name\""
}
proc prompt { } \
{
puts -nonewline stdout "MyShell > "
flush stdout
}
proc process { } \
{
global myshell_evaluation
set myshell_evaluation true
catch { uplevel #0 [gets stdin] } got
set myshell_evaluation false
if { $got ne "" } {
puts stderr $got
flush stderr
}
prompt
}
fileevent stdin readable process
prompt
while { true } { update; after 100 }
The idea is to modify the ::unknown function so that it handles MyShell evaluations as the ones of tclsh interactive session.
This is an ugly solution, as I am fixing the code of ::unknown function which can be different for different systems and diferent versions of tcl.
Is there any solution which circumvents these issues?
uplevel does not only evaluate a script, but it evaluates it in the stack context of the caller of the instance where it's executed. It's a pretty advanced command which should be used when you define your own execution control structures, and OFC it's TCL specific - I find myself unable to imagine how a tclsh equivalent should work.
If you just want to evaluate another script, the proper TCL command would be eval. If that other script is tclsh, why don't you just open another tclsh?
The simplest answer, I think, would be to use the approach you're using; to rewrite the unknown command. Specifically, there is a line in it that checks to make sure the current context is
Not run in a script
Interactive
At the top level
If you replace that line:
if {([info level] == 1) && ([info script] eq "") && [info exists tcl_interactive] && $tcl_interactive} {
with something that just checks the level
if ([info level] == 1} {
you should get what you want.
Vaghan, you do have the right solution. Using ::unknown is how tclsh itself provides the interactive-shell-functionality you're talking about (invoking external binaries, etc). And you've lifted that same code and included it in your MyShell.
But, if I understand your concerns about it being an "ugly solution", you'd rather not reset ::unknown ?
In which case, why not just append the additional functionality you want to the end of the pre-existing ::unknown's body (or prepend it - you choose)
If you search on the Tcl'ers wiki for "let unknown know", you'd see a simple proc which demonstrates this. It prepends new code to the existing ::unknown, so you can keep adding additional "fallback code" as you go along.
(apologies if I've misunderstood why you feel your solution is "ugly")
Instead of changing the unknown proc, I suggest that you make the changes to evaluate the expresion
if {([info level] == 1) && ([info script] eq "") && [info exists tcl_interactive] && $tcl_interactive} {
to true.
info level: call your stuff with uplevel #0 $code
info script: call info script {} to set it to an empty value
tcl_interactive. Simple: set ::tcl_interactive 1
so your code would be
proc prompt { } {
puts -nonewline stdout "MyShell > "
flush stdout
}
proc process { } {
catch { uplevel #0 [gets stdin] } got
if { $got ne "" } {
puts stderr $got
flush stderr
}
prompt
}
fileevent stdin readable process
set tcl_interactive 1
info script {}
prompt
vwait forever

Resources