From 74d5106e8f0d2ff5806f14c24240ddc4a99ddaf1 Mon Sep 17 00:00:00 2001
From: Hunam <hunam@disroot.org>
Date: Fri, 4 Mar 2022 11:28:11 +0100
Subject: [PATCH] cli: print cli errors in red where possible (#13647)

---
 vlib/cli/command.v | 53 ++++++++++++++++++++++------------------------
 vlib/term/term.v   | 12 ++++++++++-
 2 files changed, 36 insertions(+), 29 deletions(-)

diff --git a/vlib/cli/command.v b/vlib/cli/command.v
index d242e00453..4401c5f455 100644
--- a/vlib/cli/command.v
+++ b/vlib/cli/command.v
@@ -1,5 +1,7 @@
 module cli
 
+import term
+
 type FnCommandCallback = fn (cmd Command) ?
 
 // str returns the `string` representation of the callback.
@@ -92,8 +94,7 @@ pub fn (mut cmd Command) add_commands(commands []Command) {
 pub fn (mut cmd Command) add_command(command Command) {
 	mut subcmd := command
 	if cmd.commands.contains(subcmd.name) {
-		println('Command with the name `$subcmd.name` already exists')
-		exit(1)
+		eprintln_exit('Command with the name `$subcmd.name` already exists')
 	}
 	subcmd.parent = unsafe { cmd }
 	cmd.commands << subcmd
@@ -119,8 +120,7 @@ pub fn (mut cmd Command) add_flags(flags []Flag) {
 // add_flag adds `flag` to this `Command`.
 pub fn (mut cmd Command) add_flag(flag Flag) {
 	if cmd.flags.contains(flag.name) {
-		println('Flag with the name `$flag.name` already exists')
-		exit(1)
+		eprintln_exit('Flag with the name `$flag.name` already exists')
 	}
 	cmd.flags << flag
 }
@@ -181,16 +181,14 @@ fn (mut cmd Command) parse_flags() {
 					found = true
 					flag.found = true
 					cmd.args = flag.parse(cmd.args, cmd.posix_mode) or {
-						println('Failed to parse flag `${cmd.args[0]}`: $err')
-						exit(1)
+						eprintln_exit('Failed to parse flag `${cmd.args[0]}`: $err')
 					}
 					break
 				}
 			}
 		}
 		if !found {
-			println('Command `$cmd.name` has no flag `${cmd.args[0]}`')
-			exit(1)
+			eprintln_exit('Command `$cmd.name` has no flag `${cmd.args[0]}`')
 		}
 	}
 }
@@ -221,27 +219,21 @@ fn (mut cmd Command) parse_commands() {
 	// if no further command was found, execute current command
 	if cmd.required_args > 0 {
 		if cmd.required_args > cmd.args.len {
-			eprintln('Command `$cmd.name` needs at least $cmd.required_args arguments')
-			exit(1)
+			eprintln_exit('Command `$cmd.name` needs at least $cmd.required_args arguments')
 		}
 	}
 	cmd.check_required_flags()
-	if !isnil(cmd.pre_execute) {
-		cmd.pre_execute(*cmd) or {
-			eprintln('cli preexecution error: $err')
-			exit(1)
-		}
-	}
-	if !isnil(cmd.execute) {
-		cmd.execute(*cmd) or {
-			eprintln('cli execution error: $err')
-			exit(1)
-		}
-	}
-	if !isnil(cmd.post_execute) {
-		cmd.post_execute(*cmd) or {
-			eprintln('cli postexecution error: $err')
-			exit(1)
+
+	cmd.handle_cb(cmd.pre_execute, 'preexecution')
+	cmd.handle_cb(cmd.execute, 'execution')
+	cmd.handle_cb(cmd.post_execute, 'postexecution')
+}
+
+fn (mut cmd Command) handle_cb(cb FnCommandCallback, label string) {
+	if !isnil(cb) {
+		cb(*cmd) or {
+			label_message := term.ecolorize(term.bright_red, 'cli $label error:')
+			eprintln_exit('$label_message $err')
 		}
 	}
 }
@@ -271,8 +263,7 @@ fn (cmd Command) check_required_flags() {
 	for flag in cmd.flags {
 		if flag.required && flag.value.len == 0 {
 			full_name := cmd.full_name()
-			println('Flag `$flag.name` is required by `$full_name`')
-			exit(1)
+			eprintln_exit('Flag `$flag.name` is required by `$full_name`')
 		}
 	}
 }
@@ -305,3 +296,9 @@ fn (cmds []Command) contains(name string) bool {
 	}
 	return false
 }
+
+[noreturn]
+fn eprintln_exit(message string) {
+	eprintln(message)
+	exit(1)
+}
diff --git a/vlib/term/term.v b/vlib/term/term.v
index 1378d696c6..3cf60fef26 100644
--- a/vlib/term/term.v
+++ b/vlib/term/term.v
@@ -61,7 +61,7 @@ pub fn warn_message(s string) string {
 }
 
 // colorize returns a colored string by running the specified `cfn` over
-// the message `s`, only if colored output is supported by the terminal.
+// the message `s`, only if colored stdout is supported by the terminal.
 // Example: term.colorize(term.yellow, 'the message')
 pub fn colorize(cfn fn (string) string, s string) string {
 	if can_show_color_on_stdout() {
@@ -70,6 +70,16 @@ pub fn colorize(cfn fn (string) string, s string) string {
 	return s
 }
 
+// ecolorize returns a colored string by running the specified `cfn` over
+// the message `s`, only if colored stderr is supported by the terminal.
+// Example: term.ecolorize(term.bright_red, 'the message')
+pub fn ecolorize(cfn fn (string) string, s string) string {
+	if can_show_color_on_stderr() {
+		return cfn(s)
+	}
+	return s
+}
+
 // strip_ansi removes any ANSI sequences in the `text`
 pub fn strip_ansi(text string) string {
 	// This is a port of https://github.com/kilobyte/colorized-logs/blob/master/ansi2txt.c