Repository: miurahr/aqtinstall
Pull request #331 (2021-07-22 17:57:39)

List syntax update
This implements the splitting of the `aqt list` command into `aqt list-qt` and `aqt list-tool`, as discussed previously in #306. It does not address the other changes mentioned in #306; I think that's best left to another PR.

#306 mentions retaining the old syntax for the "install", "tool", "doc", and "src" commands to avoid breaking backwards compatibility. I have made no attempt to retain backwards compatibility for `aqt list`, because the feature is so new that I don't think anyone really depends on it yet, and I think it would make the code really messy.


Patch:
diff --git a/aqt/installer.py b/aqt/installer.py
index 81778e9..a2c7390 100644
--- a/aqt/installer.py
+++ b/aqt/installer.py
@@ -48,7 +48,7 @@ from aqt.helper import (
     getUrl,
     setup_logging,
 )
-from aqt.metadata import ArchiveId, MetadataFactory, Version, show_list
+from aqt.metadata import ArchiveId, MetadataFactory, SimpleSpec, Version, show_list
 from aqt.updater import Updater
 
 try:
@@ -487,8 +487,8 @@ class Cli:
             )
         )
 
-    def run_list(self, args: argparse.ArgumentParser) -> int:
-        """Print tools, versions of Qt, extensions, modules, architectures"""
+    def run_list_qt(self, args: argparse.ArgumentParser) -> int:
+        """Print versions of Qt, extensions, modules, architectures"""
 
         if not args.target:
             print(" ".join(ArchiveId.TARGETS_FOR_HOST[args.host]))
@@ -508,51 +508,52 @@ class Cli:
                         version_str
                     )
                 )
-                exit(1)
+                exit(1)  # TODO: maybe return 1 instead?
+
+        spec = None
+        try:
+            if args.spec is not None:
+                spec = SimpleSpec(args.spec)
+        except ValueError:
+            self.logger.error(
+                f"Invalid version specification: '{args.spec}'.\n" + SimpleSpec.usage()
+            )
+            return 1
 
         meta = MetadataFactory(
             archive_id=ArchiveId(
-                args.category,
+                "qt",
                 args.host,
                 args.target,
                 args.extension if args.extension else "",
             ),
-            filter_minor=args.filter_minor,
+            spec=spec,
             is_latest_version=args.latest_version,
             modules_ver=args.modules,
             extensions_ver=args.extensions,
             architectures_ver=args.arch,
-            tool_name=args.tool,
-            tool_long_listing=args.tool_long,
         )
         return show_list(meta)
 
-    def _make_list_parser(self, subparsers: argparse._SubParsersAction):
+    def _make_list_qt_parser(self, subparsers: argparse._SubParsersAction):
         """Creates a subparser that works with the MetadataFactory, and adds it to the `subparsers` parameter"""
         list_parser: argparse.ArgumentParser = subparsers.add_parser(
-            "list",
+            "list-qt",
             formatter_class=argparse.RawDescriptionHelpFormatter,
             epilog="Examples:\n"
-            "$ aqt list qt5 mac                                            # print all targets for Mac OS\n"
-            "$ aqt list tools mac desktop                                  # print all tools for mac desktop\n"
-            "$ aqt list tools mac desktop --tool tools_ifw                 # print all tool variant names for QtIFW\n"
-            "$ aqt list qt5 mac desktop                                    # print all versions of Qt 5\n"
-            "$ aqt list qt5 mac desktop --extension wasm                   # print all wasm versions of Qt 5\n"
-            "$ aqt list qt5 mac desktop --filter-minor 9                   # print all versions of Qt 5.9\n"
-            "$ aqt list qt5 mac desktop --filter-minor 9 --latest-version  # print latest Qt 5.9\n"
-            "$ aqt list qt5 mac desktop --modules 5.12.0                   # print modules for 5.12.0\n"
-            "$ aqt list qt5 mac desktop --filter-minor 9 --modules latest  # print modules for latest 5.9\n"
-            "$ aqt list qt5 mac desktop --extensions 5.9.0                 # print choices for --extension flag\n"
-            "$ aqt list qt5 mac desktop --arch 5.9.9                       "
+            "$ aqt list-qt mac                                            # print all targets for Mac OS\n"
+            "$ aqt list-qt mac desktop                                    # print all versions of Qt 5\n"
+            "$ aqt list-qt mac desktop --extension wasm                   # print all wasm versions of Qt 5\n"
+            '$ aqt list-qt mac desktop --spec "5.9"                       # print all versions of Qt 5.9\n'
+            '$ aqt list-qt mac desktop --spec "5.9" --latest-version      # print latest Qt 5.9\n'
+            "$ aqt list-qt mac desktop --modules 5.12.0                   # print modules for 5.12.0\n"
+            "$ aqt list-qt mac desktop --spec 5.9 --modules latest  # print modules for latest 5.9\n"
+            "$ aqt list-qt mac desktop --extensions 5.9.0                 # print choices for --extension flag\n"
+            "$ aqt list-qt mac desktop --arch 5.9.9                       "
             "# print architectures for 5.9.9/mac/desktop\n"
-            "$ aqt list qt5 mac desktop --arch latest                      "
+            "$ aqt list-qt mac desktop --arch latest                      "
             "# print architectures for the latest Qt 5\n",
         )
-        list_parser.add_argument(
-            "category",
-            choices=["tools", "qt5", "qt6"],
-            help="category of packages to list",
-        )
         list_parser.add_argument(
             "host", choices=["linux", "mac", "windows"], help="host os name"
         )
@@ -570,11 +571,11 @@ class Cli:
             "Use the `--extensions` flag to list all relevant options for a host/target.",
         )
         list_parser.add_argument(
-            "--filter-minor",
-            type=int,
-            metavar="MINOR_VERSION",
-            help="print versions for a particular minor version. "
-            "IE: `aqt list qt5 windows desktop --filter-minor 12` prints all versions beginning with 5.12",
+            "--spec",
+            type=str,
+            metavar="SPECIFICATION",
+            help="Filter output so that only versions that match the specification are printed. "
+            'IE: `aqt list-qt windows desktop --spec "5.12"` prints all versions beginning with 5.12',
         )
         output_modifier_exclusive_group = list_parser.add_mutually_exclusive_group()
         output_modifier_exclusive_group.add_argument(
@@ -604,27 +605,66 @@ class Cli:
             action="store_true",
             help="print only the newest version available",
         )
-        output_modifier_exclusive_group.add_argument(
-            "--tool",
-            type=str,
-            metavar="TOOL_NAME",
-            help="The name of a tool. Use 'aqt list tools <host> <target>' to see accepted values. "
-            "This flag only works with the 'tools' category, and cannot be combined with any other flags. "
-            "When set, this prints all 'tool variant names' available. "
-            # TODO: find a better word ^^^^^^^^^^^^^^^^^^^^; this is a mysterious help message
-            "The output of this command is intended to be used with `aqt tool`.",
+        list_parser.set_defaults(func=self.run_list_qt)
+
+    def run_list_tool(self, args: argparse.ArgumentParser) -> int:
+        """Print tools"""
+
+        if not args.target:
+            print(" ".join(ArchiveId.TARGETS_FOR_HOST[args.host]))
+            return 0
+        if args.target not in ArchiveId.TARGETS_FOR_HOST[args.host]:
+            self.logger.error(
+                "'{0.target}' is not a valid target for host '{0.host}'".format(args)
+            )
+            return 1
+
+        meta = MetadataFactory(
+            archive_id=ArchiveId("tools", args.host, args.target),
+            tool_name=args.tool_name,
+            is_long_listing=args.long,
         )
-        output_modifier_exclusive_group.add_argument(
-            "--tool-long",
-            type=str,
-            metavar="TOOL_NAME",
-            help="The name of a tool. Use 'aqt list tools <host> <target>' to see accepted values. "
-            "This flag only works with the 'tools' category, and cannot be combined with any other flags. "
-            "When set, this prints all 'tool variant names' available, along with versions and release dates. "
-            # TODO: find a better word ^^^^^^^^^^^^^^^^^^^^; this is a mysterious help message
-            "The output of this command is formatted as a table.",
+        return show_list(meta)
+
+    def _make_list_tool_parser(self, subparsers: argparse._SubParsersAction):
+        """Creates a subparser that works with the MetadataFactory, and adds it to the `subparsers` parameter"""
+        list_parser: argparse.ArgumentParser = subparsers.add_parser(
+            "list-tool",
+            formatter_class=argparse.RawDescriptionHelpFormatter,
+            epilog="Examples:\n"
+            "$ aqt list-tool mac desktop                 # print all tools for mac desktop\n"
+            "$ aqt list-tool mac desktop tools_ifw       # print all tool variant names for QtIFW\n"
+            "$ aqt list-tool mac desktop ifw             # print all tool variant names for QtIFW\n"
+            "$ aqt list-tool mac desktop -l tools_ifw    # print tool variant names with metadata for QtIFW\n"
+            "$ aqt list-tool mac desktop -l ifw          # print tool variant names with metadata for QtIFW\n",
+        )
+        list_parser.add_argument(
+            "host", choices=["linux", "mac", "windows"], help="host os name"
+        )
+        list_parser.add_argument(
+            "target",
+            nargs="?",
+            default=None,
+            choices=["desktop", "winrt", "android", "ios"],
+            help="Target SDK. When omitted, this prints all the targets available for a host OS.",
+        )
+        list_parser.add_argument(
+            "tool_name",
+            nargs="?",
+            default=None,
+            help='Name of a tool, ie "tools_mingw" or "tools_ifw". '
+            "When omitted, this prints all the tool names available for a host OS/target SDK combination. "
+            "When present, this prints all the tool variant names available for this tool. ",
+        )
+        list_parser.add_argument(
+            "-l",
+            "--long",
+            action="store_true",
+            help="Long display: shows a table of metadata associated with each tool variant. "
+            "On narrow terminals, it displays tool variant names, versions, and release dates. "
+            "On terminals wider than 95 characters, it also displays descriptions of each tool.",
         )
-        list_parser.set_defaults(func=self.run_list)
+        list_parser.set_defaults(func=self.run_list_tool)
 
     def show_help(self, args=None):
         """Display help message"""
@@ -786,11 +826,12 @@ class Cli:
             nargs="?",
             default=None,
             help="Name of full tool name such as qt.tools.ifw.31. "
-            "Please use 'aqt list --tool' to list acceptable values for this parameter.",
+            "Please use 'aqt list-tool' to list acceptable values for this parameter.",
         )
         self._set_common_options(tools_parser)
 
-        self._make_list_parser(subparsers)
+        self._make_list_qt_parser(subparsers)
+        self._make_list_tool_parser(subparsers)
         #
         help_parser = subparsers.add_parser("help")
         help_parser.set_defaults(func=self.show_help)
diff --git a/aqt/metadata.py b/aqt/metadata.py
index 22ad916..8e9c36c 100644
--- a/aqt/metadata.py
+++ b/aqt/metadata.py
@@ -41,6 +41,18 @@ from aqt.helper import Settings, getUrl, xml_to_modules
 class SimpleSpec(SemanticSimpleSpec):
     pass
 
+    @staticmethod
+    def usage() -> str:
+        return (
+            "See documentation at: "
+            "https://python-semanticversion.readthedocs.io/en/latest/reference.html#semantic_version.SimpleSpec\n"
+            "Examples:\n"
+            '* "*": matches everything\n'
+            '* "5": matches every version with major=5\n'
+            '* "5.6": matches every version beginning with 5.6\n'
+            '* "5.*.3": matches versions with major=5 and patch=3'
+        )
+
 
 class Version(SemanticVersion):
     """Override semantic_version.Version class
@@ -192,7 +204,7 @@ def get_semantic_version(qt_ver: str, is_preview: bool) -> Optional[Version]:
 
 
 class ArchiveId:
-    CATEGORIES = ("tools", "qt5", "qt6")
+    CATEGORIES = ("tools", "qt")
     HOSTS = ("windows", "mac", "linux")
     TARGETS_FOR_HOST = {
         "windows": ["android", "desktop", "winrt"],
@@ -229,7 +241,7 @@ class ArchiveId:
         return "preview" in self.extension if self.extension else False
 
     def is_qt(self) -> bool:
-        return self.category.startswith("qt")
+        return self.category == "qt"
 
     def is_tools(self) -> bool:
         return self.category == "tools"
@@ -238,10 +250,6 @@ class ArchiveId:
         """Returns True if there should be no arch attached to the module names"""
         return self.extension in ("src_doc_examples",)
 
-    def is_major_ver_mismatch(self, qt_version: Version) -> bool:
-        """Returns True if the version specifies a version different from the specified category"""
-        return self.is_qt() and int(self.category[-1]) != qt_version.major
-
     def to_url(self, qt_version_no_dots: Optional[str] = None, file: str = "") -> str:
         base = "online/qtsdkrepository/{os}{arch}/{target}/".format(
             os=self.host,
@@ -250,8 +258,9 @@ class ArchiveId:
         )
         if not qt_version_no_dots:
             return base
-        folder = "{category}_{ver}{ext}/".format(
+        folder = "{category}{major}_{ver}{ext}/".format(
             category=self.category,
+            major=qt_version_no_dots[0],
             ver=qt_version_no_dots,
             ext="_" + self.extension if self.extension else "",
         )
@@ -283,14 +292,14 @@ class ToolData:
         "Release Date",
     ]
 
-    def __init__(self, tool_data):
-        self.tool_data = tool_data
+    def __init__(self, tool_data: Dict[str, Dict[str, str]]):
+        self.tool_data: Dict[str, Dict[str, str]] = tool_data
         for key in tool_data.keys():
             self.tool_data[key]["Description"] = tool_data[key]["Description"].replace(
                 "<br>", "\n"
             )
 
-    def __format__(self, format_spec) -> str:
+    def __format__(self, format_spec: str) -> str:
         short = False
         if format_spec == "{:s}":
             return str(self)
@@ -338,36 +347,41 @@ class MetadataFactory:
         self,
         archive_id: ArchiveId,
         *,
-        filter_minor: Optional[int] = None,
+        spec: Optional[SimpleSpec] = None,
         is_latest_version: bool = False,
         modules_ver: Optional[str] = None,
         extensions_ver: Optional[str] = None,
         architectures_ver: Optional[str] = None,
         tool_name: Optional[str] = None,
-        tool_long_listing: Optional[str] = None,
+        is_long_listing: bool = False,
     ):
         """
         Construct MetadataFactory.
 
-        :param filter_minor:        When set, the MetadataFactory will filter out all versions of
-                                    Qt that don't match this minor version.
+        :param spec:                When set, the MetadataFactory will filter out all versions of
+                                    Qt that don't fit this SimpleSpec.
         :param is_latest_version:   When True, the MetadataFactory will find all versions of Qt
                                     matching filters, and only print the most recent version
         :param modules_ver:         Version of Qt for which to list modules
         :param extensions_ver:      Version of Qt for which to list extensions
         :param architectures_ver:   Version of Qt for which to list architectures
+        :param tool_name:           Name of a tool, without architecture, ie "tools_qtcreator" or "tools_ifw"
+        :param is_long_listing:     If true, long listing is used for tools output
         """
         self.logger = getLogger("aqt.metadata")
         self.archive_id = archive_id
-        self.filter_minor = filter_minor
+        self.spec = spec
 
         if archive_id.is_tools():
             if tool_name:
-                self.request_type = "tool variant names"
-                self._action = lambda: self.fetch_tool_modules(tool_name)
-            elif tool_long_listing:
-                self.request_type = "tool long listing"
-                self._action = lambda: self.fetch_tool_long_listing(tool_long_listing)
+                if not tool_name.startswith("tools_"):
+                    tool_name = "tools_" + tool_name
+                if is_long_listing:
+                    self.request_type = "tool long listing"
+                    self._action = lambda: self.fetch_tool_long_listing(tool_name)
+                else:
+                    self.request_type = "tool variant names"
+                    self._action = lambda: self.fetch_tool_modules(tool_name)
             else:
                 self.request_type = "tools"
                 self._action = self.fetch_tools
@@ -415,7 +429,7 @@ class MetadataFactory:
             version, extension = ver_ext
             return (
                 version
-                and (self.filter_minor is None or self.filter_minor == version.minor)
+                and (self.spec is None or version in self.spec)
                 and (self.archive_id.extension == extension)
             )
 
@@ -549,11 +563,6 @@ class MetadataFactory:
             version = Version(qt_ver)
         except ValueError as e:
             raise CliInputError(e)
-        if self.archive_id.is_major_ver_mismatch(version):
-            msg = "Major version mismatch between {} and {}".format(
-                self.archive_id.category, version
-            )
-            raise CliInputError(msg)
         return version
 
     @staticmethod
@@ -680,15 +689,16 @@ class MetadataFactory:
         return naive_modules_arches(modules.keys())
 
     def describe_filters(self) -> str:
-        if self.filter_minor is None:
+        if self.spec is None:
             return str(self.archive_id)
-        return "{} with minor version {}".format(self.archive_id, self.filter_minor)
+        return "{} with spec {}".format(self.archive_id, self.spec)
 
 
 def suggested_follow_up(meta: MetadataFactory) -> List[str]:
     """Makes an informed guess at what the user got wrong, in the event of an error."""
     msg = []
-    base_cmd = "aqt list {0.category} {0.host} {0.target}".format(meta.archive_id)
+    list_cmd = "list-tool" if meta.archive_id.is_tools() else "list-qt"
+    base_cmd = "aqt {0} {1.host} {1.target}".format(list_cmd, meta.archive_id)
     if meta.archive_id.extension:
         msg.append(
             f"Please use '{base_cmd} --extensions <QT_VERSION>' to list valid extensions."
@@ -696,10 +706,10 @@ def suggested_follow_up(meta: MetadataFactory) -> List[str]:
 
     if meta.archive_id.is_tools() and meta.request_type == "tool variant names":
         msg.append(f"Please use '{base_cmd}' to check what tools are available.")
-    elif meta.filter_minor is not None:
+    elif meta.spec is not None:
         msg.append(
             f"Please use '{base_cmd}' to check that versions of {meta.archive_id.category} "
-            f"exist with the minor version '{meta.filter_minor}'."
+            f"exist within the spec '{meta.spec}'."
         )
     elif meta.request_type in ("architectures", "modules", "extensions"):
         msg.append(f"Please use '{base_cmd}' to show versions of Qt available.")
diff --git a/ci/generate_azure_pipelines_matrices.py b/ci/generate_azure_pipelines_matrices.py
index c2e5d82..3271a35 100644
--- a/ci/generate_azure_pipelines_matrices.py
+++ b/ci/generate_azure_pipelines_matrices.py
@@ -28,6 +28,7 @@ class BuildJob:
         subarchives=None,
         output_dir=None,
         list_options=None,
+        spec=None,
     ):
         self.command = command
         self.qt_version = qt_version
@@ -39,6 +40,8 @@ class BuildJob:
         self.mirror = mirror
         self.subarchives = subarchives
         self.list_options = list_options if list_options else {}
+        # `steps.yml` assumes that qt_version is the highest version that satisfies spec
+        self.spec = spec
         self.output_dir = output_dir
 
 
@@ -161,17 +164,16 @@ linux_build_jobs.extend(
             "doc", "6.1.0", "linux", "desktop", "gcc_64", "gcc_64", subarchives="qtdoc"
         ),
         # test for list commands
-        BuildJob('list', '5.15.2', 'linux', 'desktop', '', '', list_options={
-            'HAS_WASM_EXTENSION': "True",
+        BuildJob('list', '5.15.2', 'linux', 'desktop', '', '', spec="<6", list_options={
             'HAS_EXTENSIONS': "True",
         }),
-        BuildJob('list', '6.1.0', 'linux', 'android', '', '', list_options={
+        BuildJob('list', '6.1.0', 'linux', 'android', '', '', spec=">6.0,<6.1.1", list_options={
             'HAS_EXTENSIONS': "True",
             'USE_EXTENSION': "armv7",
         }),
         # tests run on linux but query data about other platforms
-        BuildJob('list', '5.14.1', 'mac', 'ios', '', '', list_options={}),
-        BuildJob('list', '5.13.1', 'windows', 'winrt', '', '', list_options={}),
+        BuildJob('list', '5.14.1', 'mac', 'ios', '', '', spec="<=5.14.1", list_options={}),
+        BuildJob('list', '5.13.1', 'windows', 'winrt', '', '', spec=">5.13.0,<5.13.2", list_options={}),
     ]
 )
 mac_build_jobs.extend(
@@ -274,7 +276,7 @@ for platform_build_job in all_platform_build_jobs:
                 ("MODULE", build_job.module if build_job.module else ""),
                 ("QT_BASE_MIRROR", build_job.mirror if build_job.mirror else ""),
                 ("SUBARCHIVES", build_job.subarchives if build_job.subarchives else ""),
-                ("HAS_WASM_EXTENSION", build_job.list_options.get("HAS_WASM_EXTENSION", "False")),
+                ("SPEC", build_job.spec if build_job.spec else ""),
                 ("HAS_EXTENSIONS", build_job.list_options.get("HAS_EXTENSIONS", "False")),
                 ("USE_EXTENSION", build_job.list_options.get("USE_EXTENSION", "None")),
                 ("OUTPUT_DIR", build_job.output_dir if build_job.output_dir else ""),
diff --git a/ci/steps.yml b/ci/steps.yml
index 35f46e2..0e6d6c6 100644
--- a/ci/steps.yml
+++ b/ci/steps.yml
@@ -65,41 +65,38 @@ steps:
         fi
       fi
       if [[ "$(SUBCOMMAND)" == "list" ]]; then
-        qtversion=$(QT_VERSION)
-        MAJORMINOR=${qtversion%.*}
-        qtmajor="qt${MAJORMINOR%.*}"
-        qtminor=${MAJORMINOR##*.}
         if [[ $(USE_EXTENSION) != "None" ]]; then
           # In this case, if you don't specify an extension, Updates.xml just shows dependencies
           ext="--extension $(USE_EXTENSION)"
         fi
-        aqt list $qtmajor $(HOST)                                               # print all targets for host
-        aqt list tools $(HOST) $(TARGET)                                        # print all tools for host/target
-        aqt list tools $(HOST) $(TARGET) --tool tools_qt3dstudio_runtime_240    # print all tool variant names for qt3dstudio
-        aqt list tools $(HOST) $(TARGET) --tool-long tools_qt3dstudio_runtime_240    # print tool variant names, versions, release dates
+        aqt list-qt $(HOST)                                                     # print all targets for host
+        aqt list-tool $(HOST) $(TARGET)                                         # print all tools for host/target
+        aqt list-tool $(HOST) $(TARGET) qt3dstudio_runtime_240                  # print all tool variant names for qt3dstudio
+        aqt list-tool $(HOST) $(TARGET) qt3dstudio_runtime_240 --long           # print tool variant names, versions, release dates
         if [[ "$(TARGET)" == "desktop" ]]; then
-          aqt list tools $(HOST) $(TARGET) --tool tools_qtcreator               # print all tool variant names for qtcreator
-          aqt list tools $(HOST) $(TARGET) --tool-long tools_qtcreator          # print tool variant names, versions, release dates
+          aqt list-tool $(HOST) $(TARGET) tools_qtcreator                       # print all tool variant names for qtcreator
+          aqt list-tool $(HOST) $(TARGET) tools_qtcreator -l                    # print tool variant names, versions, release dates
         fi
-        aqt list $qtmajor $(HOST) $(TARGET)                                     # print all versions of Qt
-        if [[ $(HAS_WASM_EXTENSION) == "True" ]]; then
-          aqt list $qtmajor $(HOST) $(TARGET) --extension wasm                  # print all wasm versions of Qt5
+        aqt list-qt $(HOST) $(TARGET)                                           # print all versions of Qt
+        if [[ $(HOST) == "desktop" ]]; then
+          aqt list-qt $(HOST) $(TARGET) --extension wasm                        # print all wasm versions of Qt
         else
-          ! aqt list $qtmajor $(HOST) $(TARGET) --extension wasm                # Only Qt5 desktop provides wasm extension
+          ! aqt list-qt $(HOST) $(TARGET) --extension wasm                      # there are no wasm versions of Qt
         fi
-        aqt list $qtmajor $(HOST) $(TARGET) $ext --filter-minor $qtminor        # print all versions of Qt major/minor
-        aqt list $qtmajor $(HOST) $(TARGET) $ext --filter-minor $qtminor --latest-version  # print latest Qt major/minor
-        aqt list $qtmajor $(HOST) $(TARGET) $ext --filter-minor $qtminor --modules latest  # print modules for latest major/minor
-        aqt list $qtmajor $(HOST) $(TARGET) $ext --modules $(QT_VERSION)        # print modules for version/host/target
+        aqt list-qt $(HOST) $(TARGET) $ext --spec "$(SPEC)"                     # print all versions of Qt in SimpleSpec
+        ver=$(aqt list-qt $(HOST) $(TARGET) $ext --spec "$(SPEC)" --latest-version)  # latest Qt in SimpleSpec
+        [ $ver == $(QT_VERSION) ]                                               # latest version in SPEC must be QT_VERSION
+        aqt list-qt $(HOST) $(TARGET) $ext --spec "$(SPEC)" --modules latest    # print modules for latest in SimpleSpec
+        aqt list-qt $(HOST) $(TARGET) $ext --modules $(QT_VERSION)              # print modules for version/host/target
         if [[ $(HAS_EXTENSIONS) == "True" ]]; then
-          aqt list $qtmajor $(HOST) $(TARGET) --extensions $(QT_VERSION)        # print choices for --extension flag
-          aqt list $qtmajor $(HOST) $(TARGET) --filter-minor $qtminor --extensions latest
+          aqt list-qt $(HOST) $(TARGET) --extensions $(QT_VERSION)              # print choices for --extension flag
+          aqt list-qt $(HOST) $(TARGET) --spec "$(SPEC)" --extensions latest
         else
-          ! aqt list $qtmajor $(HOST) $(TARGET) --extensions $(QT_VERSION)
-          ! aqt list $qtmajor $(HOST) $(TARGET) --filter-minor $qtminor --extensions latest
+          ! aqt list-qt $(HOST) $(TARGET) --extensions $(QT_VERSION)
+          ! aqt list-qt $(HOST) $(TARGET) --spec "$(SPEC)" --extensions latest
         fi
-        aqt list $qtmajor $(HOST) $(TARGET) $ext --arch $(QT_VERSION)           # print architectures for version/host/target
-        aqt list $qtmajor $(HOST) $(TARGET) $ext --filter-minor $qtminor --arch latest
+        aqt list-qt $(HOST) $(TARGET) $ext --arch $(QT_VERSION)                 # print architectures for version/host/target
+        aqt list-qt $(HOST) $(TARGET) $ext --spec "$(SPEC)" --arch latest
       fi
       if [[ "$(SUBCOMMAND)" == "src" ]]; then
         python -m aqt $(SUBCOMMAND) $(QT_VERSION) $(HOST) $(TARGET) --archives $(SUBARCHIVES)
diff --git a/docs/cli.rst b/docs/cli.rst
index f19d6b9..9fb5943 100644
--- a/docs/cli.rst
+++ b/docs/cli.rst
@@ -16,21 +16,21 @@ Generic commands
     show generic help
 
 
-List command
+List Qt command
 ------------------
 
 .. program::  aqt
 
-.. option:: list [-h | --help] [--extension <extension>] [--filter-minor <Qt minor version>]
-                [--modules (<Qt version> | latest) | --extensions (<Qt version> | latest) |
-                 --arch (<Qt version> | latest) | --latest-version | --tool <tool name>]
-                <category> <target OS> [<target variant>]
+.. option:: list-qt [-h | --help]
+                    [--extension <extension>]
+                    [--spec <specification>]
+                    [--modules    (<Qt version> | latest) |
+                     --extensions (<Qt version> | latest) |
+                     --arch       (<Qt version> | latest) |
+                     --latest-version]
+                    <target OS> [<target variant>]
 
-    list available tools, versions of Qt, targets, extensions, modules, and architectures.
-
-.. describe:: category
-
-    tools, qt5 or qt6
+    List available versions of Qt, targets, extensions, modules, and architectures.
 
 .. describe:: target OS (aka host in code/help text)
 
@@ -38,7 +38,9 @@ List command
 
 .. describe:: target variant (aka target in code/help text)
 
-    desktop, winrt, ios or android. When omitted, the command prints all the targets available for a host OS.
+    desktop, winrt, ios or android.
+    When omitted, the command prints all the targets available for a host OS.
+    Note that winrt is only available on Windows, and ios is only available on Mac OS.
 
 .. option:: --help, -h
 
@@ -50,6 +52,17 @@ List command
     Use the `--extensions` flag to list all relevant options for a host/target.
     Incompatible with the `--extensions` flag, but may be combined with any other flag.
 
+.. option:: --spec <Specification>
+
+    Print versions of Qt within a version specification, as explained here:
+    https://python-semanticversion.readthedocs.io/en/latest/reference.html#semantic_version.SimpleSpec
+    You can specify partial versions, inequalities, etc.
+    `"*"` would match all versions of Qt; `">6.0.2,<6.2.0"` would match all
+    versions of Qt between 6.0.2 and 6.2.0, etc.
+    For example, `aqt list-qt windows desktop --spec "5.12"` would print
+    all versions of Qt for Windows Desktop beginning with 5.12.
+    May be combined with any other flag to filter the output of that flag.
+
 .. option:: --extensions (<Qt version> | latest)
 
     Qt version in the format of "5.X.Y", or the keyword `latest`.
@@ -57,13 +70,6 @@ List command
     Qt 5.X.Y, or the latest version of Qt if `latest` is specified.
     Incompatible with the `--extension` flag.
 
-.. option:: --filter-minor <Qt minor version>
-
-    Print versions of Qt that have a particular minor version.
-    For example, `aqt list qt5 windows desktop --filter-minor 12` would print
-    all versions of Qt for Windows Desktop beginning with 5.12.
-    May be combined with any other flag to filter the output of that flag.
-
 .. option:: --modules (<Qt version> | latest)
 
     Qt version in the format of "5.X.Y". When set, this lists all the modules
@@ -79,17 +85,57 @@ List command
 .. option:: --latest-version
 
     Print only the newest version available
-    May be combined with the `--extension` and/or `--filter-minor` flags.
+    May be combined with the `--extension` and/or `--spec` flags.
+
+
+List Tool command
+-----------------
+.. program::  aqt
+
+.. option:: list-tool [-h | --help] [-l | --long] <target OS> [<target variant>] [<tool name>]
+
+    List available tools
+
+.. describe:: target OS (aka host in code/help text)
+
+    linux, windows or mac
+
+.. describe:: target variant (aka target in code/help text)
+
+    desktop, winrt, ios or android.
+    When omitted, the command prints all the targets available for a host OS.
+    Note that winrt is only available on Windows, and ios is only available on Mac OS.
 
-.. option:: --tool <tool name>
+.. describe:: <tool name>
 
-    The name of a tool. Use `aqt list tools <host> <target>` to see accepted values.
-    This flag only works with the 'tools' category, and may noy be combined with
-    any other flags.
+    The name of a tool. Use `aqt list-tool <target OS> <target variant>` to see accepted values.
     When set, this prints all 'tool variant names' available.
 
     The output of this command is meant to be used with the `aqt tool` command:
-    See the :ref:`Tool installation commands` below.
+    See the :ref:`Tools installation command` below.
+
+.. option:: --help, -h
+
+    Display help text
+
+
+.. option:: --long, -l
+
+    Long display: shows extra metadata associated with each tool variant.
+    This metadata is displayed in a table, and includes versions and release dates
+    for each tool. If your terminal is wider than 95 characters, `aqt list-tool`
+    will also display the names and descriptions for each tool. An example of this
+    output is displayed below.
+
+.. code-block:: bash
+    $ python -m aqt list-tool windows desktop tools_conan
+
+     Tool Variant Name           Version         Release Date     Display Name              Description
+    ============================================================================================================
+    qt.tools.conan         1.33-202102101246     2021-02-10     Conan 1.33          Conan command line tool 1.33
+    qt.tools.conan.cmake   0.16.0-202102101246   2021-02-10     Conan conan.cmake   Conan conan.cmake (0.16.0)
+
+
 
 Installation command
 --------------------
@@ -197,14 +243,16 @@ Example installation command
 Tools installation command
 ---------------------------
 
+.. program::  aqt
+
 .. option:: tool <target OS> <target variant> <tool name> [<tool variant name>]
 
     install tools specified. tool name may be 'tools_openssl_x64', 'tools_vcredist', 'tools_ninja',
     'tools_ifw', 'tools_cmake'
-    You may use the :ref:`List command` with the `--tool` flag to display what tool names are available.
-
-    tool variants name may be 'qt.tools.openssl.gcc_64', 'qt.tools.vcredist_msvc2013_x64'.
-
+    tool variant names may be 'qt.tools.openssl.gcc_64', 'qt.tools.vcredist_msvc2013_x64'.
+    
+    You may use the :ref:`List Tool command` to display what tools and tool variant names are available.
+    
 
 Command examples
 ================
@@ -241,46 +289,53 @@ Example: Install Web Assembly
     aqt install 5.15.0 linux desktop wasm_32
 
 
-Example: List available versions for Qt5 on Linux
+Example: List available versions of Qt on Linux
 
 .. code-block:: console
 
-    aqt list qt5 linux desktop
+    aqt list-qt linux desktop
 
 
-Example: List available versions for Qt6 on macOS
+Example: List available versions of Qt6 on macOS
 
 .. code-block:: console
 
-    aqt list qt6 mac desktop
+    aqt list-qt mac desktop --spec "6"
 
 
-Example: List available modules for latest version of Qt6 on macOS
+Example: List available modules for latest version of Qt on macOS
 
 .. code-block:: console
 
-    aqt list qt6 mac desktop --modules latest    # prints 'qtquick3d qtshadertools', etc
+    aqt list-qt mac desktop --modules latest    # prints 'qtquick3d qtshadertools', etc
 
 
 Example: List available architectures for Qt 6.1.2 on windows
 
 .. code-block:: console
 
-    aqt list qt6 windows desktop --arch 6.1.2    # prints 'win64_mingw81 win64_msvc2019_64', etc
+    aqt list-qt windows desktop --arch 6.1.2    # prints 'win64_mingw81 win64_msvc2019_64', etc
 
 
 Example: List available tools on windows
 
 .. code-block:: console
 
-    aqt list tools windows desktop    # prints 'tools_ifw tools_qtcreator', etc
+    aqt list-tool windows desktop    # prints 'tools_ifw tools_qtcreator', etc
 
 
 Example: List the variants of IFW available:
 
 .. code-block:: console
 
-    aqt list tools linux desktop --tool tools_ifw   # prints 'qt.tools.ifw.41'
+    aqt list-tool linux desktop tools_ifw       # prints 'qt.tools.ifw.41'
+    # Alternate: `tools_` prefix is optional
+    aqt list-tool linux desktop ifw             # prints 'qt.tools.ifw.41'
+
+
+Example: List the variants of IFW, including version, release date, description, etc.:
+
+    aqt list-tool linux desktop tools_ifw -l    # prints a table of metadata
 
 
 Example: Install an Install FrameWork (IFW):


Test patch:
diff --git a/tests/data/linux-android-expect.json b/tests/data/linux-android-expect.json
index 42de0ec..1fab913 100644
--- a/tests/data/linux-android-expect.json
+++ b/tests/data/linux-android-expect.json
@@ -1,5 +1,5 @@
 {
-  "qt5": {
+  "qt": {
     "qt": [
       "5.9.0 5.9.1 5.9.2 5.9.3 5.9.4 5.9.5 5.9.6 5.9.7 5.9.8 5.9.9",
       "5.10.0 5.10.1",
@@ -7,7 +7,9 @@
       "5.12.0 5.12.1 5.12.2 5.12.3 5.12.4 5.12.5 5.12.6 5.12.7 5.12.8 5.12.9 5.12.10",
       "5.13.0 5.13.1 5.13.2",
       "5.14.0 5.14.1 5.14.2",
-      "5.15.0 5.15.1 5.15.2"
+      "5.15.0 5.15.1 5.15.2",
+      "6.0.0 6.0.1 6.0.2 6.0.3",
+      "6.1.0"
     ],
     "preview": [
       "5.9-preview",
@@ -16,21 +18,12 @@
       "5.12-preview",
       "5.13-preview",
       "5.14-preview",
-      "5.15-preview"
-    ],
-    "wasm": [],
-    "wasm_preview": [],
-    "src_doc_examples": []
-  },
-  "qt6": {
-    "qt": [
-      "6.0.0 6.0.1 6.0.2 6.0.3",
-      "6.1.0"
-    ],
-    "preview": [
+      "5.15-preview",
       "6.0-preview",
       "6.1-preview"
     ],
+    "wasm": [],
+    "wasm_preview": [],
     "src_doc_examples": [],
     "x86_64": [
       "6.0.0 6.0.1 6.0.2 6.0.3",
diff --git a/tests/data/linux-desktop-expect.json b/tests/data/linux-desktop-expect.json
index c5ebfaa..4bfde46 100644
--- a/tests/data/linux-desktop-expect.json
+++ b/tests/data/linux-desktop-expect.json
@@ -1,5 +1,5 @@
 {
-  "qt5": {
+  "qt": {
     "qt": [
       "5.9.0 5.9.1 5.9.2 5.9.3 5.9.4 5.9.5 5.9.6 5.9.7 5.9.8 5.9.9",
       "5.10.0 5.10.1",
@@ -7,7 +7,9 @@
       "5.12.0 5.12.1 5.12.2 5.12.3 5.12.4 5.12.5 5.12.6 5.12.7 5.12.8 5.12.9 5.12.10",
       "5.13.0 5.13.1 5.13.2",
       "5.14.0 5.14.1 5.14.2",
-      "5.15.0 5.15.1 5.15.2"
+      "5.15.0 5.15.1 5.15.2",
+      "6.0.0 6.0.1 6.0.2 6.0.3",
+      "6.1.0"
     ],
     "preview": [
       "5.9-preview",
@@ -16,7 +18,9 @@
       "5.12-preview",
       "5.13-preview",
       "5.14-preview",
-      "5.15-preview"
+      "5.15-preview",
+      "6.0-preview",
+      "6.1-preview"
     ],
     "wasm": [
       "5.13.0 5.13.1 5.13.2",
@@ -35,19 +39,7 @@
       "5.12.0 5.12.1 5.12.2 5.12.3 5.12.4 5.12.5 5.12.6 5.12.7 5.12.8 5.12.9 5.12.10",
       "5.13.0 5.13.1 5.13.2",
       "5.14.0 5.14.1 5.14.2",
-      "5.15.0 5.15.1 5.15.2"
-    ]
-  },
-  "qt6": {
-    "qt": [
-      "6.0.0 6.0.1 6.0.2 6.0.3",
-      "6.1.0"
-    ],
-    "preview": [
-      "6.0-preview",
-      "6.1-preview"
-    ],
-    "src_doc_examples": [
+      "5.15.0 5.15.1 5.15.2",
       "6.0.0 6.0.1 6.0.2 6.0.3",
       "6.1.0"
     ]
diff --git a/tests/data/mac-android-expect.json b/tests/data/mac-android-expect.json
index 42de0ec..1fab913 100644
--- a/tests/data/mac-android-expect.json
+++ b/tests/data/mac-android-expect.json
@@ -1,5 +1,5 @@
 {
-  "qt5": {
+  "qt": {
     "qt": [
       "5.9.0 5.9.1 5.9.2 5.9.3 5.9.4 5.9.5 5.9.6 5.9.7 5.9.8 5.9.9",
       "5.10.0 5.10.1",
@@ -7,7 +7,9 @@
       "5.12.0 5.12.1 5.12.2 5.12.3 5.12.4 5.12.5 5.12.6 5.12.7 5.12.8 5.12.9 5.12.10",
       "5.13.0 5.13.1 5.13.2",
       "5.14.0 5.14.1 5.14.2",
-      "5.15.0 5.15.1 5.15.2"
+      "5.15.0 5.15.1 5.15.2",
+      "6.0.0 6.0.1 6.0.2 6.0.3",
+      "6.1.0"
     ],
     "preview": [
       "5.9-preview",
@@ -16,21 +18,12 @@
       "5.12-preview",
       "5.13-preview",
       "5.14-preview",
-      "5.15-preview"
-    ],
-    "wasm": [],
-    "wasm_preview": [],
-    "src_doc_examples": []
-  },
-  "qt6": {
-    "qt": [
-      "6.0.0 6.0.1 6.0.2 6.0.3",
-      "6.1.0"
-    ],
-    "preview": [
+      "5.15-preview",
       "6.0-preview",
       "6.1-preview"
     ],
+    "wasm": [],
+    "wasm_preview": [],
     "src_doc_examples": [],
     "x86_64": [
       "6.0.0 6.0.1 6.0.2 6.0.3",
diff --git a/tests/data/mac-desktop-expect.json b/tests/data/mac-desktop-expect.json
index 63d8eca..b0d4add 100644
--- a/tests/data/mac-desktop-expect.json
+++ b/tests/data/mac-desktop-expect.json
@@ -1,5 +1,5 @@
 {
-  "qt5": {
+  "qt": {
     "qt": [
       "5.9.0 5.9.1 5.9.2 5.9.3 5.9.4 5.9.5 5.9.6 5.9.7 5.9.8 5.9.9",
       "5.10.0 5.10.1",
@@ -7,7 +7,9 @@
       "5.12.0 5.12.1 5.12.2 5.12.3 5.12.4 5.12.5 5.12.6 5.12.7 5.12.8 5.12.9 5.12.10",
       "5.13.0 5.13.1 5.13.2",
       "5.14.0 5.14.1 5.14.2",
-      "5.15.0 5.15.1 5.15.2"
+      "5.15.0 5.15.1 5.15.2",
+      "6.0.0 6.0.1 6.0.2 6.0.3",
+      "6.1.0"
     ],
     "preview": [
       "5.6-preview",
@@ -17,7 +19,9 @@
       "5.12-preview",
       "5.13-preview",
       "5.14-preview",
-      "5.15-preview"
+      "5.15-preview",
+      "6.0-preview",
+      "6.1-preview"
     ],
     "wasm": [
       "5.13.1 5.13.2",
@@ -36,19 +40,7 @@
       "5.12.0 5.12.1 5.12.2 5.12.3 5.12.4 5.12.5 5.12.6 5.12.7 5.12.8 5.12.9 5.12.10",
       "5.13.0 5.13.1 5.13.2",
       "5.14.0 5.14.1 5.14.2",
-      "5.15.0 5.15.1 5.15.2"
-    ]
-  },
-  "qt6": {
-    "qt": [
-      "6.0.0 6.0.1 6.0.2 6.0.3",
-      "6.1.0"
-    ],
-    "preview": [
-      "6.0-preview",
-      "6.1-preview"
-    ],
-    "src_doc_examples": [
+      "5.15.0 5.15.1 5.15.2",
       "6.0.0 6.0.1 6.0.2 6.0.3",
       "6.1.0"
     ]
diff --git a/tests/data/mac-ios-expect.json b/tests/data/mac-ios-expect.json
index ef946f5..2b1b6e7 100644
--- a/tests/data/mac-ios-expect.json
+++ b/tests/data/mac-ios-expect.json
@@ -1,5 +1,5 @@
 {
-  "qt5": {
+  "qt": {
     "qt": [
       "5.9.0 5.9.1 5.9.2 5.9.3 5.9.4 5.9.5 5.9.6 5.9.7 5.9.8 5.9.9",
       "5.10.0 5.10.1",
@@ -7,7 +7,9 @@
       "5.12.0 5.12.1 5.12.2 5.12.3 5.12.4 5.12.5 5.12.6 5.12.7 5.12.8 5.12.9 5.12.10",
       "5.13.0 5.13.1 5.13.2",
       "5.14.0 5.14.1 5.14.2",
-      "5.15.0 5.15.1 5.15.2"
+      "5.15.0 5.15.1 5.15.2",
+      "6.0.0 6.0.1 6.0.2 6.0.3",
+      "6.1.0"
     ],
     "preview": [
       "5.9-preview",
@@ -16,21 +18,12 @@
       "5.12-preview",
       "5.13-preview",
       "5.14-preview",
-      "5.15-preview"
-    ],
-    "wasm": [],
-    "wasm_preview": [],
-    "src_doc_examples": []
-  },
-  "qt6": {
-    "qt": [
-      "6.0.0 6.0.1 6.0.2 6.0.3",
-      "6.1.0"
-    ],
-    "preview": [
+      "5.15-preview",
       "6.0-preview",
       "6.1-preview"
     ],
+    "wasm": [],
+    "wasm_preview": [],
     "src_doc_examples": []
   },
   "tools": [
diff --git a/tests/data/windows-android-expect.json b/tests/data/windows-android-expect.json
index 42de0ec..1fab913 100644
--- a/tests/data/windows-android-expect.json
+++ b/tests/data/windows-android-expect.json
@@ -1,5 +1,5 @@
 {
-  "qt5": {
+  "qt": {
     "qt": [
       "5.9.0 5.9.1 5.9.2 5.9.3 5.9.4 5.9.5 5.9.6 5.9.7 5.9.8 5.9.9",
       "5.10.0 5.10.1",
@@ -7,7 +7,9 @@
       "5.12.0 5.12.1 5.12.2 5.12.3 5.12.4 5.12.5 5.12.6 5.12.7 5.12.8 5.12.9 5.12.10",
       "5.13.0 5.13.1 5.13.2",
       "5.14.0 5.14.1 5.14.2",
-      "5.15.0 5.15.1 5.15.2"
+      "5.15.0 5.15.1 5.15.2",
+      "6.0.0 6.0.1 6.0.2 6.0.3",
+      "6.1.0"
     ],
     "preview": [
       "5.9-preview",
@@ -16,21 +18,12 @@
       "5.12-preview",
       "5.13-preview",
       "5.14-preview",
-      "5.15-preview"
-    ],
-    "wasm": [],
-    "wasm_preview": [],
-    "src_doc_examples": []
-  },
-  "qt6": {
-    "qt": [
-      "6.0.0 6.0.1 6.0.2 6.0.3",
-      "6.1.0"
-    ],
-    "preview": [
+      "5.15-preview",
       "6.0-preview",
       "6.1-preview"
     ],
+    "wasm": [],
+    "wasm_preview": [],
     "src_doc_examples": [],
     "x86_64": [
       "6.0.0 6.0.1 6.0.2 6.0.3",
diff --git a/tests/data/windows-desktop-expect.json b/tests/data/windows-desktop-expect.json
index 5cd145f..e847d89 100644
--- a/tests/data/windows-desktop-expect.json
+++ b/tests/data/windows-desktop-expect.json
@@ -1,5 +1,5 @@
 {
-  "qt5": {
+  "qt": {
     "qt": [
       "5.9.0 5.9.1 5.9.2 5.9.3 5.9.4 5.9.5 5.9.6 5.9.7 5.9.8 5.9.9",
       "5.10.0 5.10.1",
@@ -7,7 +7,9 @@
       "5.12.0 5.12.1 5.12.2 5.12.3 5.12.4 5.12.5 5.12.6 5.12.7 5.12.8 5.12.9 5.12.10",
       "5.13.0 5.13.1 5.13.2",
       "5.14.0 5.14.1 5.14.2",
-      "5.15.0 5.15.1 5.15.2"
+      "5.15.0 5.15.1 5.15.2",
+      "6.0.0 6.0.1 6.0.2 6.0.3",
+      "6.1.0"
     ],
     "preview": [
       "5.6-preview",
@@ -17,7 +19,9 @@
       "5.12-preview",
       "5.13-preview",
       "5.14-preview",
-      "5.15-preview"
+      "5.15-preview",
+      "6.0-preview",
+      "6.1-preview"
     ],
     "wasm": [
       "5.13.1 5.13.2",
@@ -42,19 +46,7 @@
       "5.12.0 5.12.1 5.12.2 5.12.3 5.12.4 5.12.5 5.12.6 5.12.7 5.12.8 5.12.9 5.12.10",
       "5.13.0 5.13.1 5.13.2",
       "5.14.0 5.14.1 5.14.2",
-      "5.15.0 5.15.1 5.15.2"
-    ]
-  },
-  "qt6": {
-    "qt": [
-      "6.0.0 6.0.1 6.0.2 6.0.3",
-      "6.1.0"
-    ],
-    "preview": [
-      "6.0-preview",
-      "6.1-preview"
-    ],
-    "src_doc_examples": [
+      "5.15.0 5.15.1 5.15.2",
       "6.0.0 6.0.1 6.0.2 6.0.3",
       "6.1.0"
     ]
diff --git a/tests/data/windows-winrt-expect.json b/tests/data/windows-winrt-expect.json
index 97a27ca..c728195 100644
--- a/tests/data/windows-winrt-expect.json
+++ b/tests/data/windows-winrt-expect.json
@@ -1,5 +1,5 @@
 {
-  "qt5": {
+  "qt": {
     "qt": [
       "5.9.0 5.9.1 5.9.2 5.9.3 5.9.4 5.9.5 5.9.6 5.9.7 5.9.8 5.9.9",
       "5.10.0 5.10.1",
@@ -22,11 +22,6 @@
     "wasm_preview": [],
     "src_doc_examples": []
   },
-  "qt6": {
-    "qt": [],
-    "preview": [],
-    "src_doc_examples": []
-  },
   "tools": [
     "tools_qt3dstudio_runtime_preview",
     "tools_qt3dstudio_runtime_240",
diff --git a/tests/test_cli.py b/tests/test_cli.py
index ce66280..4f02626 100644
--- a/tests/test_cli.py
+++ b/tests/test_cli.py
@@ -11,7 +11,7 @@ def test_cli_help(capsys):
     expected = "".join(
         [
             "usage: aqt [-h] [-c CONFIG]\n",
-            "           {install,doc,examples,src,tool,list,help,version} ...\n",
+            "           {install,doc,examples,src,tool,list-qt,list-tool,help,version} ...\n",
             "\n",
             "Installer for Qt SDK.\n",
             "\n",
@@ -23,7 +23,7 @@ def test_cli_help(capsys):
             "subcommands:\n",
             "  Valid subcommands\n",
             "\n",
-            "  {install,doc,examples,src,tool,list,help,version}\n",
+            "  {install,doc,examples,src,tool,list-qt,list-tool,help,version}\n",
             "                        subcommand for aqt Qt installer\n",
         ]
     )
@@ -81,7 +81,7 @@ def test_cli_invalid_version(capsys, invalid_version):
     for cmd in (
         ("install", invalid_version, "mac", "desktop"),
         ("doc", invalid_version, "mac", "desktop"),
-        ("list", "qt5", "mac", "desktop", "--modules", invalid_version),
+        ("list-qt", "mac", "desktop", "--modules", invalid_version),
     ):
         with pytest.raises(SystemExit) as pytest_wrapped_e:
             cli = Cli()
@@ -108,7 +108,7 @@ def test_cli_launch_with_no_argument(capsys):
     expected = "".join(
         [
             "usage: aqt [-h] [-c CONFIG]\n",
-            "           {install,doc,examples,src,tool,list,help,version} ...\n",
+            "           {install,doc,examples,src,tool,list-qt,list-tool,help,version} ...\n",
             "\n",
             "Installer for Qt SDK.\n",
             "\n",
@@ -120,7 +120,7 @@ def test_cli_launch_with_no_argument(capsys):
             "subcommands:\n",
             "  Valid subcommands\n",
             "\n",
-            "  {install,doc,examples,src,tool,list,help,version}\n",
+            "  {install,doc,examples,src,tool,list-qt,list-tool,help,version}\n",
             "                        subcommand for aqt Qt installer\n",
         ]
     )
diff --git a/tests/test_list.py b/tests/test_list.py
index 19a32f0..62ce94f 100644
--- a/tests/test_list.py
+++ b/tests/test_list.py
@@ -4,7 +4,8 @@ import re
 import shutil
 import sys
 from pathlib import Path
-from typing import Generator
+from typing import Dict, List, Set, Union
+from urllib.parse import urlparse
 
 import pytest
 
@@ -15,6 +16,7 @@ from aqt.metadata import (
     ArchiveId,
     MetadataFactory,
     SimpleSpec,
+    ToolData,
     Version,
     Versions,
     format_suggested_follow_up,
@@ -25,51 +27,57 @@ from aqt.metadata import (
 Settings.load_settings()
 
 
-def test_versions():
-    versions = Versions(
-        [
-            (1, [Version("1.1.1"), Version("1.1.2")]),
-            (2, [Version("1.2.1"), Version("1.2.2")]),
-        ]
-    )
-    assert (
-        str(versions)
-        == "[[Version('1.1.1'), Version('1.1.2')], [Version('1.2.1'), Version('1.2.2')]]"
-    )
-    assert format(versions) == "1.1.1 1.1.2\n1.2.1 1.2.2"
-    assert format(versions, "s") == str(versions)
-    assert versions.flattened() == [
-        Version("1.1.1"),
-        Version("1.1.2"),
-        Version("1.2.1"),
-        Version("1.2.2"),
-    ]
-    assert isinstance(versions.__iter__(), Generator)
-    assert versions.latest() == Version("1.2.2")
-    assert versions
-
-    empty_versions = Versions(None)
-    assert str(empty_versions) == "[]"
-    assert format(empty_versions) == ""
-    assert empty_versions.flattened() == []
-    assert isinstance(empty_versions.__iter__(), Generator)
-    assert empty_versions.latest() is None
-    assert not empty_versions
-
-    one_version = Versions(Version("1.2.3"))
-    assert str(one_version) == "[[Version('1.2.3')]]"
-    assert format(one_version) == "1.2.3"
-    assert one_version.flattened() == [Version("1.2.3")]
-    assert isinstance(one_version.__iter__(), Generator)
-    assert one_version.latest() == Version("1.2.3")
-    assert one_version
+@pytest.mark.parametrize(
+    "init_data, expect_str, expect_fmt, expect_flat, expect_last, expect_bool",
+    (
+        (
+            [
+                (1, [Version("1.1.1"), Version("1.1.2")]),
+                (2, [Version("1.2.1"), Version("1.2.2")]),
+            ],
+            "[[Version('1.1.1'), Version('1.1.2')], [Version('1.2.1'), Version('1.2.2')]]",
+            "1.1.1 1.1.2\n1.2.1 1.2.2",
+            [Version("1.1.1"), Version("1.1.2"), Version("1.2.1"), Version("1.2.2")],
+            Version("1.2.2"),
+            True,
+        ),
+        (
+            [],
+            "[]",
+            "",
+            [],
+            None,
+            False,
+        ),
+        (
+            Version("1.2.3"),
+            "[[Version('1.2.3')]]",
+            "1.2.3",
+            [Version("1.2.3")],
+            Version("1.2.3"),
+            True,
+        ),
+    ),
+)
+def test_versions(
+    init_data, expect_str, expect_fmt, expect_flat, expect_last, expect_bool
+):
+    versions = Versions(init_data)
+    assert str(versions) == expect_str
+    assert format(versions) == expect_fmt
+    assert format(versions, "s") == expect_str
+    assert versions.flattened() == expect_flat
+    assert versions.latest() == expect_last
+    assert bool(versions) == expect_bool
 
     with pytest.raises(TypeError) as pytest_wrapped_e:
         format(versions, "x")
     assert pytest_wrapped_e.type == TypeError
 
 
-MINOR_REGEX = re.compile(r"^\d+\.(\d+)")
+@pytest.fixture
+def spec_regex() -> re.Pattern:
+    return re.compile(r"^(\d+\.\d+)")
 
 
 @pytest.mark.parametrize(
@@ -85,7 +93,9 @@ MINOR_REGEX = re.compile(r"^\d+\.(\d+)")
         ("mac", "ios", "mac-ios.html", "mac-ios-expect.json"),
     ],
 )
-def test_list_versions_tools(monkeypatch, os_name, target, in_file, expect_out_file):
+def test_list_versions_tools(
+    monkeypatch, spec_regex, os_name, target, in_file, expect_out_file
+):
     _html = (Path(__file__).parent / "data" / in_file).read_text("utf-8")
     monkeypatch.setattr(MetadataFactory, "fetch_http", lambda self, _: _html)
 
@@ -93,46 +103,50 @@ def test_list_versions_tools(monkeypatch, os_name, target, in_file, expect_out_f
         (Path(__file__).parent / "data" / expect_out_file).read_text("utf-8")
     )
 
-    # Test 'aqt list tools'
+    # Test 'aqt list-tool'
     tools = MetadataFactory(ArchiveId("tools", os_name, target)).getList()
     assert tools == expected["tools"]
 
-    for qt in ("qt5", "qt6"):
-        for ext, expected_output in expected[qt].items():
-            # Test 'aqt list qt'
-            archive_id = ArchiveId(qt, os_name, target, ext if ext != "qt" else "")
-            all_versions = MetadataFactory(archive_id).getList()
-
-            if len(expected_output) == 0:
-                assert not all_versions
-            else:
-                assert f"{all_versions}" == "\n".join(expected_output)
-
-            # Filter for the latest version only
-            latest_ver = MetadataFactory(archive_id, is_latest_version=True).getList()
-
-            if len(expected_output) == 0:
-                assert not latest_ver
-            else:
-                assert f"{latest_ver}" == expected_output[-1].split(" ")[-1]
-
-            for row in expected_output:
-                minor = int(MINOR_REGEX.search(row).group(1))
+    for ext, expected_output in expected["qt"].items():
+        # Test 'aqt list-qt'
+        archive_id = ArchiveId("qt", os_name, target, ext if ext != "qt" else "")
+        all_versions = MetadataFactory(archive_id).getList()
+
+        if len(expected_output) == 0:
+            assert not all_versions
+        else:
+            assert f"{all_versions}" == "\n".join(expected_output)
+
+        # Filter for the latest version only
+        latest_ver = MetadataFactory(archive_id, is_latest_version=True).getList()
+
+        if len(expected_output) == 0:
+            assert not latest_ver
+        else:
+            assert f"{latest_ver}" == expected_output[-1].split(" ")[-1]
+
+        for row in expected_output:
+            spec_str = spec_regex.search(row).group(1)
+            spec = (
+                SimpleSpec(spec_str)
+                if not ext.endswith("preview")
+                else SimpleSpec(spec_str + ".0-preview")
+            )
 
-                # Find the latest version for a particular minor version
-                latest_ver_for_minor = MetadataFactory(
-                    archive_id,
-                    filter_minor=minor,
-                    is_latest_version=True,
-                ).getList()
-                assert f"{latest_ver_for_minor}" == row.split(" ")[-1]
+            # Find the latest version for a particular spec
+            latest_ver_for_spec = MetadataFactory(
+                archive_id,
+                spec=spec,
+                is_latest_version=True,
+            ).getList()
+            assert f"{latest_ver_for_spec}" == row.split(" ")[-1]
 
-                # Find all versions for a particular minor version
-                all_ver_for_minor = MetadataFactory(
-                    archive_id,
-                    filter_minor=minor,
-                ).getList()
-                assert f"{all_ver_for_minor}" == row
+            # Find all versions for a particular spec
+            all_ver_for_spec = MetadataFactory(
+                archive_id,
+                spec=spec,
+            ).getList()
+            assert f"{all_ver_for_spec}" == row
 
 
 @pytest.mark.parametrize(
@@ -151,7 +165,7 @@ def test_list_versions_tools(monkeypatch, os_name, target, in_file, expect_out_f
 def test_list_architectures_and_modules(
     monkeypatch, version: str, extension: str, in_file: str, expect_out_file: str
 ):
-    archive_id = ArchiveId("qt" + version[0], "windows", "desktop", extension)
+    archive_id = ArchiveId("qt", "windows", "desktop", extension)
     _xml = (Path(__file__).parent / "data" / in_file).read_text("utf-8")
     expect = json.loads(
         (Path(__file__).parent / "data" / expect_out_file).read_text("utf-8")
@@ -188,113 +202,136 @@ def test_tool_modules(monkeypatch, host: str, target: str, tool_name: str):
     modules = MetadataFactory(archive_id, tool_name=tool_name).getList()
     assert modules == expect["modules"]
 
-    table = MetadataFactory(archive_id, tool_long_listing=tool_name).getList()
+    table = MetadataFactory(
+        archive_id, tool_name=tool_name, is_long_listing=True
+    ).getList()
     assert table._rows() == expect["long_listing"]
 
 
+@pytest.fixture
+def expected_windows_desktop_5140() -> Dict[str, Set[str]]:
+    xmlexpect = "windows-5140-expect.json"
+    expected_modules_arches = json.loads(
+        (Path(__file__).parent / "data" / xmlexpect).read_text("utf-8")
+    )
+    return {key: set(val) for key, val in expected_modules_arches.items()}
+
+
 @pytest.mark.parametrize(
-    "cat, host, target, minor_ver, ver, ext, xmlfile, xmlexpect, htmlfile, htmlexpect",
-    [
-        (
-            "qt5",
-            "windows",
-            "desktop",
-            "14",
-            "5.14.0",
-            "wasm",
-            "windows-5140-update.xml",
-            "windows-5140-expect.json",
-            "windows-desktop.html",
-            "windows-desktop-expect.json",
-        ),
-    ],
+    "args, expect",
+    (
+        ("--extensions latest", {"src_doc_examples"}),
+        ("--spec 5.14 --extensions latest", {"wasm", "src_doc_examples"}),
+        ("--extensions 5.14.0", {"wasm", "src_doc_examples"}),
+        ("--modules latest", "modules"),
+        ("--spec 5.14 --modules latest", "modules"),
+        ("--modules 5.14.0", "modules"),
+        ("--extension wasm --modules latest", "modules"),
+        ("--extension wasm --spec 5.14 --modules latest", "modules"),
+        ("--extension wasm --modules 5.14.0", "modules"),
+        ("--arch latest", "architectures"),
+        ("--spec 5.14 --arch latest", "architectures"),
+        ("--arch 5.14.0", "architectures"),
+        ("--extension wasm --arch latest", "architectures"),
+        ("--extension wasm --spec 5.14 --arch latest", "architectures"),
+        ("--extension wasm --arch 5.14.0", "architectures"),
+    ),
 )
-def test_list_cli(
-    capsys,
+def test_list_qt_cli(
     monkeypatch,
-    cat,
-    host,
-    target,
-    minor_ver,
-    ver,
-    ext,
-    xmlfile,
-    xmlexpect,
-    htmlfile,
-    htmlexpect,
+    capsys,
+    expected_windows_desktop_5140: Dict[str, Set[str]],
+    args: str,
+    expect: Union[Set[str], str],
 ):
-    def _mock(_, rest_of_url: str) -> str:
-        in_file = xmlfile if rest_of_url.endswith("Updates.xml") else htmlfile
-        text = (Path(__file__).parent / "data" / in_file).read_text("utf-8")
+    htmlfile, xmlfile = "windows-desktop.html", "windows-5140-update.xml"
+    version_string_to_replace = "qt5.5140"
+
+    def _mock_fetch_http(_, rest_of_url: str) -> str:
+        htmltext = (Path(__file__).parent / "data" / htmlfile).read_text("utf-8")
         if not rest_of_url.endswith("Updates.xml"):
-            return text
+            return htmltext
 
+        xmltext = (Path(__file__).parent / "data" / xmlfile).read_text("utf-8")
         # If we are serving an Updates.xml, `aqt list` will look for a Qt version number.
         # We will replace the version numbers in the file with the requested version.
-        match = re.search(r"qt\d_(\d+)", rest_of_url)
+        match = re.search(r"qt(\d)_(\d+)", rest_of_url)
         assert match
-        desired_version = match.group(1)
-        ver_to_replace = ver.replace(".", "")
-        return text.replace(ver_to_replace, desired_version)
+        major, version_nodot = match.groups()
+        desired_version_string = f"qt{major}.{version_nodot}"
+        return xmltext.replace(version_string_to_replace, desired_version_string)
 
-    monkeypatch.setattr(MetadataFactory, "fetch_http", _mock)
+    monkeypatch.setattr(MetadataFactory, "fetch_http", _mock_fetch_http)
 
-    expected_modules_arches = json.loads(
-        (Path(__file__).parent / "data" / xmlexpect).read_text("utf-8")
+    cli = Cli()
+    cli.run(["list-qt", "windows", "desktop", *args.split()])
+    out, err = capsys.readouterr()
+    output_set = set(out.strip().split())
+    expect_set = (
+        expected_windows_desktop_5140[expect] if isinstance(expect, str) else expect
     )
-    expect_modules = expected_modules_arches["modules"]
-    expect_arches = expected_modules_arches["architectures"]
+    assert output_set == expect_set
+
+
+@pytest.mark.parametrize(
+    "cmd, host, expect",
+    (
+        ("list-qt", "windows", {"desktop", "android", "winrt"}),
+        ("list-qt", "linux", {"desktop", "android"}),
+        ("list-qt", "mac", {"desktop", "android", "ios"}),
+        ("list-tool", "windows", {"desktop", "android", "winrt"}),
+        ("list-tool", "linux", {"desktop", "android"}),
+        ("list-tool", "mac", {"desktop", "android", "ios"}),
+    ),
+)
+def test_list_targets(capsys, cmd: str, host: str, expect: Set[str]):
+    cli = Cli()
+    cli.run([cmd, host])
+    out, err = capsys.readouterr()
+    output_set = set(out.strip().split())
+    assert output_set == expect
 
-    def check_extensions():
-        out, err = capsys.readouterr()
-        # We should probably generate expected from htmlexpect, but this will work for now
-        assert out.strip() == "wasm src_doc_examples"
 
-    def check_modules():
-        out, err = capsys.readouterr()
-        assert set(out.strip().split()) == set(expect_modules)
+@pytest.mark.parametrize(
+    "cmd, host, target",
+    (
+        ("list-qt", "windows", "ios"),
+        ("list-qt", "linux", "ios"),
+        ("list-qt", "linux", "winrt"),
+        ("list-qt", "mac", "winrt"),
+        ("list-tool", "windows", "ios"),
+        ("list-tool", "linux", "ios"),
+        ("list-tool", "linux", "winrt"),
+        ("list-tool", "mac", "winrt"),
+    ),
+)
+def test_list_wrong_target(capsys, cmd: str, host: str, target: str):
+    expect = f"'{target}' is not a valid target for host '{host}'"
 
-    def check_arches():
-        out, err = capsys.readouterr()
-        assert set(out.strip().split()) == set(expect_arches)
+    cli = Cli()
+    return_code = cli.run([cmd, host, target])
+    out, err = capsys.readouterr()
+    assert return_code == 1
+    assert err.strip() == expect
 
-    _minor = ["--filter-minor", minor_ver]
-    _ext = ["--extension", ext]
 
+@pytest.mark.parametrize(
+    "cmd, spec",
+    (
+        ("list-qt", "not a spec"),
+        ("list-qt", "1...3"),
+        ("list-qt", ""),
+        ("list-qt", ">3 <5"),
+    ),
+)
+def test_invalid_spec(capsys, cmd: str, spec: str):
+    expect_prefix = f"Invalid version specification: '{spec}'"
+    host, target = "linux", "desktop"
     cli = Cli()
-    # Query extensions by latest version, minor version, and specific version
-    cli.run(["list", cat, host, target, "--extensions", "latest"])
-    check_extensions()
-    cli.run(["list", cat, host, target, *_minor, "--extensions", "latest"])
-    check_extensions()
-    cli.run(["list", cat, host, target, "--extensions", ver])
-    check_extensions()
-    # Query modules by latest version, minor version, and specific version
-    cli.run(["list", cat, host, target, "--modules", "latest"])
-    check_modules()
-    cli.run(["list", cat, host, target, *_minor, "--modules", "latest"])
-    check_modules()
-    cli.run(["list", cat, host, target, "--modules", ver])
-    check_modules()
-    cli.run(["list", cat, host, target, *_ext, "--modules", "latest"])
-    check_modules()
-    cli.run(["list", cat, host, target, *_ext, *_minor, "--modules", "latest"])
-    check_modules()
-    cli.run(["list", cat, host, target, *_ext, "--modules", ver])
-    check_modules()
-    # Query architectures by latest version, minor version, and specific version
-    cli.run(["list", cat, host, target, "--arch", "latest"])
-    check_arches()
-    cli.run(["list", cat, host, target, *_minor, "--arch", "latest"])
-    check_arches()
-    cli.run(["list", cat, host, target, "--arch", ver])
-    check_arches()
-    cli.run(["list", cat, host, target, *_ext, "--arch", "latest"])
-    check_arches()
-    cli.run(["list", cat, host, target, *_ext, *_minor, "--arch", "latest"])
-    check_arches()
-    cli.run(["list", cat, host, target, *_ext, "--arch", ver])
-    check_arches()
+    return_code = cli.run([cmd, host, target, "--spec", spec])
+    out, err = capsys.readouterr()
+    assert return_code == 1
+    assert err.strip().startswith(expect_prefix)
 
 
 @pytest.mark.parametrize(
@@ -349,72 +386,66 @@ no_wasm_msg = "The extension 'wasm' is only available in Qt 5.13 to 5.15 on desk
 def test_list_invalid_extensions(
     capsys, monkeypatch, target, ext, version, expected_msg
 ):
-    def _mock(_, rest_of_url: str) -> str:
-        return ""
-
-    monkeypatch.setattr(MetadataFactory, "fetch_http", _mock)
-
-    cat = "qt" + version[0]
     host = "windows"
     extension_params = ["--extension", ext] if ext else []
     cli = Cli()
-    cli.run(["list", cat, host, target, *extension_params, "--arch", version])
+    cli.run(["list-qt", host, target, *extension_params, "--arch", version])
     out, err = capsys.readouterr()
     sys.stdout.write(out)
     sys.stderr.write(err)
     assert expected_msg in err
 
 
-mac_qt5 = ArchiveId("qt5", "mac", "desktop")
-mac_wasm = ArchiveId("qt5", "mac", "desktop", "wasm")
+mac_qt = ArchiveId("qt", "mac", "desktop")
+mac_wasm = ArchiveId("qt", "mac", "desktop", "wasm")
 wrong_qt_version_msg = [
-    "Please use 'aqt list qt5 mac desktop' to show versions of Qt available."
+    "Please use 'aqt list-qt mac desktop' to show versions of Qt available."
 ]
 wrong_ext_and_version_msg = [
-    "Please use 'aqt list qt5 mac desktop --extensions <QT_VERSION>' to list valid extensions.",
-    "Please use 'aqt list qt5 mac desktop' to show versions of Qt available.",
+    "Please use 'aqt list-qt mac desktop --extensions <QT_VERSION>' to list valid extensions.",
+    "Please use 'aqt list-qt mac desktop' to show versions of Qt available.",
 ]
 
 
 @pytest.mark.parametrize(
     "meta, expected_message",
     (
-        (MetadataFactory(mac_qt5), []),
+        (MetadataFactory(mac_qt), []),
         (
-            MetadataFactory(mac_qt5, filter_minor=0),
+            MetadataFactory(mac_qt, spec=SimpleSpec("5.0")),
             [
-                "Please use 'aqt list qt5 mac desktop' to check that versions of qt5 exist with the minor version '0'."
+                "Please use 'aqt list-qt mac desktop' to check that versions of qt exist within the spec '5.0'."
             ],
         ),
         (
             MetadataFactory(ArchiveId("tools", "mac", "desktop"), tool_name="ifw"),
             [
-                "Please use 'aqt list tools mac desktop' to check what tools are available."
+                "Please use 'aqt list-tool mac desktop' to check what tools are available."
             ],
         ),
         (
-            MetadataFactory(mac_qt5, architectures_ver="1.2.3"),
+            MetadataFactory(mac_qt, architectures_ver="1.2.3"),
             wrong_qt_version_msg,
         ),
         (
-            MetadataFactory(mac_qt5, modules_ver="1.2.3"),
+            MetadataFactory(mac_qt, modules_ver="1.2.3"),
             wrong_qt_version_msg,
         ),
         (
-            MetadataFactory(mac_qt5, extensions_ver="1.2.3"),
+            MetadataFactory(mac_qt, extensions_ver="1.2.3"),
             wrong_qt_version_msg,
         ),
         (
             MetadataFactory(mac_wasm),
             [
-                "Please use 'aqt list qt5 mac desktop --extensions <QT_VERSION>' to list valid extensions."
+                "Please use 'aqt list-qt mac desktop --extensions <QT_VERSION>' to list valid extensions."
             ],
         ),
         (
-            MetadataFactory(mac_wasm, filter_minor=0),
+            MetadataFactory(mac_wasm, spec=SimpleSpec("<5.9")),
             [
-                "Please use 'aqt list qt5 mac desktop --extensions <QT_VERSION>' to list valid extensions.",
-                "Please use 'aqt list qt5 mac desktop' to check that versions of qt5 exist with the minor version '0'.",
+                "Please use 'aqt list-qt mac desktop --extensions <QT_VERSION>' to list valid extensions.",
+                "Please use 'aqt list-qt mac desktop' to check that versions of qt exist within the spec '<5.9'.",
             ],
         ),
         (
@@ -422,8 +453,8 @@ wrong_ext_and_version_msg = [
                 ArchiveId("tools", "mac", "desktop", "wasm"), tool_name="ifw"
             ),
             [
-                "Please use 'aqt list tools mac desktop --extensions <QT_VERSION>' to list valid extensions.",
-                "Please use 'aqt list tools mac desktop' to check what tools are available.",
+                "Please use 'aqt list-tool mac desktop --extensions <QT_VERSION>' to list valid extensions.",
+                "Please use 'aqt list-tool mac desktop' to check what tools are available.",
             ],
         ),
         (
@@ -446,13 +477,13 @@ def test_suggested_follow_up(meta: MetadataFactory, expected_message: str):
 
 def test_format_suggested_follow_up():
     suggestions = [
-        "Please use 'aqt list tools mac desktop --extensions <QT_VERSION>' to list valid extensions.",
-        "Please use 'aqt list tools mac desktop' to check what tools are available.",
+        "Please use 'aqt list-tool mac desktop --extensions <QT_VERSION>' to list valid extensions.",
+        "Please use 'aqt list-tool mac desktop' to check what tools are available.",
     ]
     expected = (
         "==============================Suggested follow-up:==============================\n"
-        "* Please use 'aqt list tools mac desktop --extensions <QT_VERSION>' to list valid extensions.\n"
-        "* Please use 'aqt list tools mac desktop' to check what tools are available."
+        "* Please use 'aqt list-tool mac desktop --extensions <QT_VERSION>' to list valid extensions.\n"
+        "* Please use 'aqt list-tool mac desktop' to check what tools are available."
     )
 
     assert format_suggested_follow_up(suggestions) == expected
@@ -466,19 +497,19 @@ def test_format_suggested_follow_up_empty():
     "meta, expect",
     (
         (
-            MetadataFactory(ArchiveId("qt5", "mac", "desktop"), filter_minor=42),
-            "qt5/mac/desktop with minor version 42",
+            MetadataFactory(ArchiveId("qt", "mac", "desktop"), spec=SimpleSpec("5.42")),
+            "qt/mac/desktop with spec 5.42",
         ),
         (
             MetadataFactory(
-                ArchiveId("qt5", "mac", "desktop", "wasm"), filter_minor=42
+                ArchiveId("qt", "mac", "desktop", "wasm"), spec=SimpleSpec("5.42")
             ),
-            "qt5/mac/desktop/wasm with minor version 42",
+            "qt/mac/desktop/wasm with spec 5.42",
         ),
-        (MetadataFactory(ArchiveId("qt5", "mac", "desktop")), "qt5/mac/desktop"),
+        (MetadataFactory(ArchiveId("qt", "mac", "desktop")), "qt/mac/desktop"),
         (
-            MetadataFactory(ArchiveId("qt5", "mac", "desktop", "wasm")),
-            "qt5/mac/desktop/wasm",
+            MetadataFactory(ArchiveId("qt", "mac", "desktop", "wasm")),
+            "qt/mac/desktop/wasm",
         ),
     ),
 )
@@ -487,50 +518,37 @@ def test_list_describe_filters(meta: MetadataFactory, expect: str):
 
 
 @pytest.mark.parametrize(
-    "archive_id, filter_minor, version_str, expect",
+    "archive_id, spec, version_str, expect",
     (
-        (mac_qt5, None, "5.12.42", Version("5.12.42")),
+        (mac_qt, None, "5.12.42", Version("5.12.42")),
         (
-            mac_qt5,
-            None,
-            "6.12.42",
-            CliInputError("Major version mismatch between qt5 and 6.12.42"),
-        ),
-        (
-            mac_qt5,
+            mac_qt,
             None,
             "not a version",
             CliInputError("Invalid version string: 'not a version'"),
         ),
-        (mac_qt5, None, "latest", Version("5.15.2")),
+        (mac_qt, SimpleSpec("5"), "latest", Version("5.15.2")),
         (
-            mac_qt5,
-            0,
+            mac_qt,
+            SimpleSpec("5.0"),
             "latest",
             CliInputError(
-                "There is no latest version of Qt with the criteria 'qt5/mac/desktop with minor version 0'"
+                "There is no latest version of Qt with the criteria 'qt/mac/desktop with spec 5.0'"
             ),
         ),
     ),
 )
-def test_list_to_version(monkeypatch, archive_id, filter_minor, version_str, expect):
+def test_list_to_version(monkeypatch, archive_id, spec, version_str, expect):
     _html = (Path(__file__).parent / "data" / "mac-desktop.html").read_text("utf-8")
     monkeypatch.setattr(MetadataFactory, "fetch_http", lambda self, _: _html)
 
     if isinstance(expect, Exception):
         with pytest.raises(CliInputError) as error:
-            MetadataFactory(archive_id, filter_minor=filter_minor)._to_version(
-                version_str
-            )
+            MetadataFactory(archive_id, spec=spec)._to_version(version_str)
         assert error.type == CliInputError
         assert str(expect) == str(error.value)
     else:
-        assert (
-            MetadataFactory(archive_id, filter_minor=filter_minor)._to_version(
-                version_str
-            )
-            == expect
-        )
+        assert MetadataFactory(archive_id, spec=spec)._to_version(version_str) == expect
 
 
 def test_list_fetch_tool_by_simple_spec(monkeypatch):
@@ -635,7 +653,9 @@ def test_show_list_tools_long_ifw(capsys, monkeypatch, columns, expect):
     )
 
     meta = MetadataFactory(
-        ArchiveId("tools", "mac", "desktop"), tool_long_listing="tools_ifw"
+        ArchiveId("tools", "mac", "desktop"),
+        tool_name="tools_ifw",
+        is_long_listing=True,
     )
     assert show_list(meta) == 0
     out, err = capsys.readouterr()
@@ -649,9 +669,9 @@ def test_show_list_versions(monkeypatch, capsys):
     monkeypatch.setattr(MetadataFactory, "fetch_http", lambda *args: _html)
 
     expect_file = Path(__file__).parent / "data" / "mac-desktop-expect.json"
-    expected = "\n".join(json.loads(expect_file.read_text("utf-8"))["qt5"]["qt"]) + "\n"
+    expected = "\n".join(json.loads(expect_file.read_text("utf-8"))["qt"]["qt"]) + "\n"
 
-    assert show_list(MetadataFactory(mac_qt5)) == 0
+    assert show_list(MetadataFactory(mac_qt)) == 0
     out, err = capsys.readouterr()
     assert out == expected
 
@@ -671,6 +691,70 @@ def test_show_list_tools(monkeypatch, capsys):
     assert out == expect
 
 
+def fetch_expected_tooldata(json_filename: str) -> ToolData:
+    text = (Path(__file__).parent / "data" / json_filename).read_text("utf-8")
+    raw_tooldata: List[List[str]] = json.loads(text)["long_listing"]
+    keys = ("Version", "ReleaseDate", "DisplayName", "Description")
+
+    tools: Dict[str, Dict[str, str]] = {}
+    for variant_name, *values in raw_tooldata:
+        assert len(keys) == len(values)
+        tools[variant_name] = {k: v for k, v in zip(keys, values)}
+
+    return ToolData(tools)
+
+
+@pytest.mark.parametrize(
+    "host, target, tool_name", (("mac", "desktop", "tools_cmake"),)
+)
+def test_list_tool_cli(monkeypatch, capsys, host: str, target: str, tool_name: str):
+    html_file = f"{host}-{target}.html"
+    xml_file = f"{host}-{target}-{tool_name}-update.xml"
+    html_expect = f"{host}-{target}-expect.json"
+    xml_expect = f"{host}-{target}-{tool_name}-expect.json"
+    htmltext, xmltext, htmljson, xmljson = [
+        (Path(__file__).parent / "data" / filename).read_text("utf-8")
+        for filename in (html_file, xml_file, html_expect, xml_expect)
+    ]
+    expected_tools = set(json.loads(htmljson)["tools"])
+    xml_data = json.loads(xmljson)
+    expected_tool_modules = set(xml_data["modules"])
+
+    def _mock_fetch_http(_, rest_of_url: str) -> str:
+        if not rest_of_url.endswith("Updates.xml"):
+            return htmltext
+        folder = urlparse(rest_of_url).path.split("/")[-2]
+        assert folder.startswith("tools_")
+        return xmltext
+
+    monkeypatch.setattr(MetadataFactory, "fetch_http", _mock_fetch_http)
+
+    cli = Cli()
+    cli.run(["list-tool", host, target])
+    out, err = capsys.readouterr()
+    output_set = set(out.strip().split())
+    assert output_set == expected_tools
+
+    cli.run(["list-tool", host, target, tool_name])
+    out, err = capsys.readouterr()
+    output_set = set(out.strip().split())
+    assert output_set == expected_tool_modules
+
+    # Test abbreviated tool name: "aqt list-tool mac desktop ifw"
+    assert tool_name.startswith("tools_")
+    short_tool_name = tool_name[6:]
+    cli.run(["list-tool", host, target, short_tool_name])
+    out, err = capsys.readouterr()
+    output_set = set(out.strip().split())
+    assert output_set == expected_tool_modules
+
+    cli.run(["list-tool", host, target, tool_name, "-l"])
+    out, err = capsys.readouterr()
+
+    expected_tooldata = format(fetch_expected_tooldata(xml_expect))
+    assert out.strip() == expected_tooldata
+
+
 def test_fetch_http_ok(monkeypatch):
     monkeypatch.setattr("aqt.metadata.getUrl", lambda **kwargs: "some_html_content")
     assert MetadataFactory.fetch_http("some_url") == "some_html_content"