From 21eb90fb5fbc939eb68262efc0e916293d8299d2 Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Sat, 10 Jan 2026 15:31:36 -0800 Subject: [PATCH 1/7] tools: ynl: cli: introduce formatting for attr names in --list-attrs It's a little hard to make sense of the output of --list-attrs, it looks like a wall of text. Sprinkle a little bit of formatting - make op and attr names bold, and Enum: / Flags: keywords italics. Tested-by: Gal Pressman Acked-by: Stanislav Fomichev Reviewed-by: Donald Hunter Link: https://patch.msgid.link/20260110233142.3921386-2-kuba@kernel.org Signed-off-by: Jakub Kicinski --- tools/net/ynl/pyynl/cli.py | 30 +++++++++++++++++++++++++++--- 1 file changed, 27 insertions(+), 3 deletions(-) diff --git a/tools/net/ynl/pyynl/cli.py b/tools/net/ynl/pyynl/cli.py index 5fee45e48bbf..aa50d42e35ac 100755 --- a/tools/net/ynl/pyynl/cli.py +++ b/tools/net/ynl/pyynl/cli.py @@ -20,6 +20,29 @@ from lib import YnlFamily, Netlink, NlError, SpecFamily, SpecException, YnlExcep SYS_SCHEMA_DIR='/usr/share/ynl' RELATIVE_SCHEMA_DIR='../../../../Documentation/netlink' +# pylint: disable=too-few-public-methods,too-many-locals +class Colors: + """ANSI color and font modifier codes""" + RESET = '\033[0m' + + BOLD = '\033[1m' + ITALICS = '\033[3m' + UNDERLINE = '\033[4m' + INVERT = '\033[7m' + + +def color(text, modifiers): + """Add color to text if output is a TTY + + Returns: + Colored text if stdout is a TTY, otherwise plain text + """ + if sys.stdout.isatty(): + # Join the colors if they are a list, if it's a string this a noop + modifiers = "".join(modifiers) + return f"{modifiers}{text}{Colors.RESET}" + return text + def schema_dir(): """ Return the effective schema directory, preferring in-tree before @@ -60,7 +83,7 @@ def print_attr_list(ynl, attr_names, attr_set, indent=2): for attr_name in attr_names: if attr_name in attr_set.attrs: attr = attr_set.attrs[attr_name] - attr_info = f'{prefix}- {attr_name}: {attr.type}' + attr_info = f'{prefix}- {color(attr_name, Colors.BOLD)}: {attr.type}' if 'enum' in attr.yaml: enum_name = attr.yaml['enum'] attr_info += f" (enum: {enum_name})" @@ -68,7 +91,8 @@ def print_attr_list(ynl, attr_names, attr_set, indent=2): if enum_name in ynl.consts: const = ynl.consts[enum_name] enum_values = list(const.entries.keys()) - attr_info += f"\n{prefix} {const.type.capitalize()}: {', '.join(enum_values)}" + type_fmted = color(const.type.capitalize(), Colors.ITALICS) + attr_info += f"\n{prefix} {type_fmted}: {', '.join(enum_values)}" # Show nested attributes reference and recursively display them nested_set_name = None @@ -226,7 +250,7 @@ def main(): print(f'Operation {args.list_attrs} not found') sys.exit(1) - print(f'Operation: {op.name}') + print(f'Operation: {color(op.name, Colors.BOLD)}') print(op.yaml['doc']) for mode in ['do', 'dump', 'event']: From 101a7d57d518c0c9e9eefc3768909ae02b96b3ef Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Sat, 10 Jan 2026 15:31:37 -0800 Subject: [PATCH 2/7] tools: ynl: cli: wrap the doc text if it's long We already use textwrap when printing "doc" section about an attribute, but only to indent the text. Switch to using fill() to split and indent all the lines. While at it indent the text by 2 more spaces, so that it doesn't align with the name of the attribute. Before (I'm drawing a "box" at ~60 cols here, in an attempt for clarity): | - irq-suspend-timeout: uint | | The timeout, in nanoseconds, of how long to suspend irq| |processing, if event polling finds events | After: | - irq-suspend-timeout: uint | | The timeout, in nanoseconds, of how long to suspend | | irq processing, if event polling finds events | Tested-by: Gal Pressman Acked-by: Stanislav Fomichev Reviewed-by: Donald Hunter Link: https://patch.msgid.link/20260110233142.3921386-3-kuba@kernel.org Signed-off-by: Jakub Kicinski --- tools/net/ynl/pyynl/cli.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/tools/net/ynl/pyynl/cli.py b/tools/net/ynl/pyynl/cli.py index aa50d42e35ac..dc84619e5518 100755 --- a/tools/net/ynl/pyynl/cli.py +++ b/tools/net/ynl/pyynl/cli.py @@ -10,6 +10,7 @@ import json import os import pathlib import pprint +import shutil import sys import textwrap @@ -101,7 +102,11 @@ def print_attr_list(ynl, attr_names, attr_set, indent=2): attr_info += f" -> {nested_set_name}" if attr.yaml.get('doc'): - doc_text = textwrap.indent(attr.yaml['doc'], prefix + ' ') + doc_prefix = prefix + ' ' * 4 + term_width = shutil.get_terminal_size().columns + doc_text = textwrap.fill(attr.yaml['doc'], width=term_width, + initial_indent=doc_prefix, + subsequent_indent=doc_prefix) attr_info += f"\n{doc_text}" print(attr_info) From 1b7fbf62ad8b404e55d195021724067b5122b630 Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Sat, 10 Jan 2026 15:31:38 -0800 Subject: [PATCH 3/7] tools: ynl: cli: improve --help Improve the clarity of --help. Reorder, provide some grouping and add help messages to most of the options. No functional changes intended. Tested-by: Gal Pressman Acked-by: Stanislav Fomichev Reviewed-by: Donald Hunter Link: https://patch.msgid.link/20260110233142.3921386-4-kuba@kernel.org Signed-off-by: Jakub Kicinski --- tools/net/ynl/pyynl/cli.py | 107 ++++++++++++++++++++++++------------- 1 file changed, 69 insertions(+), 38 deletions(-) diff --git a/tools/net/ynl/pyynl/cli.py b/tools/net/ynl/pyynl/cli.py index dc84619e5518..3aa1f1e816bf 100755 --- a/tools/net/ynl/pyynl/cli.py +++ b/tools/net/ynl/pyynl/cli.py @@ -151,47 +151,78 @@ def main(): """ parser = argparse.ArgumentParser(description=description, - epilog=epilog) - spec_group = parser.add_mutually_exclusive_group(required=True) - spec_group.add_argument('--family', dest='family', type=str, - help='name of the netlink FAMILY') - spec_group.add_argument('--list-families', action='store_true', - help='list all netlink families supported by YNL (has spec)') - spec_group.add_argument('--spec', dest='spec', type=str, - help='choose the family by SPEC file path') + epilog=epilog, add_help=False) - parser.add_argument('--schema', dest='schema', type=str) - parser.add_argument('--no-schema', action='store_true') - parser.add_argument('--json', dest='json_text', type=str) + gen_group = parser.add_argument_group('General options') + gen_group.add_argument('-h', '--help', action='help', + help='show this help message and exit') - group = parser.add_mutually_exclusive_group() - group.add_argument('--do', dest='do', metavar='DO-OPERATION', type=str) - group.add_argument('--multi', dest='multi', nargs=2, action='append', - metavar=('DO-OPERATION', 'JSON_TEXT'), type=str) - group.add_argument('--dump', dest='dump', metavar='DUMP-OPERATION', type=str) - group.add_argument('--list-ops', action='store_true') - group.add_argument('--list-msgs', action='store_true') - group.add_argument('--list-attrs', dest='list_attrs', metavar='OPERATION', type=str, - help='List attributes for an operation') - group.add_argument('--validate', action='store_true') + spec_group = parser.add_argument_group('Netlink family selection') + spec_sel = spec_group.add_mutually_exclusive_group(required=True) + spec_sel.add_argument('--list-families', action='store_true', + help=('list Netlink families supported by YNL ' + '(which have a spec available in the standard ' + 'system path)')) + spec_sel.add_argument('--family', dest='family', type=str, + help='name of the Netlink FAMILY to use') + spec_sel.add_argument('--spec', dest='spec', type=str, + help='full file path to the YAML spec file') + + ops_group = parser.add_argument_group('Operations') + ops = ops_group.add_mutually_exclusive_group() + ops.add_argument('--do', dest='do', metavar='DO-OPERATION', type=str) + ops.add_argument('--dump', dest='dump', metavar='DUMP-OPERATION', type=str) + ops.add_argument('--multi', dest='multi', nargs=2, action='append', + metavar=('DO-OPERATION', 'JSON_TEXT'), type=str, + help="Multi-message operation sequence (for nftables)") + ops.add_argument('--list-ops', action='store_true', + help="List available --do and --dump operations") + ops.add_argument('--list-msgs', action='store_true', + help="List all messages of the family (incl. notifications)") + ops.add_argument('--list-attrs', dest='list_attrs', metavar='MSG', + type=str, help='List attributes for a message / operation') + ops.add_argument('--validate', action='store_true', + help="Validate the spec against schema and exit") + + io_group = parser.add_argument_group('Input / Output') + io_group.add_argument('--json', dest='json_text', type=str, + help=('Specify attributes of the message to send ' + 'to the kernel in JSON format. Can be left out ' + 'if the message is expected to be empty.')) + io_group.add_argument('--output-json', action='store_true', + help='Format output as JSON') + + ntf_group = parser.add_argument_group('Notifications') + ntf_group.add_argument('--subscribe', dest='ntf', type=str) + ntf_group.add_argument('--duration', dest='duration', type=int, + help='when subscribed, watch for DURATION seconds') + ntf_group.add_argument('--sleep', dest='duration', type=int, + help='alias for duration') + + nlflags = parser.add_argument_group('Netlink message flags (NLM_F_*)', + ('Extra flags to set in nlmsg_flags of ' + 'the request, used mostly by older ' + 'Classic Netlink families.')) + nlflags.add_argument('--replace', dest='flags', action='append_const', + const=Netlink.NLM_F_REPLACE) + nlflags.add_argument('--excl', dest='flags', action='append_const', + const=Netlink.NLM_F_EXCL) + nlflags.add_argument('--create', dest='flags', action='append_const', + const=Netlink.NLM_F_CREATE) + nlflags.add_argument('--append', dest='flags', action='append_const', + const=Netlink.NLM_F_APPEND) + + schema_group = parser.add_argument_group('Development options') + schema_group.add_argument('--schema', dest='schema', type=str, + help="JSON schema to validate the spec") + schema_group.add_argument('--no-schema', action='store_true') + + dbg_group = parser.add_argument_group('Debug options') + dbg_group.add_argument('--dbg-small-recv', default=0, const=4000, + action='store', nargs='?', type=int, metavar='INT', + help="Length of buffers used for recv()") + dbg_group.add_argument('--process-unknown', action=argparse.BooleanOptionalAction) - parser.add_argument('--duration', dest='duration', type=int, - help='when subscribed, watch for DURATION seconds') - parser.add_argument('--sleep', dest='duration', type=int, - help='alias for duration') - parser.add_argument('--subscribe', dest='ntf', type=str) - parser.add_argument('--replace', dest='flags', action='append_const', - const=Netlink.NLM_F_REPLACE) - parser.add_argument('--excl', dest='flags', action='append_const', - const=Netlink.NLM_F_EXCL) - parser.add_argument('--create', dest='flags', action='append_const', - const=Netlink.NLM_F_CREATE) - parser.add_argument('--append', dest='flags', action='append_const', - const=Netlink.NLM_F_APPEND) - parser.add_argument('--process-unknown', action=argparse.BooleanOptionalAction) - parser.add_argument('--output-json', action='store_true') - parser.add_argument('--dbg-small-recv', default=0, const=4000, - action='store', nargs='?', type=int) args = parser.parse_args() def output(msg): From aca1fe235c10f7d06e9ebab4534852f109e6a8e9 Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Sat, 10 Jan 2026 15:31:39 -0800 Subject: [PATCH 4/7] tools: ynl: cli: add --doc as alias to --list-attrs --list-attrs also provides information about the operation itself. So --doc seems more appropriate. Add an alias. Tested-by: Gal Pressman Acked-by: Stanislav Fomichev Reviewed-by: Donald Hunter Link: https://patch.msgid.link/20260110233142.3921386-5-kuba@kernel.org Signed-off-by: Jakub Kicinski --- tools/net/ynl/pyynl/cli.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/net/ynl/pyynl/cli.py b/tools/net/ynl/pyynl/cli.py index 3aa1f1e816bf..4147c498b479 100755 --- a/tools/net/ynl/pyynl/cli.py +++ b/tools/net/ynl/pyynl/cli.py @@ -179,7 +179,7 @@ def main(): help="List available --do and --dump operations") ops.add_argument('--list-msgs', action='store_true', help="List all messages of the family (incl. notifications)") - ops.add_argument('--list-attrs', dest='list_attrs', metavar='MSG', + ops.add_argument('--list-attrs', '--doc', dest='list_attrs', metavar='MSG', type=str, help='List attributes for a message / operation') ops.add_argument('--validate', action='store_true', help="Validate the spec against schema and exit") From 45b99bb464eb62da555ecbef31583d9701881d43 Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Sat, 10 Jan 2026 15:31:40 -0800 Subject: [PATCH 5/7] tools: ynl: cli: factor out --list-attrs / --doc handling We'll soon add more code to the --doc handling. Factor it out to avoid making main() too long. Tested-by: Gal Pressman Acked-by: Stanislav Fomichev Reviewed-by: Donald Hunter Link: https://patch.msgid.link/20260110233142.3921386-6-kuba@kernel.org Signed-off-by: Jakub Kicinski --- tools/net/ynl/pyynl/cli.py | 35 ++++++++++++++++++++--------------- 1 file changed, 20 insertions(+), 15 deletions(-) diff --git a/tools/net/ynl/pyynl/cli.py b/tools/net/ynl/pyynl/cli.py index 4147c498b479..6975efa7874f 100755 --- a/tools/net/ynl/pyynl/cli.py +++ b/tools/net/ynl/pyynl/cli.py @@ -137,6 +137,25 @@ def print_mode_attrs(ynl, mode, mode_spec, attr_set, print_request=True): print_attr_list(ynl, mode_spec['attributes'], attr_set) +def do_doc(ynl, op): + """Handle --list-attrs $op, print the attr information to stdout""" + print(f'Operation: {color(op.name, Colors.BOLD)}') + print(op.yaml['doc']) + + for mode in ['do', 'dump', 'event']: + if mode in op.yaml: + print_mode_attrs(ynl, mode, op.yaml[mode], op.attr_set, True) + + if 'notify' in op.yaml: + mode_spec = op.yaml['notify'] + ref_spec = ynl.msgs.get(mode_spec).yaml.get('do') + if ref_spec: + print_mode_attrs(ynl, 'notify', ref_spec, op.attr_set, False) + + if 'mcgrp' in op.yaml: + print(f"\nMulticast group: {op.yaml['mcgrp']}") + + # pylint: disable=too-many-locals,too-many-branches,too-many-statements def main(): """YNL cli tool""" @@ -286,21 +305,7 @@ def main(): print(f'Operation {args.list_attrs} not found') sys.exit(1) - print(f'Operation: {color(op.name, Colors.BOLD)}') - print(op.yaml['doc']) - - for mode in ['do', 'dump', 'event']: - if mode in op.yaml: - print_mode_attrs(ynl, mode, op.yaml[mode], op.attr_set, True) - - if 'notify' in op.yaml: - mode_spec = op.yaml['notify'] - ref_spec = ynl.msgs.get(mode_spec).yaml.get('do') - if ref_spec: - print_mode_attrs(ynl, 'notify', ref_spec, op.attr_set, False) - - if 'mcgrp' in op.yaml: - print(f"\nMulticast group: {op.yaml['mcgrp']}") + do_doc(ynl, op) try: if args.do: From 6ccc421b14613aac32b2647462ae4d40f5dd43b8 Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Sat, 10 Jan 2026 15:31:41 -0800 Subject: [PATCH 6/7] tools: ynl: cli: extract the event/notify handling in --list-attrs Event and notify handling is quite different from do / dump handling. Forcing it into print_mode_attrs() doesn't really buy us anything as events and notifications do not have requests. Call print_attr_list() directly. Apart form subjective code clarity this also removes the word "reply" from the output: Before: Event reply attributes: Now: Event attributes: Tested-by: Gal Pressman Acked-by: Stanislav Fomichev Reviewed-by: Donald Hunter Link: https://patch.msgid.link/20260110233142.3921386-7-kuba@kernel.org Signed-off-by: Jakub Kicinski --- tools/net/ynl/pyynl/cli.py | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/tools/net/ynl/pyynl/cli.py b/tools/net/ynl/pyynl/cli.py index 6975efa7874f..6d2f0412dde0 100755 --- a/tools/net/ynl/pyynl/cli.py +++ b/tools/net/ynl/pyynl/cli.py @@ -120,11 +120,11 @@ def print_attr_list(ynl, attr_names, attr_set, indent=2): print_attr_list(ynl, nested_names, nested_set, indent + 4) -def print_mode_attrs(ynl, mode, mode_spec, attr_set, print_request=True): +def print_mode_attrs(ynl, mode, mode_spec, attr_set): """Print a given mode (do/dump/event/notify).""" mode_title = mode.capitalize() - if print_request and 'request' in mode_spec and 'attributes' in mode_spec['request']: + if 'request' in mode_spec and 'attributes' in mode_spec['request']: print(f'\n{mode_title} request attributes:') print_attr_list(ynl, mode_spec['request']['attributes'], attr_set) @@ -132,25 +132,28 @@ def print_mode_attrs(ynl, mode, mode_spec, attr_set, print_request=True): print(f'\n{mode_title} reply attributes:') print_attr_list(ynl, mode_spec['reply']['attributes'], attr_set) - if 'attributes' in mode_spec: - print(f'\n{mode_title} attributes:') - print_attr_list(ynl, mode_spec['attributes'], attr_set) - def do_doc(ynl, op): """Handle --list-attrs $op, print the attr information to stdout""" print(f'Operation: {color(op.name, Colors.BOLD)}') print(op.yaml['doc']) - for mode in ['do', 'dump', 'event']: + for mode in ['do', 'dump']: if mode in op.yaml: - print_mode_attrs(ynl, mode, op.yaml[mode], op.attr_set, True) + print_mode_attrs(ynl, mode, op.yaml[mode], op.attr_set) + + if 'attributes' in op.yaml.get('event', {}): + print('\nEvent attributes:') + print_attr_list(ynl, op.yaml['event']['attributes'], op.attr_set) if 'notify' in op.yaml: mode_spec = op.yaml['notify'] ref_spec = ynl.msgs.get(mode_spec).yaml.get('do') + if not ref_spec: + ref_spec = ynl.msgs.get(mode_spec).yaml.get('dump') if ref_spec: - print_mode_attrs(ynl, 'notify', ref_spec, op.attr_set, False) + print('\nNotification attributes:') + print_attr_list(ynl, ref_spec['reply']['attributes'], op.attr_set) if 'mcgrp' in op.yaml: print(f"\nMulticast group: {op.yaml['mcgrp']}") From 60411adedf70abec0ac221ec3d88f6453b031dd2 Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Sat, 10 Jan 2026 15:31:42 -0800 Subject: [PATCH 7/7] tools: ynl: cli: print reply in combined format if possible As pointed out during review of the --list-attrs support the GET ops very often return the same attrs from do and dump. Make the output more readable by combining the reply information, from: Do request attributes: - ifindex: u32 netdev ifindex Do reply attributes: - ifindex: u32 netdev ifindex [ .. other attrs .. ] Dump reply attributes: - ifindex: u32 netdev ifindex [ .. other attrs .. ] To, after: Do request attributes: - ifindex: u32 netdev ifindex Do and Dump reply attributes: - ifindex: u32 netdev ifindex [ .. other attrs .. ] Tested-by: Gal Pressman Acked-by: Stanislav Fomichev Reviewed-by: Donald Hunter Link: https://patch.msgid.link/20260110233142.3921386-8-kuba@kernel.org Signed-off-by: Jakub Kicinski --- tools/net/ynl/pyynl/cli.py | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/tools/net/ynl/pyynl/cli.py b/tools/net/ynl/pyynl/cli.py index 6d2f0412dde0..fdac1ab10a40 100755 --- a/tools/net/ynl/pyynl/cli.py +++ b/tools/net/ynl/pyynl/cli.py @@ -120,7 +120,7 @@ def print_attr_list(ynl, attr_names, attr_set, indent=2): print_attr_list(ynl, nested_names, nested_set, indent + 4) -def print_mode_attrs(ynl, mode, mode_spec, attr_set): +def print_mode_attrs(ynl, mode, mode_spec, attr_set, consistent_dd_reply=None): """Print a given mode (do/dump/event/notify).""" mode_title = mode.capitalize() @@ -129,8 +129,15 @@ def print_mode_attrs(ynl, mode, mode_spec, attr_set): print_attr_list(ynl, mode_spec['request']['attributes'], attr_set) if 'reply' in mode_spec and 'attributes' in mode_spec['reply']: - print(f'\n{mode_title} reply attributes:') - print_attr_list(ynl, mode_spec['reply']['attributes'], attr_set) + if consistent_dd_reply and mode == "do": + title = None # Dump handling will print in combined format + elif consistent_dd_reply and mode == "dump": + title = 'Do and Dump' + else: + title = f'{mode_title}' + if title: + print(f'\n{title} reply attributes:') + print_attr_list(ynl, mode_spec['reply']['attributes'], attr_set) def do_doc(ynl, op): @@ -138,9 +145,15 @@ def do_doc(ynl, op): print(f'Operation: {color(op.name, Colors.BOLD)}') print(op.yaml['doc']) + consistent_dd_reply = False + if 'do' in op.yaml and 'dump' in op.yaml and 'reply' in op.yaml['do'] and \ + op.yaml['do']['reply'] == op.yaml['dump'].get('reply'): + consistent_dd_reply = True + for mode in ['do', 'dump']: if mode in op.yaml: - print_mode_attrs(ynl, mode, op.yaml[mode], op.attr_set) + print_mode_attrs(ynl, mode, op.yaml[mode], op.attr_set, + consistent_dd_reply=consistent_dd_reply) if 'attributes' in op.yaml.get('event', {}): print('\nEvent attributes:')