Skip to content

Commit e83a2e1

Browse files
authored
CLI/Display: Add support for NO_COLOR and CLICOLOR (#428) (#432)
Adds support for NO_COLOR and CLICOLOR specification standard for ANSI colors in terminals. --color option takes precedence over environment variables. It also takes care of configuration file options. Changes THREE_CHOICES index in order to test things correctly. Closes #428. Signed-off-by: Olivier DELHOMME <[email protected]>
1 parent 9fb1e34 commit e83a2e1

File tree

10 files changed

+99
-40
lines changed

10 files changed

+99
-40
lines changed

doc/man/man1/clush.1

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -238,7 +238,7 @@ fold nodeset using node groups
238238
return the largest of command return codes
239239
.TP
240240
.BI \-\-color\fB= WHENCOLOR
241-
whether to use ANSI colors to surround node or nodeset prefix/header with escape sequences to display them in color on the terminal. \fIWHENCOLOR\fP is \fBnever\fP, \fBalways\fP or \fBauto\fP (which use color if standard output/error refer to a terminal). Colors are set to [34m (blue foreground text) for stdout and [31m (red foreground text) for stderr, and cannot be modified.
241+
\fBclush\fP can use NO_COLOR, CLICOLOR and CLICOLOR_FORCE environment variables. NO_COLOR takes precedence over CLICOLOR_FORCE which takes precedence over CLICOLOR. When \fB\-\-color\fP option is used these environment variables are not taken into account. \fB\-\-color\fP tells whether to use ANSI colors to surround node or nodeset prefix/header with escape sequences to display them in color on the terminal. \fIWHENCOLOR\fP is \fBnever\fP, \fBalways\fP or \fBauto\fP (which use color if standard output/error refer to a terminal). Colors are set to [34m (blue foreground text) for stdout and [31m (red foreground text) for stderr, and cannot be modified.
242242
.TP
243243
.B \-\-diff
244244
show diff between common outputs (find the best reference output by focusing on largest nodeset and also smaller command return code)

doc/sphinx/tools/clush.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -589,6 +589,8 @@ output nodeset header::
589589
---------------
590590
ok
591591

592+
NO_COLOR, CLICOLOR_FORCE and CLICOLOR environment variables can also
593+
be used to change the way *clush* uses colors to display messages.
592594

593595
.. _clush-worker:
594596

doc/txt/clubak.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ OPTIONS
5757
node / line content separator string (default: `:`)
5858
-F, --fast faster but memory hungry mode (preload all messages per node)
5959
-T, --tree message tree trace mode; switch to enable ``ClusterShell.MsgTree`` trace mode, all keys/nodes being kept for each message element of the tree, thus allowing special output gathering
60-
--color=WHENCOLOR whether to use ANSI colors to surround node or nodeset prefix/header with escape sequences to display them in color on the terminal. *WHENCOLOR* is ``never``, ``always`` or ``auto`` (which use color if standard output refers to a terminal). Color is set to [34m (blue foreground text) and cannot be modified.
60+
--color=WHENCOLOR ``clush`` can use NO_COLOR, CLICOLOR and CLICOLOR_FORCE environment variables. ``--color`` command line option always takes precedence over environment variables. NO_COLOR takes precedence over CLICOLOR_FORCE which takes precedence over CLICOLOR. ``--color`` tells whether to use ANSI colors to surround node or nodeset prefix/header with escape sequences to display them in color on the terminal. *WHENCOLOR* is ``never``, ``always`` or ``auto`` (which use color if standard output refers to a terminal). Color is set to [34m (blue foreground text) and cannot be modified.
6161
--diff show diff between gathered outputs
6262

6363

doc/txt/clush.conf.txt

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -54,12 +54,16 @@ command_timeout
5454
ClusterShell library ensures that any commands complete in less than
5555
( connect_timeout + command_timeout ). If set to *0*, no timeout occurs.
5656
color
57-
Whether to use ANSI colors to surround node or nodeset prefix/header with
58-
escape sequences to display them in color on the terminal. Valid arguments
59-
are ``never``, ``always`` or ``auto`` (which use color if standard
60-
output/error refer to a terminal). Colors are set to [34m (blue foreground
61-
text) for stdout and [31m (red foreground text) for stderr, and cannot be
62-
modified.
57+
``clush`` can use NO_COLOR, CLICOLOR and CLICOLOR_FORCE
58+
environment variables. NO_COLOR takes precedence over CLICOLOR_FORCE which
59+
takes precedence over CLICOLOR. When the option is set in configuration file
60+
environment variables are taken into account only with `auto` argument.
61+
``color`` tells whether to use ANSI colors to surround node or nodeset
62+
prefix/header with escape sequences to display them in color on the terminal.
63+
Valid arguments are ``never``, ``always`` or ``auto`` (which use color if
64+
standard output/error refer to a terminal). Colors are set to [34m (blue
65+
foreground text) for stdout and [31m (red foreground text) for stderr, and
66+
cannot be modified.
6367
fd_max
6468
Maximum number of open file descriptors permitted per clush process (soft
6569
resource limit for open files). This limit can never exceed the system

doc/txt/clush.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,7 @@ Output behaviour:
160160
-B like -b but including standard error
161161
-r, --regroup fold nodeset using node groups
162162
-S, --maxrc return the largest of command return codes
163-
--color=WHENCOLOR whether to use ANSI colors to surround node or nodeset prefix/header with escape sequences to display them in color on the terminal. *WHENCOLOR* is ``never``, ``always`` or ``auto`` (which use color if standard output/error refer to a terminal). Colors are set to [34m (blue foreground text) for stdout and [31m (red foreground text) for stderr, and cannot be modified.
163+
--color=WHENCOLOR ``clush`` can use NO_COLOR, CLICOLOR and CLICOLOR_FORCE environment variables. NO_COLOR takes precedence over CLICOLOR_FORCE which takes precedence over CLICOLOR. When ``--color`` option is used these environment variables are not taken into account. ``--color`` tells whether to use ANSI colors to surround node or nodeset prefix/header with escape sequences to display them in color on the terminal. *WHENCOLOR* is ``never``, ``always`` or ``auto`` (which use color if standard output/error refer to a terminal). Colors are set to [34m (blue foreground text) for stdout and [31m (red foreground text) for stderr, and cannot be modified.
164164
--diff show diff between common outputs (find the best reference output by focusing on largest nodeset and also smaller command return code)
165165

166166
File copying:

lib/ClusterShell/CLI/Clubak.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ def clubak():
107107
if options.interpret_keys == THREE_CHOICES[-1]: # auto?
108108
enable_nodeset_key = None # AUTO
109109
else:
110-
enable_nodeset_key = (options.interpret_keys == THREE_CHOICES[1])
110+
enable_nodeset_key = (options.interpret_keys == THREE_CHOICES[2])
111111

112112
# Create new message tree
113113
if options.trace_mode:

lib/ClusterShell/CLI/Config.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ class ClushConfig(configparser.ConfigParser, object):
5353
"connect_timeout": "%f" % DEFAULTS.connect_timeout,
5454
"command_timeout": "%f" % DEFAULTS.command_timeout,
5555
"history_size": "100",
56-
"color": THREE_CHOICES[-1], # auto
56+
"color": THREE_CHOICES[0], # ''
5757
"verbosity": "%d" % VERB_STD,
5858
"node_count": "yes",
5959
"maxrc": "no",

lib/ClusterShell/CLI/Display.py

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525

2626
import difflib
2727
import sys
28+
import os
2829

2930
from ClusterShell.NodeSet import NodeSet
3031

@@ -33,7 +34,7 @@
3334
VERB_STD = 1
3435
VERB_VERB = 2
3536
VERB_DEBUG = 3
36-
THREE_CHOICES = ["never", "always", "auto"]
37+
THREE_CHOICES = ["", "never", "always", "auto"]
3738
WHENCOLOR_CHOICES = THREE_CHOICES # deprecated; use THREE_CHOICES
3839

3940
if sys.getdefaultencoding() == 'ascii':
@@ -98,6 +99,18 @@ def __init__(self, options, config=None, color=None):
9899
# display may change when 'max return code' option is set
99100
self.maxrc = getattr(options, 'maxrc', False)
100101

102+
# Be compliant with NO_COLOR and CLI_COLORS trying to solve #428
103+
# See https://no-color.org/ and https://bixense.com/clicolors/
104+
# NO_COLOR takes precedence over CLI_COLORS. --color option always
105+
# takes precedence over any environment variable.
106+
107+
if options.whencolor is None:
108+
if (config is None) or (config.color == '' or config.color == 'auto'):
109+
if 'NO_COLOR' not in os.environ:
110+
color = self._has_cli_color()
111+
else:
112+
color = False
113+
101114
if color is None:
102115
# Should we use ANSI colors?
103116
color = False
@@ -137,6 +150,26 @@ def __init__(self, options, config=None, color=None):
137150
if hasattr(options, 'debug') and options.debug:
138151
self.verbosity = VERB_DEBUG
139152

153+
def _has_cli_color(self):
154+
"""Tests CLICOLOR environment variable to determine wether to
155+
use color or not on output."""
156+
# When CLICOLOR_FORCE is set to something else than 0
157+
# colors must be used.
158+
if os.getenv("CLICOLOR_FORCE", "0") != "0":
159+
return True
160+
161+
cli_color = os.getenv("CLICOLOR")
162+
163+
if cli_color is None:
164+
return None
165+
elif cli_color != "0":
166+
# CLICOLOR is set and colored output should
167+
# be used if stdout is a tty
168+
return sys.stdout.isatty()
169+
else:
170+
# CLICOLOR is set to not display colors.
171+
return False
172+
140173
def flush(self):
141174
"""flush display object buffers"""
142175
# only used to reset diff display for now

tests/CLIConfigTest.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ def testClushConfigEmpty(self):
3535
parser.install_connector_options()
3636
options, _ = parser.parse_args([])
3737
config = ClushConfig(options, filename=f.name)
38-
self.assertEqual(config.color, WHENCOLOR_CHOICES[-1])
38+
self.assertEqual(config.color, THREE_CHOICES[0])
3939
self.assertEqual(config.verbosity, VERB_STD)
4040
self.assertEqual(config.fanout, 64)
4141
self.assertEqual(config.maxrc, False)
@@ -59,7 +59,7 @@ def testClushConfigAlmostEmpty(self):
5959
parser.install_connector_options()
6060
options, _ = parser.parse_args([])
6161
config = ClushConfig(options, filename=f.name)
62-
self.assertEqual(config.color, WHENCOLOR_CHOICES[-1])
62+
self.assertEqual(config.color, THREE_CHOICES[0])
6363
self.assertEqual(config.verbosity, VERB_STD)
6464
self.assertEqual(config.maxrc, False)
6565
self.assertEqual(config.node_count, True)
@@ -96,7 +96,7 @@ def testClushConfigDefault(self):
9696
display = Display(options, config)
9797
display.vprint(VERB_STD, "test")
9898
display.vprint(VERB_DEBUG, "shouldn't see this")
99-
self.assertEqual(config.color, WHENCOLOR_CHOICES[2])
99+
self.assertEqual(config.color, THREE_CHOICES[-1])
100100
self.assertEqual(config.verbosity, VERB_STD)
101101
self.assertEqual(config.maxrc, False)
102102
self.assertEqual(config.node_count, True)
@@ -134,7 +134,7 @@ def testClushConfigFull(self):
134134
parser.install_connector_options()
135135
options, _ = parser.parse_args([])
136136
config = ClushConfig(options, filename=f.name)
137-
self.assertEqual(config.color, WHENCOLOR_CHOICES[2])
137+
self.assertEqual(config.color, THREE_CHOICES[-1])
138138
self.assertEqual(config.verbosity, VERB_STD)
139139
self.assertEqual(config.maxrc, True)
140140
self.assertEqual(config.node_count, True)
@@ -298,7 +298,7 @@ def testClushConfigDefaultWithOptions(self):
298298
display = Display(options, config)
299299
display.vprint(VERB_STD, "test")
300300
display.vprint(VERB_DEBUG, "test")
301-
self.assertEqual(config.color, WHENCOLOR_CHOICES[1])
301+
self.assertEqual(config.color, THREE_CHOICES[2])
302302
self.assertEqual(config.verbosity, VERB_DEBUG) # takes biggest
303303
self.assertEqual(config.fanout, 36)
304304
self.assertEqual(config.connect_timeout, 7)
@@ -360,7 +360,7 @@ def testClushConfigUserOverride(self):
360360
parser.install_connector_options()
361361
options, _ = parser.parse_args([])
362362
config = ClushConfig(options) # filename=None to use defaults!
363-
self.assertEqual(config.color, WHENCOLOR_CHOICES[0])
363+
self.assertEqual(config.color, THREE_CHOICES[1])
364364
self.assertEqual(config.verbosity, VERB_VERB) # takes biggest
365365
self.assertEqual(config.fanout, 42)
366366
self.assertEqual(config.connect_timeout, 14)

tests/CLIDisplayTest.py

Lines changed: 42 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,10 @@
55

66
import tempfile
77
import unittest
8+
import os
89
from io import BytesIO
910

10-
from ClusterShell.CLI.Display import Display, WHENCOLOR_CHOICES, VERB_STD
11+
from ClusterShell.CLI.Display import Display, THREE_CHOICES, VERB_STD
1112
from ClusterShell.CLI.OptionParser import OptionParser
1213

1314
from ClusterShell.MsgTree import MsgTree
@@ -38,27 +39,46 @@ def testDisplay(self):
3839
mtree.add("hostfoo", b"message0")
3940
mtree.add("hostfoo", b"message1")
4041

41-
for whencolor in WHENCOLOR_CHOICES: # test whencolor switch
42-
for label in [True, False]: # test no-label switch
43-
options.label = label
44-
options.whencolor = whencolor
45-
disp = Display(options)
46-
# inhibit output
47-
disp.out = BytesIO()
48-
disp.err = BytesIO()
49-
# test print_* methods...
50-
disp.print_line(ns, b"foo bar")
51-
disp.print_line_error(ns, b"foo bar")
52-
disp.print_gather(ns, list(mtree.walk())[0][0])
53-
# test also string nodeset as parameter
54-
disp.print_gather("hostfoo", list(mtree.walk())[0][0])
55-
# test line_mode property
56-
self.assertEqual(disp.line_mode, False)
57-
disp.line_mode = True
58-
self.assertEqual(disp.line_mode, True)
59-
disp.print_gather("hostfoo", list(mtree.walk())[0][0])
60-
disp.line_mode = False
61-
self.assertEqual(disp.line_mode, False)
42+
list_env_vars = []
43+
list_env_vars.append(dict())
44+
list_env_vars.append(dict(NO_COLOR='0'))
45+
list_env_vars.append(dict(CLICOLOR='0'))
46+
list_env_vars.append(dict(CLICOLOR='1'))
47+
list_env_vars.append(dict(CLICOLOR='0', CLICOLOR_FORCE='0'))
48+
list_env_vars.append(dict(CLICOLOR_FORCE='1'))
49+
50+
for env_vars in list_env_vars:
51+
for var_name in env_vars:
52+
var_value = env_vars[var_name]
53+
os.environ[var_name] = var_value
54+
55+
for whencolor in THREE_CHOICES: # test whencolor switch
56+
if whencolor == "":
57+
options.whencolor = None
58+
else:
59+
options.whencolor = whencolor
60+
for label in [True, False]: # test no-label switch
61+
options.label = label
62+
disp = Display(options)
63+
# inhibit output
64+
disp.out = BytesIO()
65+
disp.err = BytesIO()
66+
# test print_* methods...
67+
disp.print_line(ns, b"foo bar")
68+
disp.print_line_error(ns, b"foo bar")
69+
disp.print_gather(ns, list(mtree.walk())[0][0])
70+
# test also string nodeset as parameter
71+
disp.print_gather("hostfoo", list(mtree.walk())[0][0])
72+
# test line_mode property
73+
self.assertEqual(disp.line_mode, False)
74+
disp.line_mode = True
75+
self.assertEqual(disp.line_mode, True)
76+
disp.print_gather("hostfoo", list(mtree.walk())[0][0])
77+
disp.line_mode = False
78+
self.assertEqual(disp.line_mode, False)
79+
80+
for var_name in env_vars:
81+
os.environ.pop(var_name)
6282

6383
def testDisplayRegroup(self):
6484
"""test CLI.Display (regroup)"""

0 commit comments

Comments
 (0)