On Thu, 15 Jan 2026, Ryota Sakamoto <[email protected]> wrote:
> Currently, kunit.py has many subcommands and options, making it difficult
> to remember them without checking the help message.
>
> Add --list-cmds and --list-opts to kunit.py to get available commands and
> options, use those outputs in kunit-completion.sh to show completion.
>
> This implementation is similar to perf and tools/perf/perf-completion.sh.
>
> Example output:
>   $ source tools/testing/kunit/kunit-completion.sh
>   $ ./tools/testing/kunit/kunit.py [TAB][TAB]
>   build   config  exec    parse   run
>   $ ./tools/testing/kunit/kunit.py run --k[TAB][TAB]
>   --kconfig_add  --kernel_args  --kunitconfig
>

The alternative would be to make the tool more friendly to existing
completion tools such as shtab [1]. Since the kernel as a project is
really averse to adding external dependencies, you could take shtab's
CLI approach, and commit the completion script in the repo. Only
whoever's updating the completions would have to install and run shtab.

And the whole thing could be taken a step further, adding, say,
tools/completions/{bash,zsh,tcsh,...} directories for all the kernel
tool completions instead of spreading them around.

Anyway, just a thought to consider before doing another kernel homebrew
NIH approach.


BR,
Jani.



[1] https://github.com/iterative/shtab


> Signed-off-by: Ryota Sakamoto <[email protected]>
> ---
>  Documentation/dev-tools/kunit/run_wrapper.rst |  9 ++++++++
>  tools/testing/kunit/kunit-completion.sh       | 33 
> +++++++++++++++++++++++++++
>  tools/testing/kunit/kunit.py                  | 30 ++++++++++++++++++++++++
>  tools/testing/kunit/kunit_tool_test.py        | 21 +++++++++++++++++
>  4 files changed, 93 insertions(+)
>
> diff --git a/Documentation/dev-tools/kunit/run_wrapper.rst 
> b/Documentation/dev-tools/kunit/run_wrapper.rst
> index 
> 6697c71ee8ca020b8ac7e91b46e29ab082d9dea0..3c0b585dcfffbd3929d0eef1ab9376fa4f380872
>  100644
> --- a/Documentation/dev-tools/kunit/run_wrapper.rst
> +++ b/Documentation/dev-tools/kunit/run_wrapper.rst
> @@ -335,3 +335,12 @@ command line arguments:
>  
>  - ``--list_tests_attr``: If set, lists all tests that will be run and all of 
> their
>    attributes.
> +
> +Command-line completion
> +==============================
> +
> +The kunit_tool comes with a bash completion script:
> +
> +.. code-block:: bash
> +
> +     source tools/testing/kunit/kunit-completion.sh
> diff --git a/tools/testing/kunit/kunit-completion.sh 
> b/tools/testing/kunit/kunit-completion.sh
> new file mode 100644
> index 
> 0000000000000000000000000000000000000000..3b9b68e3bc384c026f10f74b8a1df2129cb2cd50
> --- /dev/null
> +++ b/tools/testing/kunit/kunit-completion.sh
> @@ -0,0 +1,33 @@
> +# SPDX-License-Identifier: GPL-2.0
> +# bash completion support for KUnit
> +
> +_kunit_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
> +
> +_kunit()
> +{
> +     local cur prev words cword
> +     _init_completion || return
> +
> +     local script="${_kunit_dir}/kunit.py"
> +
> +     if [[ $cword -eq 1 && "$cur" != -* ]]; then
> +             local cmds=$(${script} --list-cmds 2>/dev/null)
> +             COMPREPLY=($(compgen -W "${cmds}" -- "$cur"))
> +             return 0
> +     fi
> +
> +     if [[ "$cur" == -* ]]; then
> +             if [[ -n "${words[1]}" && "${words[1]}" != -* ]]; then
> +                     local opts=$(${script} ${words[1]} --list-opts 
> 2>/dev/null)
> +                     COMPREPLY=($(compgen -W "${opts}" -- "$cur"))
> +                     return 0
> +             else
> +                     local opts=$(${script} --list-opts 2>/dev/null)
> +                     COMPREPLY=($(compgen -W "${opts}" -- "$cur"))
> +                     return 0
> +             fi
> +     fi
> +}
> +
> +complete -o default -F _kunit kunit.py
> +complete -o default -F _kunit kunit
> diff --git a/tools/testing/kunit/kunit.py b/tools/testing/kunit/kunit.py
> index 
> cd99c1956331dbbfb06cf4ddf130db3dcf2a7c31..a5aee1eb88e65fa2387b2623642d2ee9a66db600
>  100755
> --- a/tools/testing/kunit/kunit.py
> +++ b/tools/testing/kunit/kunit.py
> @@ -323,6 +323,17 @@ def get_default_jobs() -> int:
>               return ncpu
>       raise RuntimeError("os.cpu_count() returned None")
>  
> +def add_completion_opts(parser: argparse.ArgumentParser) -> None:
> +     parser.add_argument('--list-opts',
> +                         help=argparse.SUPPRESS,
> +                         action='store_true')
> +
> +def add_root_opts(parser: argparse.ArgumentParser) -> None:
> +     parser.add_argument('--list-cmds',
> +                         help=argparse.SUPPRESS,
> +                         action='store_true')
> +     add_completion_opts(parser)
> +
>  def add_common_opts(parser: argparse.ArgumentParser) -> None:
>       parser.add_argument('--build_dir',
>                           help='As in the make command, it specifies the 
> build '
> @@ -374,6 +385,8 @@ def add_common_opts(parser: argparse.ArgumentParser) -> 
> None:
>                           help='Additional QEMU arguments, e.g. "-smp 8"',
>                           action='append', metavar='')
>  
> +     add_completion_opts(parser)
> +
>  def add_build_opts(parser: argparse.ArgumentParser) -> None:
>       parser.add_argument('--jobs',
>                           help='As in the make command, "Specifies  the 
> number of '
> @@ -569,6 +582,7 @@ subcommand_handlers_map = {
>  def main(argv: Sequence[str]) -> None:
>       parser = argparse.ArgumentParser(
>                       description='Helps writing and running KUnit tests.')
> +     add_root_opts(parser)
>       subparser = parser.add_subparsers(dest='subcommand')
>  
>       # The 'run' command will config, build, exec, and parse in one go.
> @@ -603,12 +617,28 @@ def main(argv: Sequence[str]) -> None:
>       parse_parser.add_argument('file',
>                                 help='Specifies the file to read results 
> from.',
>                                 type=str, nargs='?', metavar='input_file')
> +     add_completion_opts(parse_parser)
>  
>       cli_args = parser.parse_args(massage_argv(argv))
>  
>       if get_kernel_root_path():
>               os.chdir(get_kernel_root_path())
>  
> +     if cli_args.list_cmds:
> +             print(" ".join(subparser.choices.keys()))
> +             return
> +
> +     if cli_args.list_opts:
> +             target_parser = subparser.choices.get(cli_args.subcommand)
> +             if not target_parser:
> +                     target_parser = parser
> +
> +             # Accessing private attribute _option_string_actions to get
> +             # the list of options. This is not a public API, but argparse
> +             # does not provide a way to inspect options programmatically.
> +             print(' '.join(target_parser._option_string_actions.keys()))
> +             return
> +
>       subcomand_handler = subcommand_handlers_map.get(cli_args.subcommand, 
> None)
>  
>       if subcomand_handler is None:
> diff --git a/tools/testing/kunit/kunit_tool_test.py 
> b/tools/testing/kunit/kunit_tool_test.py
> index 
> bbba921e0eacb18663abfcabb2bccf330d8666f5..a7f09a6c97a473ff85e087d17c2f5faf7755b994
>  100755
> --- a/tools/testing/kunit/kunit_tool_test.py
> +++ b/tools/testing/kunit/kunit_tool_test.py
> @@ -11,11 +11,13 @@ from unittest import mock
>  
>  import tempfile, shutil # Handling test_tmpdir
>  
> +import io
>  import itertools
>  import json
>  import os
>  import signal
>  import subprocess
> +import sys
>  from typing import Iterable
>  
>  import kunit_config
> @@ -855,5 +857,24 @@ class KUnitMainTest(unittest.TestCase):
>                       mock.call(args=None, build_dir='.kunit', 
> filter_glob='suite2.test1', filter='', filter_action=None, timeout=300),
>               ])
>  
> +     @mock.patch.object(sys, 'stdout', new_callable=io.StringIO)
> +     def test_list_cmds(self, mock_stdout):
> +             kunit.main(['--list-cmds'])
> +             output = mock_stdout.getvalue()
> +             output_cmds = sorted(output.split())
> +             expected_cmds = sorted(['build', 'config', 'exec', 'parse', 
> 'run'])
> +             self.assertEqual(output_cmds, expected_cmds)
> +
> +     @mock.patch.object(sys, 'stdout', new_callable=io.StringIO)
> +     def test_run_list_opts(self, mock_stdout):
> +             kunit.main(['run', '--list-opts'])
> +             output = mock_stdout.getvalue()
> +             output_cmds = set(output.split())
> +             self.assertIn('--help', output_cmds)
> +             self.assertIn('--kunitconfig', output_cmds)
> +             self.assertIn('--jobs', output_cmds)
> +             self.assertIn('--kernel_args', output_cmds)
> +             self.assertIn('--raw_output', output_cmds)
> +
>  if __name__ == '__main__':
>       unittest.main()
>
> ---
> base-commit: b71e635feefc852405b14620a7fc58c4c80c0f73
> change-id: 20260114-kunit-completion-265889f59c52
>
> Best regards,

-- 
Jani Nikula, Intel

Reply via email to