Repository: rsteube/carapace
Pull request #626 (2022-11-28 07:46:31)

added messages struct
related #584


Patch:
diff --git a/action.go b/action.go
index dd7f12a..ecfdfa1 100644
--- a/action.go
+++ b/action.go
@@ -2,7 +2,7 @@ package carapace
 
 import (
 	"fmt"
-	"regexp"
+	"os"
 	"runtime"
 	"time"
 
@@ -15,9 +15,7 @@ import (
 type Action struct {
 	rawValues []common.RawValue
 	callback  CompletionCallback
-	nospace   common.SuffixMatcher
-	skipcache bool
-	usage     string
+	meta      common.Meta
 }
 
 // ActionMap maps Actions to an identifier.
@@ -37,7 +35,7 @@ func (a Action) Cache(timeout time.Duration, keys ...pkgcache.Key) Action {
 					return actionRawValues(rawValues...)
 				}
 				invokedAction := (Action{callback: cachedCallback}).Invoke(c)
-				if !invokedAction.skipcache {
+				if invokedAction.meta.Messages.IsEmpty() {
 					_ = cache.Write(cacheFile, invokedAction.rawValues)
 				}
 				return invokedAction.ToA()
@@ -67,7 +65,9 @@ func (a Action) nestedAction(c Context, maxDepth int) Action {
 		return ActionMessage("maximum recursion depth exceeded")
 	}
 	if a.rawValues == nil && a.callback != nil {
-		return a.callback(c).nestedAction(c, maxDepth-1).noSpace(string(a.nospace)).skipCache(a.skipcache).withUsage(a.usage)
+		result := a.callback(c).nestedAction(c, maxDepth-1).noSpace(string(a.meta.Nospace)).withUsage(a.meta.Usage)
+		result.meta.Messages.Merge(a.meta.Messages)
+		return result
 	}
 	return a
 }
@@ -127,9 +127,7 @@ func (a Action) StyleF(f func(s string) string) Action {
 	return ActionCallback(func(c Context) Action {
 		invoked := a.Invoke(c)
 		for index, v := range invoked.rawValues {
-			if !v.IsMessage() {
-				invoked.rawValues[index].Style = f(v.Value)
-			}
+			invoked.rawValues[index].Style = f(v.Value)
 		}
 		return invoked.ToA()
 	})
@@ -153,9 +151,7 @@ func (a Action) TagF(f func(value string) string) Action {
 	return ActionCallback(func(c Context) Action {
 		invoked := a.Invoke(c)
 		for index, v := range invoked.rawValues {
-			if !v.IsMessage() {
-				invoked.rawValues[index].Tag = f(v.Value)
-			}
+			invoked.rawValues[index].Tag = f(v.Value)
 		}
 		return invoked.ToA()
 	})
@@ -168,6 +164,11 @@ func (a Action) Chdir(dir string) Action {
 		if err != nil {
 			return ActionMessage(err.Error())
 		}
+		if info, err := os.Stat(abs); err != nil {
+			return ActionMessage(err.Error())
+		} else if !info.IsDir() {
+			return ActionMessage("not a directory: %v", abs)
+		}
 		c.Dir = abs
 		return a.Invoke(c).ToA()
 	})
@@ -177,30 +178,8 @@ func (a Action) Chdir(dir string) Action {
 func (a Action) Suppress(expr ...string) Action {
 	return ActionCallback(func(c Context) Action {
 		invoked := a.Invoke(c)
-		filter := false
-		for _, rawValue := range invoked.rawValues {
-			if rawValue.IsMessage() && rawValue.Description != "" {
-				for _, e := range expr {
-					r, err := regexp.Compile(e)
-					if err != nil {
-						return ActionMessage(err.Error())
-					}
-					if r.MatchString(rawValue.Description) {
-						filter = true
-						break
-					}
-				}
-			}
-		}
-
-		if filter {
-			filtered := make([]common.RawValue, 0)
-			for _, r := range invoked.rawValues {
-				if !r.IsMessage() {
-					filtered = append(filtered, r)
-				}
-			}
-			invoked.rawValues = filtered
+		if err := invoked.meta.Messages.Suppress(expr...); err != nil {
+			return ActionMessage(err.Error())
 		}
 		return invoked.ToA()
 	})
@@ -221,18 +200,13 @@ func (a Action) UniqueList(divider string) Action {
 }
 
 func (a Action) noSpace(suffixes string) Action {
-	a.nospace = a.nospace.Add(suffixes)
-	return a
-}
-
-func (a Action) skipCache(state bool) Action {
-	a.skipcache = a.skipcache || state
+	a.meta.Nospace = a.meta.Nospace.Add(suffixes)
 	return a
 }
 
 func (a Action) withUsage(usage string) Action {
 	if usage != "" {
-		a.usage = usage
+		a.meta.Usage = usage
 	}
 	return a
 }
diff --git a/compat.go b/compat.go
index 912cee2..f20d17c 100644
--- a/compat.go
+++ b/compat.go
@@ -40,7 +40,7 @@ func cobraValuesFor(action InvokedAction) []string {
 
 func cobraDirectiveFor(action InvokedAction) cobra.ShellCompDirective {
 	directive := cobra.ShellCompDirectiveNoFileComp
-	if action.nospace != "" {
+	if action.meta.Nospace != "" {
 		directive = directive | cobra.ShellCompDirectiveNoSpace
 	}
 	return directive
diff --git a/defaultActions.go b/defaultActions.go
index 3f18c9e..f701fba 100644
--- a/defaultActions.go
+++ b/defaultActions.go
@@ -63,8 +63,9 @@ func ActionImport(output []byte) Action {
 			return ActionMessage(err.Error())
 		}
 		a := actionRawValues(e.RawValues...)
-		a.nospace = e.Nospace
-		a.usage = e.Usage
+		a.meta.Nospace = e.Nospace
+		a.meta.Usage = e.Usage
+		a.meta.Messages = e.Messages
 		return a
 	})
 }
@@ -179,17 +180,14 @@ func ActionStyledValuesDescribed(values ...string) Action {
 }
 
 // ActionMessage displays a help messages in places where no completions can be generated.
-func ActionMessage(msg string, a ...interface{}) Action {
+func ActionMessage(msg string, args ...interface{}) Action {
 	return ActionCallback(func(c Context) Action {
-		if len(a) > 0 {
-			msg = fmt.Sprintf(msg, a...)
+		if len(args) > 0 {
+			msg = fmt.Sprintf(msg, args...)
 		}
-		m := ActionStyledValuesDescribed("_", "", style.Default, "ERR", msg, style.Carapace.Error).Invoke(c)
-		for index := range m.rawValues {
-			m.rawValues[index].Tag = "messages"
-		}
-		return m.Prefix(c.CallbackValue).ToA(). // needs to be prefixed with current callback value to not be filtered out
-							noSpace("*").skipCache(true)
+		a := ActionValues().NoSpace()
+		a.meta.Messages.Add(msg)
+		return a
 	})
 }
 
diff --git a/example/cmd/action.go b/example/cmd/action.go
index 36080d1..cab47f1 100644
--- a/example/cmd/action.go
+++ b/example/cmd/action.go
@@ -26,6 +26,7 @@ func init() {
 	actionCmd.Flags().String("files-filtered", "", "ActionFiles(\".md\", \"go.mod\", \"go.sum\")")
 	actionCmd.Flags().String("import", "", "ActionImport()")
 	actionCmd.Flags().String("message", "", "ActionMessage()")
+	actionCmd.Flags().String("message-multiple", "", "ActionMessage()")
 	actionCmd.Flags().String("multiparts", "", "ActionMultiParts()")
 	actionCmd.Flags().String("multiparts-nested", "", "ActionMultiParts(...ActionMultiParts...)")
 	actionCmd.Flags().String("styled-values", "", "ActionStyledValues()")
@@ -68,6 +69,12 @@ func init() {
 }
 		`)),
 		"message": carapace.ActionMessage("example message"),
+		"message-multiple": carapace.Batch(
+			carapace.ActionMessage("first message"),
+			carapace.ActionMessage("second message"),
+			carapace.ActionMessage("third message"),
+			carapace.ActionValues("one", "two", "three"),
+		).ToA(),
 		"multiparts": carapace.ActionMultiParts(":", func(c carapace.Context) carapace.Action {
 			switch len(c.Parts) {
 			case 0:
diff --git a/internal/common/message.go b/internal/common/message.go
new file mode 100644
index 0000000..46f420a
--- /dev/null
+++ b/internal/common/message.go
@@ -0,0 +1,144 @@
+package common
+
+import (
+	"encoding/json"
+	"fmt"
+	"regexp"
+	"sort"
+	"strings"
+
+	"github.com/rsteube/carapace/pkg/style"
+)
+
+type Messages struct {
+	messages map[string]bool
+}
+
+func (m *Messages) init() {
+	if m.messages == nil {
+		m.messages = make(map[string]bool)
+	}
+}
+
+func (m Messages) IsEmpty() bool {
+	// TODO replacement for Action.skipCache - does this need to consider suppressed messages or is this fine?
+	return len(m.messages) == 0
+}
+
+func (m *Messages) Add(s string) {
+	m.init()
+	m.messages[s] = true
+}
+
+func (m Messages) Get() []string {
+	messages := make([]string, 0)
+	for message := range m.messages {
+		messages = append(messages, message)
+	}
+	sort.Strings(messages)
+	return messages
+}
+
+func (m *Messages) Suppress(expr ...string) error {
+	m.init()
+
+	for _, e := range expr {
+		r, err := regexp.Compile(e)
+		if err != nil {
+			return err
+		}
+
+		for key := range m.messages {
+			if r.MatchString(key) {
+				delete(m.messages, key)
+			}
+		}
+	}
+	return nil
+}
+
+func (m *Messages) Merge(other Messages) {
+	if other.messages == nil {
+		return
+	}
+
+	for key := range other.messages {
+		m.Add(key)
+	}
+}
+
+func (m Messages) Integrate(values RawValues, prefix string) RawValues {
+	m.init()
+
+	if len(m.messages) == 0 {
+		return values
+	}
+
+	sorted := make([]string, 0)
+	for message := range m.messages {
+		sorted = append(sorted, message)
+	}
+	sort.Strings(sorted)
+
+	switch {
+	case strings.HasSuffix(prefix, "ERR"):
+		prefix = strings.TrimSuffix(prefix, "ERR")
+	case strings.HasSuffix(prefix, "ER"):
+		prefix = strings.TrimSuffix(prefix, "ER")
+	case strings.HasSuffix(prefix, "E"):
+		prefix = strings.TrimSuffix(prefix, "E")
+	}
+
+	i := 0
+	for _, message := range sorted {
+		value := prefix + "ERR"
+		for {
+			if i > 0 {
+				value = fmt.Sprintf("%vERR%v", prefix, i)
+			}
+			i += 1
+
+			if !values.contains(value) {
+				break
+			}
+		}
+
+		values = append(values, RawValue{
+			Value:       value,
+			Display:     value,
+			Description: message,
+			Style:       style.Carapace.Error,
+		})
+	}
+
+	if len(values) == 1 {
+		values = append(values, RawValue{
+			Value:       prefix + "_",
+			Display:     "_",
+			Description: "",
+			Style:       style.Default,
+		})
+	}
+	sort.Sort(ByDisplay(values))
+	return values
+}
+
+func (m Messages) MarshalJSON() ([]byte, error) {
+	var result []string = make([]string, 0, len(m.messages))
+	for key := range m.messages {
+		result = append(result, key)
+	}
+	sort.Strings(result)
+	return json.Marshal(&result)
+}
+
+func (m *Messages) UnmarshalJSON(data []byte) (err error) {
+	var result []string
+	if err = json.Unmarshal(data, &result); err != nil {
+		return err
+	}
+	for _, item := range result {
+		m.Add(item)
+	}
+	return
+}
diff --git a/internal/common/meta.go b/internal/common/meta.go
new file mode 100644
index 0000000..5864e47
--- /dev/null
+++ b/internal/common/meta.go
@@ -0,0 +1,7 @@
+package common
+
+type Meta struct {
+	Messages Messages
+	Nospace  SuffixMatcher
+	Usage    string
+}
diff --git a/internal/common/value.go b/internal/common/value.go
index 03cf058..39234b1 100644
--- a/internal/common/value.go
+++ b/internal/common/value.go
@@ -16,11 +16,6 @@ type RawValue struct {
 	Tag         string `json:",omitempty"`
 }
 
-// IsMessage checks if the value is a message (ActionMessage)
-func (r RawValue) IsMessage() bool {
-	return r.Tag == "messages"
-}
-
 // TrimmedDescription returns the trimmed description.
 func (r RawValue) TrimmedDescription() string {
 	maxLength := 80
@@ -44,6 +39,30 @@ func RawValuesFrom(values ...string) RawValues {
 	return rawValues
 }
 
+func (r RawValues) contains(s string) bool {
+	for _, value := range r {
+		if value.Value == s {
+			return true
+		}
+	}
+	return false
+}
+
+// Filter filters values.
+func (r RawValues) Filter(values ...string) RawValues {
+	toremove := make(map[string]bool)
+	for _, v := range values {
+		toremove[v] = true
+	}
+	filtered := make([]RawValue, 0)
+	for _, rawValue := range r {
+		if _, ok := toremove[rawValue.Value]; !ok {
+			filtered = append(filtered, rawValue)
+		}
+	}
+	return filtered
+}
+
 // FilterPrefix filters values with given prefix.
 func (r RawValues) FilterPrefix(prefix string) RawValues {
 	filtered := make(RawValues, 0)
diff --git a/internal/shell/bash/action.go b/internal/shell/bash/action.go
index 1a5c233..db2238a 100644
--- a/internal/shell/bash/action.go
+++ b/internal/shell/bash/action.go
@@ -68,18 +68,16 @@ func commonValuePrefix(values ...common.RawValue) (prefix string) {
 	return
 }
 
-const nospaceIndicator = "\001"
-
 // ActionRawValues formats values for bash.
-func ActionRawValues(currentWord string, usage string, nospace common.SuffixMatcher, values common.RawValues) string {
+func ActionRawValues(currentWord string, meta common.Meta, values common.RawValues) string {
 	lastSegment := currentWord // last segment of currentWord split by COMP_WORDBREAKS
 
-	for _, r := range values {
+	for valueIndex, value := range values {
 		// TODO optimize
 		if wordbreaks, ok := os.LookupEnv("COMP_WORDBREAKS"); ok {
 			wordbreaks = strings.Replace(wordbreaks, " ", "", -1)
 			if index := strings.LastIndexAny(currentWord, wordbreaks); index != -1 {
-				r.Value = strings.TrimPrefix(r.Value, currentWord[:index+1])
+				values[valueIndex].Value = strings.TrimPrefix(value.Value, currentWord[:index+1])
 				lastSegment = currentWord[index+1:]
 			}
 		}
@@ -88,22 +86,23 @@ func ActionRawValues(currentWord string, usage string, nospace common.SuffixMatc
 	if len(values) > 1 && commonDisplayPrefix(values...) != "" {
 		// When all display values have the same prefix bash will insert is as partial completion (which skips prefixes/formatting).
 		if valuePrefix := commonValuePrefix(values...); lastSegment != valuePrefix {
-			// replace values with common value prefix (`\001` is removed in snippet and compopt nospace will be set)
-			values = common.RawValuesFrom(commonValuePrefix(values...) + nospaceIndicator)
+			// replace values with common value prefix
+			values = common.RawValuesFrom(commonValuePrefix(values...))
 		} else {
 			// prevent insertion of partial display values by prefixing one with space
 			values[0].Display = " " + values[0].Display
 		}
+		meta.Nospace = meta.Nospace.Add("*")
 	}
 
 	vals := make([]string, len(values))
 	for index, val := range values {
-		if nospace.Matches(val.Value) {
-			val.Value = val.Value + nospaceIndicator
-		}
-
 		if len(values) == 1 {
 			vals[index] = quoter.Replace(sanitizer.Replace(val.Value))
+			if !meta.Nospace.Matches(val.Value) {
+				vals[index] = val.Value + " "
+			}
+
 		} else {
 			if val.Description != "" {
 				vals[index] = fmt.Sprintf("%v (%v)", val.Display, sanitizer.Replace(val.TrimmedDescription()))
diff --git a/internal/shell/bash/snippet.go b/internal/shell/bash/snippet.go
index db50606..ca30645 100644
--- a/internal/shell/bash/snippet.go
+++ b/internal/shell/bash/snippet.go
@@ -19,10 +19,7 @@ _%v_completion() {
   mapfile -t COMPREPLY < <(echo "$compline" | sed -e "s/ \$/ ''/" -e 's/"/\"/g' | xargs %v _carapace bash)
   [[ "${COMPREPLY[*]}" == "" ]] && COMPREPLY=() # fix for mapfile creating a non-empty array from empty command output
 
-  [[ ${COMPREPLY[0]} == *[/=@:.,$'\001'] ]] && compopt -o nospace
-  # TODO use mapfile
-  # shellcheck disable=SC2206
-  [[ ${#COMPREPLY[@]} -eq 1 ]] && COMPREPLY=(${COMPREPLY%%$'\001'})
+  compopt -o nospace
 }
 
 complete -F _%v_completion %v
diff --git a/internal/shell/bash_ble/action.go b/internal/shell/bash_ble/action.go
index 6d5f159..62dbb8b 100644
--- a/internal/shell/bash_ble/action.go
+++ b/internal/shell/bash_ble/action.go
@@ -8,11 +8,11 @@ import (
 )
 
 // ActionRawValues formats values for bash_ble.
-func ActionRawValues(currentWord string, usage string, nospace common.SuffixMatcher, values common.RawValues) string {
+func ActionRawValues(currentWord string, meta common.Meta, values common.RawValues) string {
 	vals := make([]string, len(values))
 	for index, val := range values {
 		suffix := " "
-		if nospace.Matches(val.Value) {
+		if meta.Nospace.Matches(val.Value) {
 			suffix = ""
 		}
 		vals[index] = fmt.Sprintf("%v\t%v\x1c%v\x1c%v\x1c%v", val.Value, val.Display, "", suffix, val.TrimmedDescription())
diff --git a/internal/shell/elvish/action.go b/internal/shell/elvish/action.go
index 4e7356b..d840508 100644
--- a/internal/shell/elvish/action.go
+++ b/internal/shell/elvish/action.go
@@ -23,17 +23,23 @@ func sanitize(values []common.RawValue) []common.RawValue {
 	return values
 }
 
-type complexCandidate struct {
-	Value            string
-	Display          string
-	Description      string
+type completion struct {
+	Usage            string
+	Messages         common.Messages
 	DescriptionStyle string
-	CodeSuffix       string
-	Style            string
+	Candidates       []complexCandidate
+}
+
+type complexCandidate struct {
+	Value       string
+	Display     string
+	Description string
+	CodeSuffix  string
+	Style       string
 }
 
 // ActionRawValues formats values for elvish.
-func ActionRawValues(currentWord string, usage string, nospace common.SuffixMatcher, values common.RawValues) string {
+func ActionRawValues(currentWord string, meta common.Meta, values common.RawValues) string {
 	valueStyle := "default"
 	if s := style.Carapace.Value; s != "" && ui.ParseStyling(s) != nil {
 		valueStyle = s
@@ -47,15 +53,21 @@ func ActionRawValues(currentWord string, usage string, nospace common.SuffixMatc
 	vals := make([]complexCandidate, len(values))
 	for index, val := range sanitize(values) {
 		suffix := " "
-		if nospace.Matches(val.Value) {
+		if meta.Nospace.Matches(val.Value) {
 			suffix = ""
 		}
 
 		if val.Style == "" || ui.ParseStyling(val.Style) == nil {
 			val.Style = valueStyle
 		}
-		vals[index] = complexCandidate{Value: val.Value, Display: val.Display, Description: val.Description, CodeSuffix: suffix, Style: val.Style, DescriptionStyle: descriptionStyle}
+		vals[index] = complexCandidate{Value: val.Value, Display: val.Display, Description: val.Description, CodeSuffix: suffix, Style: val.Style}
 	}
-	m, _ := json.Marshal(vals)
+
+	m, _ := json.Marshal(completion{
+		Usage:            meta.Usage,
+		Messages:         meta.Messages,
+		DescriptionStyle: descriptionStyle,
+		Candidates:       vals,
+	})
 	return string(m)
 }
diff --git a/internal/shell/elvish/snippet.go b/internal/shell/elvish/snippet.go
index 538bed6..f792efc 100644
--- a/internal/shell/elvish/snippet.go
+++ b/internal/shell/elvish/snippet.go
@@ -11,12 +11,17 @@ import (
 // Snippet creates the elvish completion script.
 func Snippet(cmd *cobra.Command) string {
 	return fmt.Sprintf(`set edit:completion:arg-completer[%v] = {|@arg|
-    %v _carapace elvish (all $arg) | from-json | all (one) | each {|c| 
-        if (eq $c[Description] "") {
-            edit:complex-candidate $c[Value] &display=(styled $c[Display] $c[Style]) &code-suffix=$c[CodeSuffix]
-        } else {
-            edit:complex-candidate $c[Value] &display=(styled $c[Display] $c[Style])(styled " " $c[DescriptionStyle]" bg-default")(styled "("$c[Description]")" $c[DescriptionStyle]) &code-suffix=$c[CodeSuffix]
-        }
+    %v _carapace elvish (all $arg) | from-json | each {|completion|
+		put $completion[Messages] | all (one) | each {|m|
+			edit:notify (styled "error: " red)$m
+		}
+		put $completion[Candidates] | all (one) | each {|c|
+			if (eq $c[Description] "") {
+		    	edit:complex-candidate $c[Value] &display=(styled $c[Display] $c[Style]) &code-suffix=$c[CodeSuffix]
+			} else {
+		    	edit:complex-candidate $c[Value] &display=(styled $c[Display] $c[Style])(styled " " $completion[DescriptionStyle]" bg-default")(styled "("$c[Description]")" $completion[DescriptionStyle]) &code-suffix=$c[CodeSuffix]
+			}
+		}
     }
 }
 `, cmd.Name(), uid.Executable())
diff --git a/internal/shell/export/action.go b/internal/shell/export/action.go
index 8ac3609..e2e6727 100644
--- a/internal/shell/export/action.go
+++ b/internal/shell/export/action.go
@@ -11,16 +11,18 @@ import (
 type Export struct {
 	Version   string
 	Usage     string
+	Messages  common.Messages
 	Nospace   common.SuffixMatcher
 	RawValues common.RawValues
 }
 
-func ActionRawValues(currentWord string, usage string, nospace common.SuffixMatcher, values common.RawValues) string {
+func ActionRawValues(currentWord string, meta common.Meta, values common.RawValues) string {
 	sort.Sort(common.ByValue(values))
 	m, _ := json.Marshal(Export{
 		Version:   version(),
-		Usage:     usage,
-		Nospace:   nospace,
+		Usage:     meta.Usage,
+		Messages:  meta.Messages,
+		Nospace:   meta.Nospace,
 		RawValues: values,
 	})
 	return string(m)
diff --git a/internal/shell/fish/action.go b/internal/shell/fish/action.go
index 9a03dd5..d5f68d8 100644
--- a/internal/shell/fish/action.go
+++ b/internal/shell/fish/action.go
@@ -13,7 +13,7 @@ var sanitizer = strings.NewReplacer(
 )
 
 // ActionRawValues formats values for fish.
-func ActionRawValues(currentWord string, usage string, nospace common.SuffixMatcher, values common.RawValues) string {
+func ActionRawValues(currentWord string, meta common.Meta, values common.RawValues) string {
 	vals := make([]string, len(values))
 	for index, val := range values {
 		vals[index] = fmt.Sprintf("%v\t%v", sanitizer.Replace(val.Value), sanitizer.Replace(val.TrimmedDescription()))
diff --git a/internal/shell/ion/action.go b/internal/shell/ion/action.go
index 29794b5..71b20cf 100644
--- a/internal/shell/ion/action.go
+++ b/internal/shell/ion/action.go
@@ -28,10 +28,10 @@ type suggestion struct {
 }
 
 // ActionRawValues formats values for ion.
-func ActionRawValues(currentWord string, usage string, nospace common.SuffixMatcher, values common.RawValues) string {
+func ActionRawValues(currentWord string, meta common.Meta, values common.RawValues) string {
 	vals := make([]suggestion, len(values))
 	for index, val := range sanitize(values) {
-		if !nospace.Matches(val.Value) {
+		if !meta.Nospace.Matches(val.Value) {
 			val.Value = val.Value + " "
 		}
 
diff --git a/internal/shell/nushell/action.go b/internal/shell/nushell/action.go
index 950ab87..361393b 100644
--- a/internal/shell/nushell/action.go
+++ b/internal/shell/nushell/action.go
@@ -28,14 +28,14 @@ func sanitize(values []common.RawValue) []common.RawValue {
 }
 
 // ActionRawValues formats values for nushell.
-func ActionRawValues(currentWord string, usage string, nospace common.SuffixMatcher, values common.RawValues) string {
+func ActionRawValues(currentWord string, meta common.Meta, values common.RawValues) string {
 	vals := make([]record, len(values))
 	for index, val := range sanitize(values) {
 		if strings.ContainsAny(val.Value, ` {}()[]<>$&"|;#\`+"`") {
 			val.Value = fmt.Sprintf("'%v'", val.Value)
 		}
 
-		if !nospace.Matches(val.Value) {
+		if !meta.Nospace.Matches(val.Value) {
 			val.Value = val.Value + " "
 		}
 
diff --git a/internal/shell/oil/action.go b/internal/shell/oil/action.go
index da49cb1..2ee5917 100644
--- a/internal/shell/oil/action.go
+++ b/internal/shell/oil/action.go
@@ -15,10 +15,10 @@ var sanitizer = strings.NewReplacer(
 const nospaceIndicator = "\001"
 
 // ActionRawValues formats values for oil.
-func ActionRawValues(currentWord string, usage string, nospace common.SuffixMatcher, values common.RawValues) string {
+func ActionRawValues(currentWord string, meta common.Meta, values common.RawValues) string {
 	vals := make([]string, len(values))
 	for index, val := range values {
-		if nospace.Matches(val.Value) {
+		if meta.Nospace.Matches(val.Value) {
 			val.Value = val.Value + nospaceIndicator
 		}
 
diff --git a/internal/shell/powershell/action.go b/internal/shell/powershell/action.go
index 7e87555..f1ddb85 100644
--- a/internal/shell/powershell/action.go
+++ b/internal/shell/powershell/action.go
@@ -30,7 +30,7 @@ func ensureNotEmpty(s string) string {
 }
 
 // ActionRawValues formats values for powershell.
-func ActionRawValues(currentWord string, usage string, nospace common.SuffixMatcher, values common.RawValues) string {
+func ActionRawValues(currentWord string, meta common.Meta, values common.RawValues) string {
 	valueStyle := "default"
 	if s := style.Carapace.Value; s != "" && ui.ParseStyling(s) != nil {
 		valueStyle = s
@@ -50,7 +50,7 @@ func ActionRawValues(currentWord string, usage string, nospace common.SuffixMatc
 				val.Value = fmt.Sprintf("'%v'", val.Value)
 			}
 
-			if !nospace.Matches(val.Value) {
+			if !meta.Nospace.Matches(val.Value) {
 				val.Value = val.Value + " "
 			}
 
diff --git a/internal/shell/shell.go b/internal/shell/shell.go
index 0e750ae..ab51c2b 100644
--- a/internal/shell/shell.go
+++ b/internal/shell/shell.go
@@ -55,8 +55,8 @@ func Snippet(cmd *cobra.Command, shell string) (string, error) {
 	return "", fmt.Errorf("expected one of '%v' [was: %v]", strings.Join(expected, "', '"), shell)
 }
 
-func Value(shell string, callbackValue string, usage string, nospace common.SuffixMatcher, values common.RawValues) string { // TODO use context instead?
-	shellFuncs := map[string]func(currentWord string, usage string, nospace common.SuffixMatcher, values common.RawValues) string{
+func Value(shell string, callbackValue string, meta common.Meta, values common.RawValues) string { // TODO use context instead?
+	shellFuncs := map[string]func(currentWord string, meta common.Meta, values common.RawValues) string{
 		"bash":       bash.ActionRawValues,
 		"bash-ble":   bash_ble.ActionRawValues,
 		"fish":       fish.ActionRawValues,
@@ -72,8 +72,13 @@ func Value(shell string, callbackValue string, usage string, nospace common.Suff
 	}
 	if f, ok := shellFuncs[shell]; ok {
 		filtered := values.FilterPrefix(callbackValue)
+		switch shell {
+		case "elvish", "export", "zsh": // shells with support for showing messages
+		default:
+			filtered = meta.Messages.Integrate(filtered, callbackValue)
+		}
 		sort.Sort(common.ByDisplay(filtered))
-		return f(callbackValue, usage, nospace, filtered)
+		return f(callbackValue, meta, filtered)
 	}
 	return ""
 }
diff --git a/internal/shell/tcsh/action.go b/internal/shell/tcsh/action.go
index d0f6ab5..292d176 100644
--- a/internal/shell/tcsh/action.go
+++ b/internal/shell/tcsh/action.go
@@ -68,7 +68,7 @@ func commonValuePrefix(values ...common.RawValue) (prefix string) {
 }
 
 // ActionRawValues formats values for bash.
-func ActionRawValues(currentWord string, usage string, nospace common.SuffixMatcher, values common.RawValues) string {
+func ActionRawValues(currentWord string, meta common.Meta, values common.RawValues) string {
 	lastSegment := currentWord // last segment of currentWord split by COMP_WORDBREAKS
 
 	for _, r := range values {
diff --git a/internal/shell/xonsh/action.go b/internal/shell/xonsh/action.go
index 28d1173..81e0ad7 100644
--- a/internal/shell/xonsh/action.go
+++ b/internal/shell/xonsh/action.go
@@ -21,7 +21,7 @@ type richCompletion struct {
 }
 
 // ActionRawValues formats values for xonsh.
-func ActionRawValues(currentWord string, usage string, nospace common.SuffixMatcher, values common.RawValues) string {
+func ActionRawValues(currentWord string, meta common.Meta, values common.RawValues) string {
 	vals := make([]richCompletion, len(values))
 	for index, val := range values {
 		val.Value = sanitizer.Replace(val.Value)
@@ -34,7 +34,7 @@ func ActionRawValues(currentWord string, usage string, nospace common.SuffixMatc
 			}
 		}
 
-		if !nospace.Matches(val.Value) {
+		if !meta.Nospace.Matches(val.Value) {
 			val.Value = val.Value + " "
 		}
 
diff --git a/internal/shell/zsh/action.go b/internal/shell/zsh/action.go
index ae0623d..86494ec 100644
--- a/internal/shell/zsh/action.go
+++ b/internal/shell/zsh/action.go
@@ -48,7 +48,7 @@ func quoteValue(s string) string {
 }
 
 // ActionRawValues formats values for zsh
-func ActionRawValues(currentWord string, usage string, nospace common.SuffixMatcher, values common.RawValues) string {
+func ActionRawValues(currentWord string, meta common.Meta, values common.RawValues) string {
 	maxLength := 0
 	for _, r := range values {
 		if length := len(r.Display); length > maxLength {
@@ -71,7 +71,7 @@ func ActionRawValues(currentWord string, usage string, nospace common.SuffixMatc
 	for index, val := range values {
 		val.Value = sanitizer.Replace(val.Value)
 		val.Value = quoteValue(val.Value)
-		if !nospace.Matches(val.Value) {
+		if !meta.Nospace.Matches(val.Value) {
 			val.Value = val.Value + " "
 		}
 		val.Display = sanitizer.Replace(val.Display)
@@ -94,7 +94,7 @@ func ActionRawValues(currentWord string, usage string, nospace common.SuffixMatc
 		zstyles = make([]string, 0)
 	}
 
-	return fmt.Sprintf(":%v\n%v\n%v", strings.Join(zstyles, ":"), santizeMessage(usage), strings.Join(vals, "\n"))
+	return fmt.Sprintf(":%v\n%v\n%v", strings.Join(zstyles, ":"), message{meta}.format(), strings.Join(vals, "\n"))
 }
 
 var zstyleQuoter = strings.NewReplacer(
@@ -114,19 +114,3 @@ var zstyleQuoter = strings.NewReplacer(
 func formatZstyle(s, _styleValue, _styleDescription string) string {
 	return fmt.Sprintf("=(#b)%v=0=%v=%v=%v", s, style.SGR(_styleValue), style.SGR(_styleDescription+" bg-default"), style.SGR(_styleDescription))
 }
-
-func santizeMessage(s string) string { // TODO cleanup
-	s = strings.NewReplacer(
-		"\n", ``,
-		"\r", ``,
-		"\t", ``,
-		"\v", ``,
-		"\f", ``,
-		"\b", ``,
-	).Replace(s)
-
-	if s == "" {
-		return "NONE" // TODO fix empty line issue in script
-	}
-	return s
-}
diff --git a/internal/shell/zsh/message.go b/internal/shell/zsh/message.go
new file mode 100644
index 0000000..fefeb50
--- /dev/null
+++ b/internal/shell/zsh/message.go
@@ -0,0 +1,53 @@
+package zsh
+
+import (
+	"fmt"
+	"strings"
+
+	"github.com/rsteube/carapace/internal/common"
+	"github.com/rsteube/carapace/pkg/style"
+)
+
+type message struct {
+	common.Meta
+}
+
+func (m message) format() string {
+	formatted := make([]string, 0)
+	for _, message := range m.Messages.Get() {
+		formatted = append(formatted, m.formatMessage(message, style.Carapace.Error))
+	}
+	if m.Usage != "" {
+		formatted = append(formatted, m.formatMessage(m.Usage, style.Carapace.Description))
+	}
+
+	if len(formatted) > 0 {
+		return strings.Join(formatted, "\t")
+	}
+	return "NONE" // TODO fix empty line issue in script
+}
+
+func (m message) formatMessage(message, _style string) string {
+	msg := strings.NewReplacer(
+		"\n", ``,
+		"\r", ``,
+		"\t", ``,
+		"\v", ``,
+		"\f", ``,
+		"\b", ``,
+
+		// TODO verify which are needed
+		`&`, `\&`,
+		`'`, `\'`,
+		`$`, `\$`,
+		`#`, `\#`,
+		`;`, `\;`,
+		`*`, `\*`,
+		`\`, `\\`,
+		`~`, `\~`,
+
+		`\`, `\\`,
+	).Replace(message)
+
+	return fmt.Sprintf("\x1b[%vm%v\x1b[%vm", style.SGR(_style), msg, style.SGR("fg-default"))
+}
diff --git a/internal/shell/zsh/snippet.go b/internal/shell/zsh/snippet.go
index edac388..e9f5a70 100644
--- a/internal/shell/zsh/snippet.go
+++ b/internal/shell/zsh/snippet.go
@@ -27,7 +27,8 @@ function _%v_completion {
   fi
 
   export ZLS_COLOURS="${lines[1]}"
-  [[ ! "${lines[2]}" == "NONE" ]] && _message -r "${lines[2]}"
+  local line_break=$'\n'
+  [[ ! "${lines[2]}" == "NONE" ]] && _message -r "${lines[2]//$'\t'/${line_break}}"
   
   # shellcheck disable=SC2034,2206
   lines=(${lines[@]:2})
diff --git a/invokedAction.go b/invokedAction.go
index 213011e..c9f0412 100644
--- a/invokedAction.go
+++ b/invokedAction.go
@@ -19,17 +19,8 @@ type InvokedAction struct {
 //	a := carapace.ActionValues("A", "B", "C").Invoke(c)
 //	b := a.Filter([]string{"B"}) // ["A", "C"]
 func (a InvokedAction) Filter(values []string) InvokedAction {
-	toremove := make(map[string]bool)
-	for _, v := range values {
-		toremove[v] = true
-	}
-	filtered := make([]common.RawValue, 0)
-	for _, rawValue := range a.rawValues {
-		if _, ok := toremove[rawValue.Value]; !ok {
-			filtered = append(filtered, rawValue)
-		}
-	}
-	return InvokedAction{actionRawValues(filtered...).noSpace(string(a.nospace)).skipCache(a.skipcache).withUsage(a.usage)}
+	a.rawValues = common.RawValues(a.rawValues).Filter(values...)
+	return a
 }
 
 // Merge merges InvokedActions (existing values are overwritten)
@@ -39,25 +30,28 @@ func (a InvokedAction) Filter(values []string) InvokedAction {
 //	c := a.Merge(b) // ["A", "B", "C"]
 func (a InvokedAction) Merge(others ...InvokedAction) InvokedAction {
 	uniqueRawValues := make(map[string]common.RawValue)
-	nospace := a.nospace
-	skipcache := a.skipcache
-	usage := a.usage
+	nospace := a.meta.Nospace
+	usage := a.meta.Usage
+	messages := a.meta.Messages
 	for _, other := range append([]InvokedAction{a}, others...) {
 		for _, c := range other.rawValues {
 			uniqueRawValues[c.Value] = c
 		}
-		nospace = nospace.Add(string(other.nospace))
-		skipcache = skipcache || other.skipcache
-		if other.usage != "" {
-			usage = other.usage
+		nospace = nospace.Add(string(other.meta.Nospace))
+		if other.meta.Usage != "" {
+			usage = other.meta.Usage
 		}
+		messages.Merge(other.meta.Messages)
 	}
 
 	rawValues := make([]common.RawValue, 0, len(uniqueRawValues))
 	for _, c := range uniqueRawValues {
 		rawValues = append(rawValues, c)
 	}
-	return InvokedAction{actionRawValues(rawValues...).noSpace(string(nospace)).skipCache(skipcache).withUsage(usage)}
+
+	invoked := InvokedAction{actionRawValues(rawValues...).noSpace(string(nospace)).withUsage(usage)}
+	invoked.meta.Messages = messages // TODO verify & optimize
+	return invoked
 }
 
 // Prefix adds a prefix to values (only the ones inserted, not the display values)
@@ -162,5 +156,5 @@ func (a InvokedAction) ToMultiPartsA(dividers ...string) Action {
 }
 
 func (a InvokedAction) value(shell string, callbackValue string) string {
-	return _shell.Value(shell, callbackValue, a.usage, a.nospace, a.rawValues)
+	return _shell.Value(shell, callbackValue, a.meta, a.rawValues)
 }
diff --git a/storage.go b/storage.go
index df11d86..66334be 100644
--- a/storage.go
+++ b/storage.go
@@ -40,8 +40,8 @@ func (s _storage) getFlag(cmd *cobra.Command, name string) Action {
 
 		return ActionCallback(func(c Context) Action { // TODO verify order of execution is correct
 			invoked := a.Invoke(c)
-			if invoked.usage == "" {
-				invoked.usage = flag.Usage
+			if invoked.meta.Usage == "" {
+				invoked.meta.Usage = flag.Usage
 			}
 			return invoked.ToA()
 		})
@@ -85,8 +85,8 @@ func (s _storage) getPositional(cmd *cobra.Command, pos int) Action {
 
 	return ActionCallback(func(c Context) Action { // TODO verify order of execution is correct
 		invoked := a.Invoke(c)
-		if invoked.usage == "" && len(strings.Fields(cmd.Use)) > 1 {
-			invoked.usage = cmd.Use
+		if invoked.meta.Usage == "" && len(strings.Fields(cmd.Use)) > 1 {
+			invoked.meta.Usage = cmd.Use
 		}
 		return invoked.ToA()
 	})


Test patch:
diff --git a/action_test.go b/action_test.go
index b8d9b72..ab68e9d 100644
--- a/action_test.go
+++ b/action_test.go
@@ -23,9 +23,11 @@ func assertEqual(t *testing.T, expected, actual InvokedAction) {
 
 	e, _ := json.MarshalIndent(expected.rawValues, "", "  ")
 	a, _ := json.MarshalIndent(actual.rawValues, "", "  ")
-
 	assert.Equal(t, string(e), string(a))
-	assert.Equal(t, string(expected.nospace), string(actual.nospace))
+
+	eMeta, _ := json.MarshalIndent(expected.meta, "", "  ")
+	aMeta, _ := json.MarshalIndent(actual.meta, "", "  ")
+	assert.Equal(t, string(eMeta), string(aMeta))
 }
 
 func assertNotEqual(t *testing.T, expected, actual InvokedAction) {
@@ -51,8 +53,9 @@ func TestActionCallback(t *testing.T) {
 	expected := InvokedAction{
 		Action{
 			rawValues: common.RawValuesFrom("a", "b", "c"),
-			nospace:   "",
-			skipcache: false,
+			meta: common.Meta{
+				Nospace: "",
+			},
 		},
 	}
 	actual := a.Invoke(Context{})
@@ -86,11 +89,11 @@ func TestSkipCache(t *testing.T) {
 			Suffix("").
 			ToA()
 	})
-	if a.skipcache {
-		t.Fatal("uninvoked skipcache should be false")
+	if !a.meta.Messages.IsEmpty() {
+		t.Fatal("uninvoked action should not contain messages")
 	}
-	if !a.Invoke(Context{}).skipcache {
-		t.Fatal("invoked skipcache should be true")
+	if a.Invoke(Context{}).meta.Messages.IsEmpty() {
+		t.Fatal("invoked action should contain messages")
 	}
 }
 
@@ -105,10 +108,10 @@ func TestNoSpace(t *testing.T) {
 			Suffix("").
 			ToA()
 	})
-	if a.nospace != "" {
+	if a.meta.Nospace != "" {
 		t.Fatal("uninvoked nospace should be empty")
 	}
-	if a.Invoke(Context{}).nospace != "*" {
+	if a.Invoke(Context{}).meta.Nospace != "*" {
 		t.Fatal("invoked nospace should be `*`")
 	}
 }
@@ -181,12 +184,12 @@ func TestActionFilesChdir(t *testing.T) {
 	oldWd, _ := os.Getwd()
 
 	assertEqual(t,
-		ActionStyledValuesDescribed("ERR", fmt.Sprintf("open %v: no such file or directory", wd("nonexistent")), style.Carapace.Error, "_", "", style.Default).noSpace("/").Style("fg-default bg-default").Invoke(Context{}),
-		ActionFiles(".md").Chdir("nonexistent").Invoke(Context{CallbackValue: ""}),
+		ActionMessage(fmt.Sprintf("stat %v: no such file or directory", wd("nonexistent"))).NoSpace('/').Invoke(Context{}),
+		ActionFiles(".md").Chdir("nonexistent").Invoke(Context{}),
 	)
 
 	assertEqual(t,
-		ActionStyledValuesDescribed("ERR", fmt.Sprintf("readdirent %v/go.mod: not a directory", wd("")), style.Carapace.Error, "_", "", style.Default).noSpace("/").Style("fg-default bg-default").Invoke(Context{}),
+		ActionMessage(fmt.Sprintf("not a directory: %v/go.mod", wd(""))).Invoke(Context{}),
 		ActionFiles(".md").Chdir("go.mod").Invoke(Context{CallbackValue: ""}),
 	)
 
@@ -204,12 +207,11 @@ func TestActionFilesChdir(t *testing.T) {
 }
 
 func TestActionMessage(t *testing.T) {
-	expected := ActionStyledValuesDescribed("_", "", style.Default, "ERR", "example message", style.Carapace.Error).noSpace("*").skipCache(true).Invoke(Context{}).Prefix("docs/")
-	for index := range expected.rawValues {
-		expected.rawValues[index].Tag = "messages"
-	}
+	expected := ActionValues().NoSpace()
+	expected.meta.Messages.Add("example message")
+
 	assertEqual(t,
-		expected,
+		expected.Invoke(Context{}),
 		ActionMessage("example message").Invoke(Context{CallbackValue: "docs/"}),
 	)
 }
@@ -220,13 +222,13 @@ func TestActionMessageSuppress(t *testing.T) {
 			ActionMessage("example message").Suppress("example"),
 			ActionValues("test"),
 		).ToA().Invoke(Context{}),
-		ActionValues("test").noSpace("*").skipCache(true).Invoke(Context{}), // TODO suppress does not reset nospace (is that even possible?)
+		ActionValues("test").noSpace("*").Invoke(Context{}), // TODO suppress does not reset nospace (is that even possible?)
 	)
 }
 
 func TestActionExecCommand(t *testing.T) {
 	assertEqual(t,
-		ActionMessage("go unknown: unknown command").noSpace("/").skipCache(true).Invoke(Context{}).Prefix("docs/"),
+		ActionMessage("go unknown: unknown command").noSpace("/").Invoke(Context{}).Prefix("docs/"),
 		ActionExecCommand("go", "unknown")(func(output []byte) Action { return ActionValues() }).Invoke(Context{CallbackValue: "docs/"}),
 	)
 
diff --git a/batch_test.go b/batch_test.go
index d9d5324..307eb48 100644
--- a/batch_test.go
+++ b/batch_test.go
@@ -15,8 +15,6 @@ func TestBatch(t *testing.T) {
 	expected := InvokedAction{
 		Action{
 			rawValues: common.RawValuesFrom("A", "B", "C", "D"),
-			nospace:   common.SuffixMatcher(""),
-			skipcache: false,
 		},
 	}
 	actual := b.Invoke(Context{}).Merge()
@@ -30,8 +28,6 @@ func TestBatchSingle(t *testing.T) {
 	expected := InvokedAction{
 		Action{
 			rawValues: common.RawValuesFrom("A", "B"),
-			nospace:   common.SuffixMatcher(""),
-			skipcache: false,
 		},
 	}
 	actual := b.Invoke(Context{}).Merge()
@@ -43,8 +39,6 @@ func TestBatchNone(t *testing.T) {
 	expected := InvokedAction{
 		Action{
 			rawValues: common.RawValuesFrom(),
-			nospace:   common.SuffixMatcher(""),
-			skipcache: false,
 		},
 	}
 	actual := b.Invoke(Context{}).Merge()
@@ -60,8 +54,6 @@ func TestBatchToA(t *testing.T) {
 	expected := InvokedAction{
 		Action{
 			rawValues: common.RawValuesFrom("A", "B", "C", "D"),
-			nospace:   common.SuffixMatcher(""),
-			skipcache: false,
 		},
 	}
 	actual := b.ToA().Invoke(Context{})
diff --git a/carapace_test.go b/carapace_test.go
index 60b0915..776a1f1 100644
--- a/carapace_test.go
+++ b/carapace_test.go
@@ -230,7 +230,7 @@ func TestComplete(t *testing.T) {
 	cmd.Flags().BoolP("a", "1", false, "")
 	cmd.Flags().BoolP("b", "2", false, "")
 
-	if s, err := complete(cmd, []string{"elvish", "_", "test", "-1"}); err != nil || s != `[{"Value":"-12","Display":"2","Description":"","DescriptionStyle":"dim white","CodeSuffix":"","Style":"default"}]` {
+	if s, err := complete(cmd, []string{"elvish", "_", "test", "-1"}); err != nil || s != `{"Usage":"","Messages":[],"DescriptionStyle":"dim white","Candidates":[{"Value":"-12","Display":"2","Description":"","CodeSuffix":"","Style":"default"}]}` {
 		t.Error(s)
 	}
 }
@@ -246,7 +246,7 @@ func TestCompleteOptarg(t *testing.T) {
 		"opt": ActionValuesDescribed("value", "description"),
 	})
 
-	if s, err := complete(cmd, []string{"elvish", "_", "test", "--opt="}); err != nil || s != `[{"Value":"--opt=value","Display":"value","Description":"description","DescriptionStyle":"dim white","CodeSuffix":" ","Style":"default"}]` {
+	if s, err := complete(cmd, []string{"elvish", "_", "test", "--opt="}); err != nil || s != `{"Usage":"","Messages":[],"DescriptionStyle":"dim white","Candidates":[{"Value":"--opt=value","Display":"value","Description":"description","CodeSuffix":" ","Style":"default"}]}` {
 		t.Error(s)
 	}
 }
@@ -270,7 +270,7 @@ func TestCompletePositionalWithSpace(t *testing.T) {
 		ActionValues("positional with space"),
 	)
 
-	if s, err := complete(cmd, []string{"elvish", "_", "positional "}); err != nil || s != `[{"Value":"positional with space","Display":"positional with space","Description":"","DescriptionStyle":"dim white","CodeSuffix":" ","Style":"default"}]` {
+	if s, err := complete(cmd, []string{"elvish", "_", "positional "}); err != nil || s != `{"Usage":"","Messages":[],"DescriptionStyle":"dim white","Candidates":[{"Value":"positional with space","Display":"positional with space","Description":"","CodeSuffix":" ","Style":"default"}]}` {
 		t.Error(s)
 	}
 }
diff --git a/example/cmd/_test/bash-ble.sh b/example/cmd/_test/bash-ble.sh
index 0ee8a6a..df7bf7b 100644
--- a/example/cmd/_test/bash-ble.sh
+++ b/example/cmd/_test/bash-ble.sh
@@ -7,10 +7,7 @@ _example_completion() {
   mapfile -t COMPREPLY < <(echo "$compline" | sed -e "s/ \$/ ''/" -e 's/"/\"/g' | xargs example _carapace bash)
   [[ "${COMPREPLY[*]}" == "" ]] && COMPREPLY=() # fix for mapfile creating a non-empty array from empty command output
 
-  [[ ${COMPREPLY[0]} == *[/=@:.,$'\001'] ]] && compopt -o nospace
-  # TODO use mapfile
-  # shellcheck disable=SC2206
-  [[ ${#COMPREPLY[@]} -eq 1 ]] && COMPREPLY=(${COMPREPLY%$'\001'})
+  compopt -o nospace
 }
 
 
diff --git a/example/cmd/_test/bash.sh b/example/cmd/_test/bash.sh
index 03f64af..5489de3 100644
--- a/example/cmd/_test/bash.sh
+++ b/example/cmd/_test/bash.sh
@@ -7,10 +7,7 @@ _example_completion() {
   mapfile -t COMPREPLY < <(echo "$compline" | sed -e "s/ \$/ ''/" -e 's/"/\"/g' | xargs example _carapace bash)
   [[ "${COMPREPLY[*]}" == "" ]] && COMPREPLY=() # fix for mapfile creating a non-empty array from empty command output
 
-  [[ ${COMPREPLY[0]} == *[/=@:.,$'\001'] ]] && compopt -o nospace
-  # TODO use mapfile
-  # shellcheck disable=SC2206
-  [[ ${#COMPREPLY[@]} -eq 1 ]] && COMPREPLY=(${COMPREPLY%$'\001'})
+  compopt -o nospace
 }
 
 complete -F _example_completion example
diff --git a/example/cmd/_test/elvish.elv b/example/cmd/_test/elvish.elv
index eea0c97..5f67c9d 100644
--- a/example/cmd/_test/elvish.elv
+++ b/example/cmd/_test/elvish.elv
@@ -1,10 +1,15 @@
 set edit:completion:arg-completer[example] = {|@arg|
-    example _carapace elvish (all $arg) | from-json | all (one) | each {|c| 
-        if (eq $c[Description] "") {
-            edit:complex-candidate $c[Value] &display=(styled $c[Display] $c[Style]) &code-suffix=$c[CodeSuffix]
-        } else {
-            edit:complex-candidate $c[Value] &display=(styled $c[Display] $c[Style])(styled " " $c[DescriptionStyle]" bg-default")(styled "("$c[Description]")" $c[DescriptionStyle]) &code-suffix=$c[CodeSuffix]
-        }
+    example _carapace elvish (all $arg) | from-json | each {|completion|
+		put $completion[Messages] | all (one) | each {|m|
+			edit:notify (styled "error: " red)$m
+		}
+		put $completion[Candidates] | all (one) | each {|c|
+			if (eq $c[Description] "") {
+		    	edit:complex-candidate $c[Value] &display=(styled $c[Display] $c[Style]) &code-suffix=$c[CodeSuffix]
+			} else {
+		    	edit:complex-candidate $c[Value] &display=(styled $c[Display] $c[Style])(styled " " $completion[DescriptionStyle]" bg-default")(styled "("$c[Description]")" $completion[DescriptionStyle]) &code-suffix=$c[CodeSuffix]
+			}
+		}
     }
 }
 
diff --git a/example/cmd/_test/zsh.sh b/example/cmd/_test/zsh.sh
index 0e6232b..0a4a45f 100644
--- a/example/cmd/_test/zsh.sh
+++ b/example/cmd/_test/zsh.sh
@@ -15,7 +15,8 @@ function _example_completion {
   fi
 
   export ZLS_COLOURS="${lines[1]}"
-  [[ ! "${lines[2]}" == "NONE" ]] && _message -r "${lines[2]}"
+  local line_break=$'\n'
+  [[ ! "${lines[2]}" == "NONE" ]] && _message -r "${lines[2]//$'\t'/${line_break}}"
   
   # shellcheck disable=SC2034,2206
   lines=(${lines[@]:2})
diff --git a/example/main_test.go b/example/main_test.go
index a3284b9..eb68c79 100644
--- a/example/main_test.go
+++ b/example/main_test.go
@@ -61,25 +61,19 @@ func doComplete(t *testing.T, shell string, cmdline string, contained ...string)
 var tests = map[string]string{
 	`example -ap `:               "action",
 	`example action `:            "p",
-	`example action -z`:          "unknown",
-	`example action --fail `:     "unknown",
-	`example --fail acti`:        "unknown",
-	`example -z acti`:            "unknown",
 	`example action positional`:  "positional1",
 	`example action positional1`: "positional1 with space",
 	//`example action "positional1 `: "positional1 with space", // TODO this test does not yet work with bash as it's missing quote handling in the snippet
 	//`example action --`:                                            "--values_described", // weird: causes regex match in expect/xonsh not to work
 	//`example action -`:                                             "-o", // weird: causes regex match in expect/xonsh not to work
-	`example action --callback `:                                   "values flag is not set",
 	`example flag --optarg `:                                       "p",
 	`example flag --optarg positional`:                             "positional1",
 	`example flag --optar`:                                         "--optarg",
-	`example flag --optarg=`:                                       "--optarg=optarg",
+	`example flag --optarg=`:                                       "optarg",
 	`example flag -o`:                                              "count flag",
 	`example flag -oc`:                                             "count flag",
 	`example flag -o `:                                             "p",
 	`example flag -o pos`:                                          "positional",
-	`example flag -o=`:                                             "unknown", // seems shorthand flag should not accept optional arguments and `=` is seen as another flag
 	`example multiparts `:                                          "VALUE",
 	`example multiparts -`:                                         "-c",
 	`example multiparts --`:                                        "--comma",
@@ -112,6 +106,15 @@ var tests = map[string]string{
 	`example multiparts VALUE=one,DIRECTORY=`:                      "/",
 }
 
+var testsIntegratedMessage = map[string]string{
+	`example action -z`:          "unknown",
+	`example action --fail `:     "unknown",
+	`example --fail acti`:        "unknown",
+	`example -z acti`:            "unknown",
+	`example flag -o=`:           "unknown", // seems shorthand flag should not accept optional arguments and `=` is seen as another flag
+	`example action --callback `: "values flag is not set",
+}
+
 func TestBash(t *testing.T) {
 	if err := exec.Command("bash", "--version").Run(); err != nil {
 		t.Skip("skipping bash")
@@ -119,6 +122,9 @@ func TestBash(t *testing.T) {
 	for cmdline, text := range tests {
 		doComplete(t, "bash", cmdline, text)
 	}
+	for cmdline, text := range testsIntegratedMessage {
+		doComplete(t, "bash", cmdline, text)
+	}
 }
 
 func TestElvish(t *testing.T) {
@@ -137,6 +143,9 @@ func TestFish(t *testing.T) {
 	for cmdline, text := range tests {
 		doComplete(t, "fish", cmdline, text)
 	}
+	for cmdline, text := range testsIntegratedMessage {
+		doComplete(t, "fish", cmdline, text)
+	}
 }
 
 func TestXonsh(t *testing.T) {
@@ -146,6 +155,9 @@ func TestXonsh(t *testing.T) {
 	for cmdline, text := range tests {
 		doComplete(t, "xonsh", cmdline, text)
 	}
+	for cmdline, text := range testsIntegratedMessage {
+		doComplete(t, "xonsh", cmdline, text)
+	}
 }
 
 func TestOil(t *testing.T) {
@@ -155,6 +167,9 @@ func TestOil(t *testing.T) {
 	for cmdline, text := range tests {
 		doComplete(t, "oil", cmdline, text)
 	}
+	for cmdline, text := range testsIntegratedMessage {
+		doComplete(t, "oil", cmdline, text)
+	}
 }
 
 //func TestPowershell(t *testing.T) {
@@ -164,6 +179,9 @@ func TestOil(t *testing.T) {
 //	for cmdline, text := range tests {
 //		doComplete(t, "powershell", cmdline, text)
 //	}
+//	for cmdline, text := range testsIntegratedMessage {
+//		doComplete(t, "powershell", cmdline, text)
+//	}
 //}
 
 func TestZsh(t *testing.T) {