elvish/eval/exception.go
2017-08-30 20:52:27 +02:00

233 lines
5.2 KiB
Go

package eval
import (
"bytes"
"fmt"
"strconv"
"strings"
"syscall"
"unsafe"
"github.com/elves/elvish/parse"
"github.com/elves/elvish/util"
"github.com/xiaq/persistent/hash"
)
// Exception represents an elvish exception. It is both a Value accessible to
// elvishscript, and the type of error returned by public facing evaluation
// methods like (*Evaler)PEval.
type Exception struct {
Cause error
Traceback *util.SourceContext
}
// OK is a pointer to the zero value of Exception, representing the absence of
// exception.
var OK = &Exception{}
func (exc *Exception) Error() string {
return exc.Cause.Error()
}
func (exc *Exception) Pprint(indent string) string {
buf := new(bytes.Buffer)
// Error message
var msg string
if pprinter, ok := exc.Cause.(util.Pprinter); ok {
msg = pprinter.Pprint(indent)
} else {
msg = "\033[31;1m" + exc.Cause.Error() + "\033[m"
}
fmt.Fprintf(buf, "Exception: %s\n", msg)
buf.WriteString(indent + "Traceback:")
for tb := exc.Traceback; tb != nil; tb = tb.Next {
buf.WriteString("\n" + indent + " ")
tb.Pprint(buf, indent+" ")
}
if pipeExcs, ok := exc.Cause.(PipelineError); ok {
buf.WriteString("\n" + indent + "Caused by:")
for _, e := range pipeExcs.Errors {
if e == OK {
continue
}
buf.WriteString("\n" + indent + " " + e.Pprint(indent+" "))
}
}
return buf.String()
}
func (exc *Exception) Kind() string {
return "exception"
}
func (exc *Exception) Repr(indent int) string {
if exc.Cause == nil {
return "$ok"
}
if r, ok := exc.Cause.(Reprer); ok {
return r.Repr(indent)
}
return "?(fail " + parse.Quote(exc.Cause.Error()) + ")"
}
// Equal compares by identity.
func (exc *Exception) Equal(rhs interface{}) bool {
return exc == rhs
}
func (exc *Exception) Hash() uint32 {
return hash.Pointer(unsafe.Pointer(exc))
}
func (exc *Exception) Bool() bool {
return exc.Cause == nil
}
// PipelineError represents the errors of pipelines, in which multiple commands
// may error.
type PipelineError struct {
Errors []*Exception
}
func (pe PipelineError) Repr(indent int) string {
// TODO Make a more generalized ListReprBuilder and use it here.
b := new(bytes.Buffer)
b.WriteString("?(multi-error")
elemIndent := indent + len("?(multi-error ")
for _, e := range pe.Errors {
if indent > 0 {
b.WriteString("\n" + strings.Repeat(" ", elemIndent))
} else {
b.WriteString(" ")
}
b.WriteString(e.Repr(elemIndent))
}
b.WriteString(")")
return b.String()
}
func (pe PipelineError) Error() string {
b := new(bytes.Buffer)
b.WriteString("(")
for i, e := range pe.Errors {
if i > 0 {
b.WriteString(" | ")
}
if e == nil || e.Cause == nil {
b.WriteString("<nil>")
} else {
b.WriteString(e.Error())
}
}
b.WriteString(")")
return b.String()
}
// ComposeExceptionsFromPipeline takes a slice of Exception pointers and
// composes a suitable error. If all elements of the slice are either nil or OK,
// a nil is returned. If there is exactly non-nil non-OK Exception, it is
// returned. Otherwise, a PipelineError built from the slice is returned, with
// nil items turned into OK's for easier access from elvishscript.
func ComposeExceptionsFromPipeline(excs []*Exception) error {
newexcs := make([]*Exception, len(excs))
notOK, lastNotOK := 0, 0
for i, e := range excs {
if e == nil {
newexcs[i] = OK
} else {
newexcs[i] = e
if e.Cause != nil {
notOK++
lastNotOK = i
}
}
}
switch notOK {
case 0:
return nil
case 1:
return newexcs[lastNotOK]
default:
return PipelineError{newexcs}
}
}
// Flow is a special type of error used for control flows.
type Flow uint
// Control flows.
const (
Return Flow = iota
Break
Continue
)
var flowNames = [...]string{
"return", "break", "continue",
}
func (f Flow) Repr(int) string {
return "?(" + f.Error() + ")"
}
func (f Flow) Error() string {
if f >= Flow(len(flowNames)) {
return fmt.Sprintf("!(BAD FLOW: %v)", f)
}
return flowNames[f]
}
func (f Flow) Pprint(string) string {
return "\033[33;1m" + f.Error() + "\033[m"
}
// ExternalCmdExit contains the exit status of external commands. If the
// command was stopped rather than terminated, the Pid field contains the pid
// of the process.
type ExternalCmdExit struct {
syscall.WaitStatus
CmdName string
Pid int
}
func NewExternalCmdExit(name string, ws syscall.WaitStatus, pid int) error {
if ws.Exited() && ws.ExitStatus() == 0 {
return nil
}
if !ws.Stopped() {
pid = 0
}
return ExternalCmdExit{ws, name, pid}
}
func FakeExternalCmdExit(name string, exit int, sig syscall.Signal) ExternalCmdExit {
return ExternalCmdExit{syscall.WaitStatus(exit<<8 + int(sig)), name, 0}
}
func (exit ExternalCmdExit) Error() string {
ws := exit.WaitStatus
quotedName := parse.Quote(exit.CmdName)
switch {
case ws.Exited():
return quotedName + " exited with " + strconv.Itoa(ws.ExitStatus())
case ws.Signaled():
msg := quotedName + " killed by signal " + ws.Signal().String()
if ws.CoreDump() {
msg += " (core dumped)"
}
return msg
case ws.Stopped():
msg := quotedName + " stopped by signal " + fmt.Sprintf("%s (pid=%d)", ws.StopSignal(), exit.Pid)
trap := ws.TrapCause()
if trap != -1 {
msg += fmt.Sprintf(" (trapped %v)", trap)
}
return msg
default:
return fmt.Sprint(quotedName, " has unknown WaitStatus ", ws)
}
}