Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions changelog/68982.added.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
Added ``_color`` modifier for ``state_output``: setting ``state_output`` to
``full_color``, ``terse_color``, ``mixed_color``, ``changes_color``, or
``filter_color`` enables colorized unified diff output in the highstate
outputter. Added lines are green, removed lines are red, hunk headers
(``@@``) are cyan, file headers (``---``) are red, and context lines are
gray. All other behavior is identical to the base mode without ``_color``.
15 changes: 13 additions & 2 deletions doc/ref/cli/_includes/output-options.rst
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,19 @@ Output Options
.. option:: --state-output=STATE_OUTPUT, --state_output=STATE_OUTPUT

Override the configured state_output value for minion
output. One of 'full', 'terse', 'mixed', 'changes' or
'filter'. Default: 'none'.
output. One of ``full``, ``terse``, ``mixed``, ``changes`` or
``filter``. Default: ``none``.

Each mode accepts two optional suffixes:

* ``_id`` — use the state ID as the display name instead of the state's
``name`` value (e.g. ``full_id``).
* ``_color`` — colorize unified diffs in the changes section: added lines
green, removed lines red, hunk headers (``@@``) cyan, file headers
(``---``) red, context lines gray (e.g. ``full_color``).

The two suffixes can be combined in either order, e.g. ``full_id_color``
or ``full_color_id``.

.. option:: --state-verbose=STATE_VERBOSE, --state_verbose=STATE_VERBOSE

Expand Down
10 changes: 10 additions & 0 deletions doc/ref/configuration/master.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3072,10 +3072,20 @@ The state_output setting controls which results will be output full multi line:
``full_id``, ``mixed_id``, ``changes_id`` and ``terse_id`` are also allowed;
when set, the state ID will be used as name in the output.

Any of the above modes can be suffixed with ``_color`` (e.g. ``full_color``,
``mixed_color``) to enable colorized unified diff output in the changes
section. Added lines are shown in green, removed lines in red, hunk headers
in cyan, and context lines in gray. All other output behavior is identical to
the mode without the ``_color`` suffix.

.. code-block:: yaml

state_output: full

.. code-block:: yaml

state_output: full_color

.. conf_master:: state_output_diff

``state_output_diff``
Expand Down
10 changes: 10 additions & 0 deletions doc/ref/configuration/minion.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2408,10 +2408,20 @@ The state_output setting controls which results will be output full multi line:
``full_id``, ``mixed_id``, ``changes_id`` and ``terse_id`` are also allowed;
when set, the state ID will be used as name in the output.

Any of the above modes can be suffixed with ``_color`` (e.g. ``full_color``,
``mixed_color``) to enable colorized unified diff output in the changes
section. Added lines are shown in green, removed lines in red, hunk headers
in cyan, and context lines in gray. All other output behavior is identical to
the mode without the ``_color`` suffix.

.. code-block:: yaml

state_output: full

.. code-block:: yaml

state_output: full_color

.. conf_minion:: state_output_diff

``state_output_diff``
Expand Down
146 changes: 143 additions & 3 deletions salt/output/highstate.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,13 +44,22 @@
These can be set as such from the command line, or in the Salt config as
`state_output_exclude` or `state_output_terse`, respectively.

The output modes have one modifier:
The output modes have two modifiers that can be combined:

``full_id``, ``terse_id``, ``mixed_id``, ``changes_id`` and ``filter_id``
If ``_id`` is used, then the corresponding form will be used, but the value for ``name``
will be drawn from the state ID. This is useful for cases where the name
value might be very long and hard to read.

``full_color``, ``terse_color``, ``mixed_color``, ``changes_color`` and ``filter_color``
If ``_color`` is used, unified diffs in the changes section will be
colorized: added lines in green, removed lines in red, hunk headers
(``@@``) in cyan, file headers (``---``) in red, and context lines in
gray. All other output behavior is identical to the base mode.

The ``_id`` and ``_color`` modifiers can be combined, e.g. ``full_id_color``
or ``full_color_id``.

state_tabular:
If `state_output` uses the terse output, set this to `True` for an aligned
output format. If you wish to use a custom format, this can be set to a
Expand Down Expand Up @@ -479,6 +488,9 @@ def _format_host(host, data, indent_level=1):
tcolor = colors["LIGHT_YELLOW"]

state_output = __opts__.get("state_output", "full").lower()
# Strip the _color modifier before all mode comparisons so that
# e.g. "full_color" behaves identically to "full" for layout purposes.
state_output = state_output.replace("_color", "")
comps = tname.split("_|-")

if state_output.endswith("_id"):
Expand Down Expand Up @@ -742,23 +754,148 @@ def _counts(label, count):
return "\n".join(hstrs), nchanges > 0


def _render_diff(diff_str, indent):
"""
Render a unified diff string with per-line ANSI colorization.

Each line is colored according to its unified-diff role:
``---`` / ``+++`` (file headers) -> LIGHT_RED (bold)
``@@`` (hunk header) -> CYAN
``+`` (added line) -> GREEN
``-`` (removed line) -> RED
context lines (leading space) -> GREEN (same as other change values)

The ``indent`` argument (an integer) is prepended as spaces to every line,
matching the nesting depth used by the surrounding nested outputter output.
"""
prefix = " " * indent

if __opts__.get("color") is False:
return "\n".join(prefix + line for line in diff_str.splitlines())

colors = salt.utils.color.get_colors(True, __opts__.get("color_theme"))
GREEN = str(colors["GREEN"])
ENDC = str(colors["ENDC"])
RED = str(colors["RED"])
CYAN = str(colors["CYAN"])
WHITE = str(colors["LIGHT_GRAY"])
LIGHT_RED = str(colors["LIGHT_RED"])

result = []
for line in diff_str.splitlines():
if line.startswith("---"):
color = LIGHT_RED
elif line.startswith("+++"):
color = GREEN
elif line.startswith("@@"):
color = CYAN
elif line.startswith("+"):
color = GREEN
elif line.startswith("-"):
color = RED
else:
color = WHITE
result.append(f"{prefix}{color}{line}{ENDC}")
return "\n".join(result)


def _render_changes_dict(changes, indent):
"""
Render a changes dict as indented lines, mirroring nested outputter style.

Does not go through the Salt loader, so nested_indent is guaranteed to
apply correctly regardless of Salt version. Returns a list of strings
(no trailing newline).
"""
colors = salt.utils.color.get_colors(
__opts__.get("color"), __opts__.get("color_theme")
)
CYAN = str(colors["CYAN"])
GREEN = str(colors["GREEN"])
ENDC = str(colors["ENDC"])

val_indent = indent + 4
pad = " " * indent
val_pad = " " * val_indent
lines = []
# Top-level separator (mirrors what NestDisplay.display does for Mapping at indent>0)
lines.append(f"{pad}{CYAN}----------{ENDC}")
for key in sorted(changes):
lines.append(f"{pad}{CYAN}{key}:{ENDC}")
val = changes[key]
if isinstance(val, str):
lines.extend(f"{val_pad}{GREEN}{line}{ENDC}" for line in val.splitlines())
elif isinstance(val, dict):
lines.extend(_render_changes_dict(val, val_indent))
else:
lines.append(f"{val_pad}{GREEN}{val}{ENDC}")
return lines


def _nested_changes(changes):
"""
Print the changes data using the nested outputter
Print the changes data using the nested outputter.
"""
ret = "\n"
ret += salt.output.out_format(changes, "nested", __opts__, nested_indent=14)
return ret


def _nested_changes_colorized(changes):
"""
Print the changes data with diff colorization (used when state_output
contains the ``_color`` modifier, e.g. ``full_color``).

If the changes dict contains a ``diff`` key whose value is a string, that
diff is rendered with per-line color (added=green, removed=red, etc.).
All other values are rendered by ``_render_changes_dict`` which mirrors the
nested outputter layout without going through the Salt loader, ensuring
correct indentation on all Salt versions.
"""
diff_str = None
if isinstance(changes, dict) and isinstance(changes.get("diff"), str):
diff_str = changes.pop("diff")

# key_indent=14: "----------" separator and key names sit at 14 spaces.
# val_indent=18: string values sit 4 spaces deeper.
key_indent = 14
val_indent = key_indent + 4

colors = salt.utils.color.get_colors(
__opts__.get("color"), __opts__.get("color_theme")
)
CYAN = str(colors["CYAN"])
ENDC = str(colors["ENDC"])

ret = "\n"
if changes:
ret += "\n".join(_render_changes_dict(changes, key_indent))
elif diff_str is not None:
# No other keys: emit the separator manually.
ret += f"{' ' * key_indent}{CYAN}----------{ENDC}"

if diff_str is not None:
key_line = f"{' ' * key_indent}{CYAN}diff:{ENDC}"
rendered_diff = _render_diff(diff_str, val_indent)
ret += "\n" + key_line + "\n" + rendered_diff
# Restore the diff key so the caller's data structure is unchanged.
changes["diff"] = diff_str

return ret


def _format_changes(changes, orchestration=False):
"""
Format the changes dict based on what the data is
"""
if not changes:
return False, ""

colorize = "_color" in __opts__.get("state_output", "").lower()

if orchestration:
if colorize:
return True, _nested_changes_colorized(changes)
return True, _nested_changes(changes)

if not isinstance(changes, dict):
Expand All @@ -774,7 +911,10 @@ def _format_changes(changes, orchestration=False):
changed = changed or c
else:
changed = True
ctext = _nested_changes(changes)
if colorize:
ctext = _nested_changes_colorized(changes)
else:
ctext = _nested_changes(changes)
return changed, ctext


Expand Down
Loading
Loading