Browse Source

libdpkg, scripts: Add very basic color support

This adds disabled by default color output, that can be enabled with
the new DPKG_COLOR environment variable.

The colors are currently hard-coded ANSI escape sequences, but will be
made configurable eventually.
Guillem Jover 8 years ago
parent
commit
e0c33c729c

+ 1 - 0
check.am

@@ -27,6 +27,7 @@ check-local: $(test_data) $(test_programs) $(test_scripts)
 	[ -z "$(test_tmpdir)" ] || $(MKDIR_P) $(test_tmpdir)
 	PATH="$(abs_top_builddir)/src:$(abs_top_builddir)/scripts:$(abs_top_builddir)/utils:$(PATH)" \
 	  LC_ALL=C \
+	  DPKG_COLORS=never \
 	  $(TEST_ENV_VARS) \
 	  srcdir=$(srcdir) builddir=$(builddir) \
 	  CC=$(CC) \

+ 3 - 0
debian/changelog

@@ -71,6 +71,9 @@ dpkg (1.18.5) UNRELEASED; urgency=medium
     Prompted by Andrey Utkin <andrey.krieger.utkin@gmail.com>.
   * Promote a print to a warning for missing control files in dpkg-deb.
   * Use info() instead of print in dpkg-buildpackage and dpkg-genchanges.
+  * Add very basic color support to all dpkg namespaced programs, enabled by
+    setting the environment variable DPKG_COLORS to “auto”, “always” or
+    “never”, the latter being the default.
   * Portability:
     - Move DPKG_ADMINDIR environment variable name out from update-alternatives
       code, to make life easier for non-dpkg-based systems.

+ 2 - 0
lib/dpkg/Makefile.am

@@ -48,6 +48,7 @@ libdpkg_la_SOURCES = \
 	buffer.c \
 	c-ctype.c \
 	cleanup.c \
+	color.c \
 	command.c \
 	compress.c \
 	dbdir.c \
@@ -108,6 +109,7 @@ pkginclude_HEADERS = \
 	atomic-file.h \
 	buffer.h \
 	c-ctype.h \
+	color.h \
 	command.h \
 	compress.h \
 	deb-version.h \

+ 74 - 0
lib/dpkg/color.c

@@ -0,0 +1,74 @@
+/*
+ * libdpkg - Debian packaging suite library routines
+ * color.c - color support
+ *
+ * Copyright © 2015-2016 Guillem Jover <guillem@debian.org>
+ *
+ * This is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <config.h>
+#include <compat.h>
+
+#include <stdbool.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+#include <dpkg/macros.h>
+#include <dpkg/color.h>
+
+static enum color_mode color_mode = COLOR_MODE_UNKNOWN;
+static bool use_color = false;
+
+bool
+color_set_mode(const char *mode)
+{
+	if (strcmp(mode, "auto") == 0) {
+		color_mode = COLOR_MODE_AUTO;
+		use_color = isatty(STDOUT_FILENO);
+	} else if (strcmp(mode, "always") == 0) {
+		color_mode = COLOR_MODE_ALWAYS;
+		use_color = true;
+	} else {
+		color_mode = COLOR_MODE_NEVER;
+		use_color = false;
+	}
+
+	return use_color;
+}
+
+static bool
+color_enabled(void)
+{
+	const char *mode;
+
+	if (color_mode != COLOR_MODE_UNKNOWN)
+		return use_color;
+
+	mode = getenv("DPKG_COLORS");
+	if (mode == NULL)
+		mode = "never";
+
+	return color_set_mode(mode);
+}
+
+const char *
+color_get(const char *color)
+{
+	if (!color_enabled())
+		return "";
+
+	return color;
+}

+ 87 - 0
lib/dpkg/color.h

@@ -0,0 +1,87 @@
+/*
+ * libdpkg - Debian packaging suite library routines
+ * color.h - color support
+ *
+ * Copyright © 2015-2016 Guillem Jover <guillem@debian.org>
+ *
+ * This is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef LIBDPKG_COLOR_H
+#define LIBDPKG_COLOR_H
+
+#include <stdbool.h>
+
+#include <dpkg/macros.h>
+
+DPKG_BEGIN_DECLS
+
+/**
+ * @defgroup color Color support
+ * @ingroup dpkg-internal
+ * @{
+ */
+
+/* Standard ANSI colors and attributes. */
+#define COLOR_NORMAL		""
+#define COLOR_RESET		"\e[0m"
+#define COLOR_BOLD		"\e[1m"
+#define COLOR_BLACK		"\e[30m"
+#define COLOR_RED		"\e[31m"
+#define COLOR_GREEN		"\e[32m"
+#define COLOR_YELLOW		"\e[33m"
+#define COLOR_BLUE		"\e[34m"
+#define COLOR_MAGENTA		"\e[35m"
+#define COLOR_CYAN		"\e[36m"
+#define COLOR_WHITE		"\e[37m"
+#define COLOR_BOLD_BLACK	"\e[1;30m"
+#define COLOR_BOLD_RED		"\e[1;31m"
+#define COLOR_BOLD_GREEN	"\e[1;32m"
+#define COLOR_BOLD_YELLOW	"\e[1;33m"
+#define COLOR_BOLD_BLUE		"\e[1;34m"
+#define COLOR_BOLD_MAGENTA	"\e[1;35m"
+#define COLOR_BOLD_CYAN		"\e[1;36m"
+#define COLOR_BOLD_WHITE	"\e[1;37m"
+
+/* Current defaults. These might become configurable in the future. */
+#define COLOR_PROG		COLOR_BOLD
+#define COLOR_INFO		COLOR_GREEN
+#define COLOR_NOTICE		COLOR_YELLOW
+#define COLOR_WARN		COLOR_BOLD_YELLOW
+#define COLOR_ERROR		COLOR_BOLD_RED
+
+enum color_mode {
+	COLOR_MODE_UNKNOWN = -1,
+	COLOR_MODE_NEVER,
+	COLOR_MODE_ALWAYS,
+	COLOR_MODE_AUTO,
+};
+
+bool
+color_set_mode(const char *mode);
+
+const char *
+color_get(const char *color);
+
+static inline const char *
+color_reset(void)
+{
+	return color_get(COLOR_RESET);
+}
+
+/** @} */
+
+DPKG_END_DECLS
+
+#endif /* LIBDPKG_COLOR_H */

+ 7 - 3
lib/dpkg/ehandle.c

@@ -32,6 +32,7 @@
 #include <dpkg/macros.h>
 #include <dpkg/i18n.h>
 #include <dpkg/progname.h>
+#include <dpkg/color.h>
 #include <dpkg/ehandle.h>
 
 /* 6x255 for inserted strings (%.255s &c in fmt; and %s with limited length arg)
@@ -355,7 +356,9 @@ catch_fatal_error(void)
 void
 print_fatal_error(const char *emsg, const void *data)
 {
-  fprintf(stderr, _("%s: error: %s\n"), dpkg_get_progname(), emsg);
+  fprintf(stderr, "%s%s:%s %s%s:%s %s\n",
+          color_get(COLOR_PROG), dpkg_get_progname(), color_reset(),
+          color_get(COLOR_ERROR), _("error"), color_reset(), emsg);
 }
 
 void
@@ -391,8 +394,9 @@ do_internerr(const char *file, int line, const char *func, const char *fmt, ...)
   vsnprintf(buf, sizeof(buf), fmt, args);
   va_end(args);
 
-  fprintf(stderr, _("%s:%s:%d:%s: internal error: %s\n"),
-          dpkg_get_progname(), file, line, func, buf);
+  fprintf(stderr, "%s%s:%s:%d:%s:%s %s%s:%s %s\n", color_get(COLOR_PROG),
+          dpkg_get_progname(), file, line, func, color_reset(),
+          color_get(COLOR_ERROR), _("internal error"), color_reset(), buf);
 
   abort();
 }

+ 5 - 0
lib/dpkg/libdpkg.map

@@ -27,6 +27,11 @@ local:
 };
 
 LIBDPKG_PRIVATE {
+	# Color handling
+	color_set_mode;
+	color_get;
+	color_reset;
+
 	# Error handling
 	push_error_context_jump;
 	push_error_context_func;

+ 8 - 3
lib/dpkg/report.c

@@ -29,6 +29,7 @@
 #include <dpkg/macros.h>
 #include <dpkg/i18n.h>
 #include <dpkg/progname.h>
+#include <dpkg/color.h>
 #include <dpkg/report.h>
 
 static int piped_mode = _IOLBF;
@@ -63,7 +64,9 @@ warningv(const char *fmt, va_list args)
 
 	warn_count++;
 	vsnprintf(buf, sizeof(buf), fmt, args);
-	fprintf(stderr, _("%s: warning: %s\n"), dpkg_get_progname(), buf);
+	fprintf(stderr, "%s%s:%s %s%s:%s %s\n",
+	        color_get(COLOR_PROG), dpkg_get_progname(), color_reset(),
+	        color_get(COLOR_WARN), _("warning"), color_reset(), buf);
 }
 
 void
@@ -86,7 +89,8 @@ notice(const char *fmt, ...)
 	vsnprintf(buf, sizeof(buf), fmt, args);
 	va_end(args);
 
-	fprintf(stderr, "%s: %s\n", dpkg_get_progname(), buf);
+	fprintf(stderr, "%s%s:%s %s\n",
+	        color_get(COLOR_PROG), dpkg_get_progname(), color_reset(), buf);
 }
 
 void
@@ -99,5 +103,6 @@ info(const char *fmt, ...)
 	vsnprintf(buf, sizeof(buf), fmt, args);
 	va_end(args);
 
-	printf("%s: %s\n", dpkg_get_progname(), buf);
+	printf("%s%s:%s %s\n",
+	       color_get(COLOR_PROG), dpkg_get_progname(), color_reset(), buf);
 }

+ 5 - 0
man/dpkg-buildpackage.1

@@ -388,6 +388,11 @@ If set, it will be used as the active build profile(s) for the package
 being built (since dpkg 1.17.2).
 It is a space separated list of profile names.
 Overridden by the \fB\-P\fP option.
+.TP
+.B DPKG_COLORS
+Sets the color mode (since dpkg 1.18.5).
+The currently accepted values are: \fBauto\fP, \fBalways\fP and
+\fBnever\fP (default).
 
 .SS Reliance on exported environment flags
 Even if \fBdpkg\-buildpackage\fP exports some variables, \fBdebian/rules\fP

+ 5 - 0
man/dpkg.1

@@ -850,6 +850,11 @@ The program \fBdpkg\fP will execute when starting a new interactive shell.
 Sets the number of columns \fBdpkg\fP should use when displaying formatted
 text.
 Currently only used by \fB\-\-list\fP.
+.TP
+.B DPKG_COLORS
+Sets the color mode (since dpkg 1.18.5).
+The currently accepted values are: \fBauto\fP, \fBalways\fP and
+\fBnever\fP (default).
 .SS Internal environment
 .TP
 .B DPKG_SHELL_REASON

+ 3 - 3
scripts/Dpkg/Changelog.pm

@@ -37,7 +37,7 @@ use warnings;
 our $VERSION = '1.00';
 
 use Dpkg::Gettext;
-use Dpkg::ErrorHandling qw(:DEFAULT report);
+use Dpkg::ErrorHandling qw(:DEFAULT report REPORT_WARN);
 use Dpkg::Control;
 use Dpkg::Control::Changelog;
 use Dpkg::Control::Fields;
@@ -166,9 +166,9 @@ sub get_parse_errors {
 	my $res = '';
 	foreach my $e (@{$self->{parse_errors}}) {
 	    if ($e->[3]) {
-		$res .= report(g_('warning'), g_("%s(l%s): %s\nLINE: %s"), @$e);
+		$res .= report(REPORT_WARN, g_("%s(l%s): %s\nLINE: %s"), @$e);
 	    } else {
-		$res .= report(g_('warning'), g_('%s(l%s): %s'), @$e);
+		$res .= report(REPORT_WARN, g_('%s(l%s): %s'), @$e);
 	    }
 	}
 	return $res;

+ 119 - 10
scripts/Dpkg/ErrorHandling.pm

@@ -18,6 +18,15 @@ use warnings;
 
 our $VERSION = '0.02';
 our @EXPORT_OK = qw(
+    REPORT_PROGNAME
+    REPORT_COMMAND
+    REPORT_STATUS
+    REPORT_INFO
+    REPORT_NOTICE
+    REPORT_WARN
+    REPORT_ERROR
+    report_pretty
+    report_color
     report
 );
 our @EXPORT = qw(
@@ -34,12 +43,71 @@ our @EXPORT = qw(
 );
 
 use Exporter qw(import);
+use Term::ANSIColor;
 
 use Dpkg ();
 use Dpkg::Gettext;
 
 my $quiet_warnings = 0;
 my $info_fh = \*STDOUT;
+my $use_color = 0;
+
+sub setup_color
+{
+    my $mode = $ENV{'DPKG_COLORS'} // 'never';
+
+    if ($mode eq 'auto') {
+        ## no critic (InputOutput::ProhibitInteractiveTest)
+        $use_color = 1 if -t *STDOUT or -t *STDERR;
+    } elsif ($mode eq 'always') {
+        $use_color = 1;
+    } else {
+        $use_color = 0;
+    }
+}
+
+setup_color();
+
+use constant {
+    REPORT_PROGNAME => 1,
+    REPORT_COMMAND => 2,
+    REPORT_STATUS => 3,
+    REPORT_INFO => 4,
+    REPORT_NOTICE => 5,
+    REPORT_WARN => 6,
+    REPORT_ERROR => 7,
+};
+
+my %report_mode = (
+    REPORT_PROGNAME() => {
+        color => 'bold',
+    },
+    REPORT_COMMAND() => {
+        color => 'bold magenta',
+    },
+    REPORT_STATUS() => {
+        color => 'clear',
+        # We do not translate this name because the untranslated output is
+        # part of the interface.
+        name => 'status',
+    },
+    REPORT_INFO() => {
+        color => 'green',
+        name => g_('info'),
+    },
+    REPORT_NOTICE() => {
+        color => 'yellow',
+        name => g_('notice'),
+    },
+    REPORT_WARN() => {
+        color => 'bold yellow',
+        name => g_('warning'),
+    },
+    REPORT_ERROR() => {
+        color => 'bold red',
+        name => g_('error'),
+    },
+);
 
 sub report_options
 {
@@ -53,50 +121,91 @@ sub report_options
     }
 }
 
+sub report_name
+{
+    my $type = shift;
+
+    return $report_mode{$type}{name} // '';
+}
+
+sub report_color
+{
+    my $type = shift;
+
+    return $report_mode{$type}{color} // 'clear';
+}
+
+sub report_pretty
+{
+    my ($msg, $color) = @_;
+
+    if ($use_color) {
+        return colored($msg, $color);
+    } else {
+        return $msg;
+    }
+}
+
+sub _progname_prefix
+{
+    return report_pretty("$Dpkg::PROGNAME: ", report_color(REPORT_PROGNAME));
+}
+
+sub _typename_prefix
+{
+    my $type = shift;
+
+    return report_pretty(report_name($type), report_color($type));
+}
+
 sub report(@)
 {
     my ($type, $msg) = (shift, shift);
 
     $msg = sprintf($msg, @_) if (@_);
-    return "$Dpkg::PROGNAME: $type: $msg\n";
+
+    my $progname = _progname_prefix();
+    my $typename = _typename_prefix($type);
+
+    return "$progname$typename: $msg\n";
 }
 
 sub info($;@)
 {
-    print { $info_fh } report(g_('info'), @_) if (!$quiet_warnings);
+    print { $info_fh } report(REPORT_INFO, @_) if not $quiet_warnings;
 }
 
 sub notice
 {
-    warn report(g_('notice'), @_) if not $quiet_warnings;
+    warn report(REPORT_NOTICE, @_) if not $quiet_warnings;
 }
 
 sub warning($;@)
 {
-    warn report(g_('warning'), @_) if (!$quiet_warnings);
+    warn report(REPORT_WARN, @_) if not $quiet_warnings;
 }
 
 sub syserr($;@)
 {
     my $msg = shift;
-    die report(g_('error'), "$msg: $!", @_);
+    die report(REPORT_ERROR, "$msg: $!", @_);
 }
 
 sub error($;@)
 {
-    die report(g_('error'), @_);
+    die report(REPORT_ERROR, @_);
 }
 
 sub errormsg($;@)
 {
-    print { *STDERR } report(g_('error'), @_);
+    print { *STDERR } report(REPORT_ERROR, @_);
 }
 
 sub printcmd
 {
     my (@cmd) = @_;
 
-    print { *STDERR } " @cmd\n";
+    print { *STDERR } report_pretty(" @cmd\n", report_color(REPORT_COMMAND));
 }
 
 sub subprocerr(@)
@@ -123,8 +232,8 @@ sub usageerr(@)
     my ($msg) = (shift);
 
     $msg = sprintf($msg, @_) if (@_);
-    warn "$Dpkg::PROGNAME: $msg\n\n";
-    warn "$printforhelp\n";
+    warn report(REPORT_ERROR, $msg);
+    warn "\n$printforhelp\n";
     exit(2);
 }
 

+ 5 - 5
scripts/dpkg-buildflags.pl

@@ -23,7 +23,7 @@ use warnings;
 
 use Dpkg ();
 use Dpkg::Gettext;
-use Dpkg::ErrorHandling qw(:DEFAULT report);
+use Dpkg::ErrorHandling qw(:DEFAULT report REPORT_STATUS);
 use Dpkg::Build::Env;
 use Dpkg::BuildFlags;
 use Dpkg::Vendor qw(get_current_vendor);
@@ -155,12 +155,12 @@ if ($action eq 'list') {
     my @envvars = Dpkg::Build::Env::list_accessed();
     for my $envvar (@envvars) {
 	if (exists $ENV{$envvar}) {
-	    printf report('status', 'environment variable %s=%s',
+	    printf report(REPORT_STATUS, 'environment variable %s=%s',
 	           $envvar, $ENV{$envvar});
 	}
     }
     my $vendor = Dpkg::Vendor::get_current_vendor() || 'undefined';
-    print report('status', "vendor is $vendor");
+    print report(REPORT_STATUS, "vendor is $vendor");
     # Then the resulting features:
     foreach my $area (sort $build_flags->get_feature_areas()) {
 	my $fs;
@@ -168,13 +168,13 @@ if ($action eq 'list') {
 	foreach my $feature (sort keys %features) {
 	    $fs .= sprintf(' %s=%s', $feature, $features{$feature} ? 'yes' : 'no');
 	}
-	print report('status', "$area features:$fs");
+	print report(REPORT_STATUS, "$area features:$fs");
     }
     # Then the resulting values (with their origin):
     foreach my $flag ($build_flags->list()) {
 	my $value = $build_flags->get($flag);
 	my $origin = $build_flags->get_origin($flag);
 	my $maintainer = $build_flags->is_maintainer_modified($flag) ? '+maintainer' : '';
-	print report('status', "$flag [$origin$maintainer]: $value");
+	print report(REPORT_STATUS, "$flag [$origin$maintainer]: $value");
     }
 }