refactor(memmap)!: change behavioural flags to commands

This change factors out the following memory map tool flags into
independent commands:

- `--footprint` (becomes `memory footprint`)
- `--tree` (becomes `memory tree`)
- `--symbol` (becomes `memory symbol`)

So, for example, where previously you would generate the memory
footprint of a build with:

    memory --tree

You would now instead use:

    memory footprint

Any flags specific to a command (e.g. `--depth` for `tree`) must be
specified after the command, e.g.

    memory tree --depth 1

... instead of:

    memory --depth 1 tree

BREAKING-CHANGE: The image memory map visualization tool now uses
  commands, rather than arguments, to determine the behaviour of the
  script. See the commit message for further details.

Change-Id: I11d54d1f6276b8447bdfb8496544ab80399459ac
Signed-off-by: Chris Kay <chris.kay@arm.com>
diff --git a/Makefile b/Makefile
index 150aa30..de608fe 100644
--- a/Makefile
+++ b/Makefile
@@ -1852,7 +1852,7 @@
 
 memmap: all
 	$(if $(host-poetry),$(q)poetry -q install --no-root)
-	$(q)$(if $(host-poetry),poetry run )memory -sr ${BUILD_PLAT}
+	$(q)$(if $(host-poetry),poetry run )memory symbols --root ${BUILD_PLAT}
 
 tl: ${BUILD_PLAT}/tl.bin
 ${BUILD_PLAT}/tl.bin: ${HW_CONFIG}
diff --git a/docs/tools/memory-layout-tool.rst b/docs/tools/memory-layout-tool.rst
index d9c358d..2506d2f 100644
--- a/docs/tools/memory-layout-tool.rst
+++ b/docs/tools/memory-layout-tool.rst
@@ -41,7 +41,7 @@
 
 .. code:: shell
 
-    $ poetry run memory -s
+    $ poetry run memory symbols
     build-path: build/fvp/release
     Virtual Address Map:
                +------------__BL1_RAM_END__------------+---------------------------------------+
@@ -116,14 +116,14 @@
 Memory Footprint
 ~~~~~~~~~~~~~~~~
 
-The tool enables users to view static memory consumption. When the options
-``-f``, or ``--footprint`` are provided, the script analyses the ELF binaries in
-the build path to generate a table (per memory type), showing memory allocation
-and usage. This is the default output generated by the tool.
+The tool enables users to view static memory consumption. When the ``footprint``
+command is provided, the script analyses the ELF binaries in the build path to
+generate a table (per memory type), showing memory allocation and usage. This is
+the default output generated by the tool.
 
 .. code:: shell
 
-    $ poetry run memory -f
+    $ poetry run memory footprint
     build-path: build/fvp/release
     +----------------------------------------------------------------------------+
     |                         Memory Usage (bytes) [RAM]                         |
@@ -150,13 +150,13 @@
 Memory Tree
 ~~~~~~~~~~~
 
-A hierarchical view of the memory layout can be produced by passing the option
-``-t`` or ``--tree`` to the tool. This gives the start, end, and size of each
-module, their ELF segments as well as sections.
+A hierarchical view of the memory layout can be produced by passing the ``tree``
+command to the tool. This gives the start, end, and size of each module, their
+ELF segments as well as sections.
 
 .. code:: shell
 
-    $ poetry run memory -t
+    $ poetry run memory tree
     build-path: build/fvp/release
     name                                       start        end       size
     bl1                                            0    400c000    400c000
@@ -209,7 +209,7 @@
 
 .. code::
 
-    $ poetry run memory -t --depth 2
+    $ poetry run memory tree --depth 2
     build-path: build/fvp/release
     name                          start        end       size
     bl1                               0    400c000    400c000
diff --git a/tools/memory/src/memory/buildparser.py b/tools/memory/src/memory/buildparser.py
deleted file mode 100755
index 3c73ea4..0000000
--- a/tools/memory/src/memory/buildparser.py
+++ /dev/null
@@ -1,93 +0,0 @@
-#
-# Copyright (c) 2023-2025, Arm Limited. All rights reserved.
-#
-# SPDX-License-Identifier: BSD-3-Clause
-#
-
-import re
-from pathlib import Path
-from typing import (
-    Any,
-    Dict,
-    List,
-    Union,
-)
-
-from memory.elfparser import TfaElfParser
-from memory.image import Region
-from memory.mapparser import TfaMapParser
-
-
-class TfaBuildParser:
-    """A class for performing analysis on the memory layout of a TF-A build."""
-
-    def __init__(self, path: Path, map_backend: bool = False) -> None:
-        self._modules: Dict[str, Union[TfaElfParser, TfaMapParser]] = {}
-        self._path: Path = path
-        self.map_backend: bool = map_backend
-        self._parse_modules()
-
-    def __getitem__(self, module: str) -> Union[TfaElfParser, TfaMapParser]:
-        """Returns an TfaElfParser instance indexed by module."""
-        return self._modules[module]
-
-    def _parse_modules(self) -> None:
-        """Parse the build files using the selected backend."""
-        backend = TfaElfParser
-        files = list(self._path.glob("**/*.elf"))
-        io_perms = "rb"
-
-        if self.map_backend or not files:
-            backend = TfaMapParser
-            files = self._path.glob("**/*.map")
-            io_perms = "r"
-
-        for file in files:
-            module_name = file.name.split("/")[-1].split(".")[0]
-            with open(file, io_perms) as f:
-                self._modules[module_name] = backend(f)
-
-        if not self._modules:
-            raise FileNotFoundError(
-                f"failed to find files to analyse in path {self._path}!"
-            )
-
-    @property
-    def symbols(self) -> Dict[str, Dict[str, int]]:
-        return {k: v.symbols for k, v in self._modules.items()}
-
-    @staticmethod
-    def filter_symbols(
-        images: Dict[str, Dict[str, int]], regex: str
-    ) -> Dict[str, Dict[str, int]]:
-        """Returns a map of symbols to modules."""
-
-        return {
-            image: {
-                symbol: symbol_value
-                for symbol, symbol_value in symbols.items()
-                if re.match(regex, symbol)
-            }
-            for image, symbols in images.items()
-        }
-
-    def get_mem_usage_dict(self) -> Dict[str, Dict[str, Region]]:
-        """Returns map of memory usage per memory type for each module."""
-        return {k: v.footprint for k, v in self._modules.items()}
-
-    def get_mem_tree_as_dict(self) -> Dict[str, Dict[str, Any]]:
-        """Returns _tree of modules, segments and segments and their total
-        memory usage."""
-        return {
-            k: {
-                "name": k,
-                **v.get_mod_mem_usage_dict(),
-                "children": v.get_seg_map_as_dict(),
-            }
-            for k, v in self._modules.items()
-        }
-
-    @property
-    def module_names(self) -> List[str]:
-        """Returns sorted list of module names."""
-        return sorted(self._modules.keys())
diff --git a/tools/memory/src/memory/memmap.py b/tools/memory/src/memory/memmap.py
index 10087f2..fd24680 100755
--- a/tools/memory/src/memory/memmap.py
+++ b/tools/memory/src/memory/memmap.py
@@ -4,17 +4,28 @@
 # SPDX-License-Identifier: BSD-3-Clause
 #
 
+import re
 import shutil
+from dataclasses import dataclass
 from pathlib import Path
-from typing import Optional
+from typing import Any, Dict, List, Optional
 
 import click
 
-from memory.buildparser import TfaBuildParser
+from memory.elfparser import TfaElfParser
+from memory.image import Image
+from memory.mapparser import TfaMapParser
 from memory.printer import TfaPrettyPrinter
 
 
-@click.command()
+@dataclass
+class Context:
+    build_path: Optional[Path] = None
+    printer: Optional[TfaPrettyPrinter] = None
+
+
+@click.group()
+@click.pass_obj
 @click.option(
     "-r",
     "--root",
@@ -37,31 +48,6 @@
     type=click.Choice(["debug", "release"], case_sensitive=False),
 )
 @click.option(
-    "-f",
-    "--footprint",
-    is_flag=True,
-    show_default=True,
-    help="Generate a high level view of memory usage by memory types.",
-)
-@click.option(
-    "-t",
-    "--tree",
-    is_flag=True,
-    help="Generate a hierarchical view of the modules, segments and sections.",
-)
-@click.option(
-    "--depth",
-    default=3,
-    show_default=True,
-    help="Generate a virtual address map of important TF symbols.",
-)
-@click.option(
-    "-s",
-    "--symbols",
-    is_flag=True,
-    help="Generate a map of important TF symbols.",
-)
-@click.option(
     "-w",
     "--width",
     type=int,
@@ -74,48 +60,138 @@
     default=False,
     help="Display numbers in decimal base.",
 )
-@click.option(
-    "--no-elf-images",
-    is_flag=True,
-    help="Analyse the build's map files instead of ELF images.",
-)
-def main(
+def cli(
+    obj: Context,
     root: Optional[Path],
     platform: str,
     build_type: str,
-    footprint: bool,
-    tree: bool,
-    symbols: bool,
-    depth: int,
     width: int,
     d: bool,
-    no_elf_images: bool,
 ):
-    build_path: Path = root if root is not None else Path("build", platform, build_type)
-    click.echo(f"build-path: {build_path.resolve()}")
+    obj.build_path = root if root is not None else Path("build", platform, build_type)
+    click.echo(f"build-path: {obj.build_path.resolve()}")
+
+    obj.printer = TfaPrettyPrinter(columns=width, as_decimal=d)
+
+
+@cli.command()
+@click.pass_obj
+@click.option(
+    "--no-elf-images",
+    is_flag=True,
+    help="Analyse the build's map files instead of ELF images.",
+)
+def footprint(obj: Context, no_elf_images: bool):
+    """Generate a high level view of memory usage by memory types."""
+
+    assert obj.build_path is not None
+    assert obj.printer is not None
+
+    elf_image_paths: List[Path] = (
+        [] if no_elf_images else list(obj.build_path.glob("**/*.elf"))
+    )
 
-    parser: TfaBuildParser = TfaBuildParser(build_path, map_backend=no_elf_images)
-    printer: TfaPrettyPrinter = TfaPrettyPrinter(columns=width, as_decimal=d)
+    map_file_paths: List[Path] = (
+        [] if not no_elf_images else list(obj.build_path.glob("**/*.map"))
+    )
 
-    if footprint or not (tree or symbols):
-        printer.print_footprint(parser.get_mem_usage_dict())
+    images: Dict[str, Image] = dict()
 
-    if tree:
-        printer.print_mem_tree(
-            parser.get_mem_tree_as_dict(),
-            parser.module_names,
-            depth=depth,
-        )
+    for elf_image_path in elf_image_paths:
+        with open(elf_image_path, "rb") as elf_image_io:
+            images[elf_image_path.stem.upper()] = TfaElfParser(elf_image_io)
+
+    for map_file_path in map_file_paths:
+        with open(map_file_path, "r") as map_file_io:
+            images[map_file_path.stem.upper()] = TfaMapParser(map_file_io)
+
+    obj.printer.print_footprint({k: v.footprint for k, v in images.items()})
+
+
+@cli.command()
+@click.pass_obj
+@click.option(
+    "--depth",
+    default=3,
+    show_default=True,
+    help="Generate a virtual address map of important TF symbols.",
+)
+def tree(obj: Context, depth: int):
+    """Generate a hierarchical view of the modules, segments and sections."""
+
+    assert obj.build_path is not None
+    assert obj.printer is not None
+
+    paths: List[Path] = list(obj.build_path.glob("**/*.elf"))
+    images: Dict[str, TfaElfParser] = dict()
+
+    for path in paths:
+        with open(path, "rb") as io:
+            images[path.stem] = TfaElfParser(io)
+
+    mtree: Dict[str, Dict[str, Any]] = {
+        k: {
+            "name": k,
+            **v.get_mod_mem_usage_dict(),
+            **{"children": v.get_seg_map_as_dict()},
+        }
+        for k, v in images.items()
+    }
+
+    obj.printer.print_mem_tree(mtree, list(mtree.keys()), depth=depth)
+
+
+@cli.command()
+@click.pass_obj
+@click.option(
+    "--no-elf-images",
+    is_flag=True,
+    help="Analyse the build's map files instead of ELF images.",
+)
+def symbols(obj: Context, no_elf_images: bool):
+    """Generate a map of important TF symbols."""
+
+    assert obj.build_path is not None
+    assert obj.printer is not None
+
+    expr: str = (
+        r"(.*)(TEXT|BSS|RO|RODATA|STACKS|_OPS|PMF|XLAT|GOT|FCONF|RELA"
+        r"|R.M)(.*)(START|UNALIGNED|END)__$"
+    )
+
+    elf_image_paths: List[Path] = (
+        [] if no_elf_images else list(obj.build_path.glob("**/*.elf"))
+    )
+
+    map_file_paths: List[Path] = (
+        [] if not no_elf_images else list(obj.build_path.glob("**/*.map"))
+    )
+
+    images: Dict[str, Image] = dict()
+
+    for elf_image_path in elf_image_paths:
+        with open(elf_image_path, "rb") as elf_image_io:
+            images[elf_image_path.stem] = TfaElfParser(elf_image_io)
+
+    for map_file_path in map_file_paths:
+        with open(map_file_path, "r") as map_file_io:
+            images[map_file_path.stem] = TfaMapParser(map_file_io)
+
+    symbols = {k: v.symbols for k, v in images.items()}
+    symbols = {
+        image: {
+            symbol: symbol_value
+            for symbol, symbol_value in symbols.items()
+            if re.match(expr, symbol)
+        }
+        for image, symbols in symbols.items()
+    }
+
+    obj.printer.print_symbol_table(symbols, list(images.keys()))
+
 
-    if symbols:
-        expr: str = (
-            r"(.*)(TEXT|BSS|RO|RODATA|STACKS|_OPS|PMF|XLAT|GOT|FCONF|RELA"
-            r"|R.M)(.*)(START|UNALIGNED|END)__$"
-        )
-        printer.print_symbol_table(
-            parser.filter_symbols(parser.symbols, expr),
-            parser.module_names,
-        )
+def main():
+    cli(obj=Context())
 
 
 if __name__ == "__main__":
diff --git a/tools/memory/src/memory/printer.py b/tools/memory/src/memory/printer.py
index 1a8c1c3..6debf53 100755
--- a/tools/memory/src/memory/printer.py
+++ b/tools/memory/src/memory/printer.py
@@ -95,7 +95,7 @@
                     val = vals[mem]
                     table.add_row(
                         [
-                            mod.upper(),
+                            mod,
                             *self.format_args(
                                 *[
                                     val.start if val.start is not None else "?",