diff --git a/.gitignore b/.gitignore
index f75d8ba..2bbcf11 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,4 +1,4 @@
-.venv/
+.venv*/
__pycache__
/build/
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..c51dc59
--- /dev/null
+++ b/README.md
@@ -0,0 +1,192 @@
+# DTSh
+
+**DTSh** is a Devicetree source (DTS) file viewer with a shell-like command line interface:
+
+- browse a devicetree through a hierarchical file system metaphor
+- search for devices, bindings, buses or interrupts with flexible criteria
+- filter, sort and format commands output
+- generate simple documentation artifacts (text, HTML, SVG) by redirecting the output of commands to files
+- *rich* Textual User Interface, command line auto-completion, command history, user themes
+
+The considered use cases include:
+
+- to help getting started with Devicetree: hierarchically or semantically explore a devicetree, contextually access binding files or the Devicetree specification, save figures to illustrate notes
+- to have on hand a simple DTS file viewer: quickly check the enabled buses and connected devices or the RAM and Flash memory, get a first insight when debugging a Devicetree issue, document hardware configurations
+
+
+
+
+## Status
+
+This project started as a Proof of Concept for a simple tool that could assist newcomers to Zephyr in understanding what a devicetree is, and how bindings describe and constrain its content: source code and documentation for this prototype are still available as the [main](https://github.com/dottspina/dtsh/tree/main) branch of this repository.
+
+The PoC has since been rewritten and the new code base serves as a proposal to upstream DTSh as a West command: [RFC-DTSh, shell-like interface with Devicetree](https://github.com/zephyrproject-rtos/zephyr/pull/59863)
+
+The **default** branch (`dtsh-next`) now [mirrors](https://github.com/dottspina/zephyr/tree/rfc-dtsh/scripts/dts/dtsh) and [packages](https://pypi.org/project/dtsh/) this work to ensure that interested users won't install and test outdated software, or comment on obsolete issues.
+
+DTSh releases target versions of the Zephyr hardware model, refer to the table bellow:
+
+| DTSh | Zephyr | python-devicetree |
+|---------------------------------------------------------------------|-------------|------------------------------------------------------------------------|
+| [0.2rc1](https://github.com/dottspina/dtsh/releases/tag/v0.2.0-rc1) | 3.5.x | [403640b](https://github.com/zephyrproject-rtos/zephyr/commit/403640b) |
+| [0.2rc1](https://github.com/dottspina/dtsh/releases/tag/v0.2.0-rc1) | *3.6.0-rc1* | [403640b](https://github.com/zephyrproject-rtos/zephyr/commit/403640b) |
+
+**Latest stable release**: [0.2rc1](https://github.com/dottspina/dtsh/releases/tag/v0.2.0-rc1)
+
+
+## Getting Started
+
+The DTSh [User Guide](doc/ug/DTSh.pdf) contains extensive documentation and examples (just replace `west dtsh` with `dtsh`): bellow are simple instructions to, well, get started.
+
+
+### Requirements
+
+DTSh should install and run OOTB on GNU Linux (including WSL) and macOS with Python 3.8 to 3.11.
+
+DTSh will install the following requirements from PyPI:
+
+| Requirement | PyPI |
+|------------------------------------------------------|------------------------------------------------------|
+| PyYAML, YAML parser | [PyYAML](https://pypi.org/project/PyYAML/) |
+| Textualize's rich library for *beautiful formatting* | [rich](https://pypi.org/project/rich/) |
+| Stand-alone GNU readline module (macOS only) | [gnureadline](https://pypi.org/project/gnureadline/) |
+
+On **Windows**, the [GNU Readline](https://tiswww.cwru.edu/php/chet/readline/rltop.html) support will likely be disabled, resulting in a **degraded user experience**: no command line auto-completion nor command history.
+
+> ⚠ **python-devicetree**:
+>
+> DTSh relies on the [python-devicetree](https://github.com/zephyrproject-rtos/zephyr/tree/main/scripts/dts) library, part of Zephyr, to parse DTS and binding files into Devicetree models.
+>
+> Although this API should eventually become a *standalone source code library*, it's not currently a priority:
+> - it's not tagged independently of the main Zephyr project
+> - the [PyPI](https://pypi.org/project/devicetree/) package is no longer updated
+>
+> When distributed independently of Zephyr, DTSh has therefore to re-package snapshots ([src/devicetree](src/devicetree)) of this library (see [ci: bundle devicetree Python package ](https://github.com/dottspina/dtsh/commit/5e803eb) for details).
+>
+> As a consequence, it's very likely that you'll generate the DTS files (e.g. `west build`) and open them (e.g. `dtsh build/zephyr/zephyr.dts`) with different versions of the library: although compatibility is mostly determined by the bindings, this might prove confusing in certain circumstances.
+
+
+### Install
+
+DTSh can be installed in the same Python virtual environment as the West workspace you use for Zephyr development, or stand-alone in any Python environment.
+
+#### Stand-alone
+
+This method installs DTSh in a dedicated Python virtual environment: it's a bit less convenient, but recommended if you prefer to test DTSh without installing anything in a development environment you actually depend on.
+
+For example (Linux and macOS):
+
+``` sh
+# Initialize Python virtual environment.
+mkdir dtsh
+cd dtsh
+python -m venv .venv
+
+# Activate and update system tools.
+. .venv/bin/activate
+pip install --upgrade pip setuptools
+
+# Install DTSh from PyPI.
+pip install dtsh
+```
+
+To uninstall, just remove your test directory, e.g. `rm -r dtsh`.
+
+
+#### West Workspace
+
+This method installs DTSh in the same Python virtual environment as a Zephyr workspace.
+
+Assuming you've followed Zephyr [Getting Started Guide](https://docs.zephyrproject.org/latest/develop/getting_started/index.html), the workspace should look like this:
+
+```
+zephyrproject/
+├── .venv
+├── .west
+├── bootloader
+├── modules
+├── tools
+└── zephyr
+```
+
+Then:
+
+``` sh
+# Active the Python virtual environment if not already done.
+zephyrproject/.venv/bin/activate
+
+# Install DTSh from PyPI.
+pip install dtsh
+```
+
+Uninstall as usual: `pip uninstall dtsh`.
+
+**Note**: Installing DTSh in a Zephyr workspace does not make it available as a West command.
+
+
+### Run
+
+Once installed, DTSh should be available as `dtsh`:
+
+```
+$ dtsh -h
+usage: dtsh [-h] [-b DIR] [-u] [--preferences FILE] [--theme FILE] [DTS]
+
+shell-like interface with Devicetree
+
+options:
+ -h, --help show this help message and exit
+
+open a DTS file:
+ -b DIR, --bindings DIR
+ directory to search for binding files
+ DTS path to the DTS file
+
+user files:
+ -u, --user-files initialize per-user configuration files and exit
+ --preferences FILE load additional preferences file
+ --theme FILE load additional styles file
+```
+
+To open a DTS file, simply pass its path as the command argument:
+
+```
+$ dtsh build/zephyr/zephyr.dts
+dtsh (0.2rc1): Shell-like interface with Devicetree
+How to exit: q, or quit, or exit, or press Ctrl-D
+
+/
+❭ cd /soc/flash-controller@4001e000
+
+/soc/flash-controller@4001e000
+❭ tree -l
+ Description
+ ─────────────────────────────────────────────────────────────────
+flash-controller@4001e000 Nordic NVMC (Non-Volatile Memory Controller)
+└── flash@0 Flash node
+ └── partitions This binding is used to describe fixed partitions of a flash (or…
+ ├── partition@0 Each child node of the fixed-partitions node represents…
+ ├── partition@c000 Each child node of the fixed-partitions node represents…
+ ├── partition@82000 Each child node of the fixed-partitions node represents…
+ └── partition@f8000 Each child node of the fixed-partitions node represents…
+```
+
+A DTS file alone is actually an incomplete Devicetree source: interpreting its contents requires finding the defining bindings.
+
+By default, DTSh will fist try to retrieve the bindings Zephyr has used at build-time, when the DTS file was generated. For this, it will rely on the CMake cache file contents, assuming a typical build layout:
+
+```
+build/
+├── CMakeCache.txt
+└── zephyr/
+ └── zephyr.dts
+```
+
+When no suitable CMake cache is available, DTSh will instead try to work out the search path Zephyr would use if it were to generate the DTS *now* (see [Where bindings are located](https://docs.zephyrproject.org/latest/build/dts/bindings-intro.html#where-bindings-are-located)): a valid `ZEPHYR_BASE` is then required.
+
+If the command line does not specify a DTS file path, `dtsh` will try to open the devicetree at `build/zephy/zephyr.dts`. When DTSh is installed in a Zephyr workspace, opening the devicetree of a project you're working on is then as simple as:
+
+```
+$ west build
+$ dtsh
+```
diff --git a/README.org b/README.org
deleted file mode 100644
index bcaba2d..0000000
--- a/README.org
+++ /dev/null
@@ -1,836 +0,0 @@
-#+title: dtsh
-
-*dtsh* is an interactive /shell-like/ interface with a devicetree and its bindings:
-
-- browse the devicetree through a familiar hierarchical file-system metaphor
-- retrieve nodes and bindings with accustomed command names and command line syntax
-- generate simple documentation artifacts by redirecting commands output to files (text, HTML, SVG)
-- common command line interface paradigms (auto-completion, history) and keybindings
-
-It's said an ~ls /soc -l > soc.svg~ speaks a thousand words:
-
-[[./doc/img/soc.svg]]
-
-#+begin_quote
-DISCLAIMER: This software was created as a Proof of Concept for a simple tool
-that could assist newcomers to Zephyr in understanding what a devicetree is,
-and how bindings describe and constrain its content.
-
-It's still in its inception phase:
-
-- while the current feature set may already prove helpful to beginners,
- it might also quickly frustrate more knowledgeable users
-- possible bugs could instead disastrously confuse beginners
-- should install and run more or less Out Of The Box™ on most GNU/Linux distributions
- and BSD-like systems (e.g. macOS); will likely refuse to run on Windows due to missing readline support
-- should be compatible with any source file in DTS format, but requires that the bindings are consistently available as YAML files:
- unfortunately, this does NOT directly apply to the devicetree use by the Linux kernel
-
-All kinds of feedback and contribution are encouraged: please refer to the bottom CONTRIBUTE section.
-
-*See also the project's status*.
-#+end_quote
-
-- [[https://github.com/dottspina/dtsh#status][Status]]
-- [[https://github.com/dottspina/dtsh#get-started][Get started]]
- - [[https://github.com/dottspina/dtsh#requirements][Requirements]]
- - [[https://github.com/dottspina/dtsh#install][Install]]
- - [[https://github.com/dottspina/dtsh#run][Run]]
- - [[https://github.com/dottspina/dtsh#zephyr-integration][Zephyr integration]]
-- [[https://github.com/dottspina/dtsh#users-guide][User's guide]]
- - [[https://github.com/dottspina/dtsh#the-shell][The shell]]
- - [[https://github.com/dottspina/dtsh#file-system-metaphot][Fle system metaphor]]
- - [[https://github.com/dottspina/dtsh#the-command-string][The command string]]
- - [[https://github.com/dottspina/dtsh#output-redirection][Output redirection]]
- - [[https://github.com/dottspina/dtsh#built-ins][Built-ins]]
- - [[https://github.com/dottspina/dtsh#manual-pages][Manual pages]]
- - [[https://github.com/dottspina/dtsh#system-information][System information]]
- - [[https://github.com/dottspina/dtsh#find-nodes][Find nodes]]
- - [[https://github.com/dottspina/dtsh#format-strings][Format strings]]
- - [[https://github.com/dottspina/dtsh#user-interface][User interface]]
- - [[https://github.com/dottspina/dtsh#the-prompt][The prompt]]
- - [[https://github.com/dottspina/dtsh#commands-history][Commands history]]
- - [[https://github.com/dottspina/dtsh#auto-completion][Auto-completion]]
- - [[https://github.com/dottspina/dtsh#the-pager][The pager]]
- - [[https://github.com/dottspina/dtsh#external-links][External links]]
- - [[https://github.com/dottspina/dtsh#keybindings][Keybindings]]
- - [[https://github.com/dottspina/dtsh#theme][Theme]]
- - [[https://github.com/dottspina/dtsh#how-to][How To]]
- - [[https://github.com/dottspina/dtsh#soc-overview][SoC overview]]
- - [[https://github.com/dottspina/dtsh#board-definition][Board definition]]
- - [[https://github.com/dottspina/dtsh#compatibles-overview][Compatibles overview]]
- - [[https://github.com/dottspina/dtsh#bus-devices-overview][Bus devices overview]]
- - [[https://github.com/dottspina/dtsh#interrupts-overview][Interrupts overview]]
- - [[https://github.com/dottspina/dtsh#commands-cheat-sheet][Commands Cheat Sheet]]
-- [[https://github.com/dottspina/dtsh#contribute][Contribute]]
-- [[https://github.com/dottspina/dtsh#references][References]]
-
-* Status
-
-Latest stable release: [[https://github.com/dottspina/dtsh/releases/tag/v0.1.0b2][v0.1.0b2]] ([[https://pypi.org/project/dtsh/0.1.0b2/][PyPI]])
-
-*Latest Changes*:
-
-- [[https://github.com/dottspina/dtsh/commit/96ffa28][96ffa28]] update python-devicetree to Zephyr 3.5.0
-- [[https://github.com/dottspina/dtsh/commit/55b2b9e0faf537997bd7104cadb17d29708e4e5f][55b2b9e]] update python-devicetree to Zephyr 3.4.0
-
-*Note*:
-
-- this prototype is now /stale/: I may update it e.g. when Zephyr 3.6 is out, or to fix obvious bugs if asked to, but issues that would require more work (e.g. fixing the poorly wrapped output, or adding support for DTS labels in paths) won't be addressed
-- for some time now, all real work has moved to an upstream [[https://github.com/zephyrproject-rtos/zephyr/pull/59863][RFC]] (PR's [[https://github.com/dottspina/zephyr/tree/rfc-dtsh][origin]]) that may eventually add ~dtsh~ as a West extension to Zephyr: report feedback about this version preferably (see [[https://github.com/dottspina/dtsh#try-the-west-command-rfc][Try the West command RFC]])
-
-* Get started
-
-#+begin_example
-# Install dtsh in a dedicated Python virtual environment
-$ python -m venv .venv
-$ . .venv/bin/activate
-$ pip install --upgrade pip setuptools
-$ pip install --upgrade dtsh
-
-# Setting ZEPHYR_BASE will help dtsh in building a default bindings search path
-export ZEPHYR_BASE=/path/to/zephyrproject/zephyr
-
-# Open an existing DTS file, using Zephyr bindings
-$ dtsh /path/to/build/zephyr/zephyr.dts
-dtsh (0.1.0a6): Shell-like interface to a devicetree
-Help: man dtsh
-How to exit: q, or quit, or exit, or press Ctrl-D
-
-/
-❯ tree -L 1 -l
-/
-├── chosen
-├── aliases
-├── soc
-├── pin-controller The nRF pin controller is a singleton node responsible for controlling…
-├── entropy_bt_hci Bluetooth module that uses Zephyr's Bluetooth Host Controller Interface as…
-├── cpus
-├── sw-pwm nRFx S/W PWM
-├── leds This allows you to define a group of LEDs. Each LED in the group is…
-├── pwmleds PWM LEDs parent node
-├── buttons GPIO KEYS parent node
-├── connector GPIO pins exposed on Arduino Uno (R3) headers…
-└── analog-connector ADC channels exposed on Arduino Uno (R3) headers…
-#+end_example
-
-To get the /big picture/:
-
-- may be this [[https://youtu.be/pc2AMx1iPPE][short abrupt video]], that at least illustrates the main /shell metaphor/, the auto-completion behavior
- and the most useful keybindings
-- if already comfortable with Zephyr, try running the [[https://github.com/dottspina/dtsh#interactive-tests][interactive tests]] to explore ~dtsh~ with various configurations
- and DTS source files
-
-** Requirements
-
-*** Host tools and libraries
-
-**** POSIX
-
-This is an abusive keyword for facilities most POSIX-like operating systems provide one way or another:
-
-- the [[https://tiswww.cwru.edu/php/chet/readline/rltop.html][GNU readline]] library we rely upon for command line auto-completion, commands history,
- and standardized keybindings
-- an ANSI ([[https://www.ecma-international.org/publications-and-standards/standards/ecma-48/][ECMA-48]]) terminal emulator, preferably 256 colors support and a font that includes unicode glyphs
- for a few common symbols
-- a /pager/, preferably with ANSI escape codes support, e.g. [[https://www.greenwoodsoftware.com/less/faq.html][less]]
-
-Note: on BSD-like systems, where the Python ~readline~ package will likely depend on [[https://www.thrysoee.dk/editline/][editline]],
-~dtsh~ will try to install ~gnureadline~ ([[https://pypi.org/project/gnureadline/][PyPI]]) and to load the ~readline~ package implementation
-from there instead of from the standard Python library.
-
-**** CMake
-
-~dtsh~ may need to access a few CMake cached variables for setting sensible default values,
-e.g. when building the default bindings search path.
-
-*** Python requirements
-
-The minimal requirement is set to Python 3.8, with proper support for virtual environments, [[https://pip.pypa.io/en/stable/][pip]], and [[https://setuptools.pypa.io/en/latest/setuptools.html][setuptools]].
-
-Most ~dtsh~ software requirements are common Python libraries that will be installed as declared dependencies (e.g. by ~setup.py~):
-
-- « rich text and beautiful formatting in the terminal »: [[https://www.textualize.io/][Textualize]] /rich/ API ([[https://github.com/Textualize/rich][GitHub]], [[https://pypi.org/project/rich/][PyPI]])
-- syntax highlighting support: [[https://pygments.org/][Pygments]] ([[https://pypi.org/project/Pygments/][PyPI]])
-- [[https://pyyaml.org/][PyYAML]] ([[https://pypi.org/project/PyYAML][PyPI]])
-- DT sources and bindings /parser/, devicetree model: ~edtlib~, maintained as part of the Zephyr project ([[https://github.com/zephyrproject-rtos/python-devicetree][GitHub]], [[https://pypi.org/project/devicetree/][PyPI]]);
- see bellow
-
-**** edtlib
-
-The EDT library is no longer installed from PyPI, but bundled with ~dtsh~ (see [[https://github.com/dottspina/dtsh/commit/5e803ebdd3482db75dc752baa3cca6866750eff5][5e803eb]]).
-
-** Install
-
-It's recommended to install ~dtsh~ in a dedicated Python virtual environment.
-
-*** Python virtual environment
-
-A Python /best practice/ is to always install a consistent set of /scripts/ and their dependencies in a dedicated
-[[https://peps.python.org/pep-0405/][virtual environment]], with up-to-date ~pip~, ~setuptools~ and ~wheel~ packages.
-
-See also [[https://packaging.python.org/en/latest/guides/installing-using-pip-and-virtual-environments/][Installing packages using pip and virtual environments]].
-
-*** Install from sources
-
-Install from sources in a dedicated Python virtual environment:
-
-#+begin_src sh
-git clone https://github.com/dottspina/dtsh.git
-cd dtsh
-python -m venv .venv
-. .venv/bin/activate
-pip install --upgrade pip setuptools
-pip install .
-#+end_src
-
-*** Install from PyPI
-
-Install from [[https://pypi.org/project/dtsh/][PyPI]] in a dedicated Python virtual environment:
-
-#+begin_src sh
-python -m venv .venv
-. .venv/bin/activate
-pip install --upgrade pip setuptools
-pip install --upgrade dtsh
-#+end_src
-
-*** Uninstall
-
-To remove ~dtsh~ and all its direct dependencies from a dedicated virtual environment:
-
-#+begin_src sh
-cd dtsh
-. .venv/bin/activate
-pip uninstall dtsh rich Pygments
-#+end_src
-
-** Run
-
-To start a shell session: ~dtsh [] [*]~
-
-where:
-
-- ~~: path to the devicetree source file in [[https://devicetree-specification.readthedocs.io/en/latest/chapter6-source-language.html][DTS Format]] (~.dts~);
- if unspecified, defaults to ~$PWD/build/zephyr/zephyr.dts~
-- ~~: directory to search for [[https://yaml.org/][YAML]] binding files;
- if unspecified, but the environment variable ~ZEPHYR_BASE~ is set,
- defaults to the [[https://github.com/dottspina/dtsh#zephyr-bindings-search-path][Zephyr bindings search path]] bellow
-
-To open an arbitrary DTS file with custom bindings:
-
-#+begin_example
-$ dtsh /path/to/foobar.dts /path/to/custom/bindings /path/to/other/custom/bindings
-#+end_example
-
-To open the same DTS file, with /default/ bindings:
-
-#+begin_example
-$ export ZEPHYR_BASE=/path/to/zephyr
-$ dtsh /path/to/foobar.dts
-#+end_example
-
-On startup, ~dtsh~ will output a banner, followed by the first prompt:
-
-#+begin_example
-dtsh (0.1.0a5): Shell-like interface to a devicetree
-Help: man dtsh
-How to exit: q, or quit, or exit, or press Ctrl-D
-
-/
-❯
-#+end_example
-
-*** Zephyr bindings search path
-
-When no bindings are explicitly provided, ~dtsh~ will try to reassemble the /bindings search path/ Zephyr would rely on at build time (see [[https://docs.zephyrproject.org/latest/build/dts/bindings.html#where-bindings-are-located][Where bindings are located]]):
-
-- the zephyr repository: ~$ZEPHYR_BASE/dts/bindings~
-- the application source directory: ~APPLICATION_SOURCE_DIR/dts/bindings~; if ~dtsh~ fails to access the CMake
- variable ~APPLICATION_SOURCE_DIR~, will fallback to ~$PWD/dts/bindings~ (assuming the current directory is
- the /project/ directory)
-- the board directory: ~BOARD_DIR/dts/bindings~; if ~dtsh~ fails to access the CMake variable ~BOARD_DIR~, will
- fallback to ~$ZEPHYR_BASE/boards~ (to include /all/ Zephyr defined boards) plus ~$PWD/boards~ (to include a possible
- custom boards directory)
-- any directories in ~DTS_ROOT~: all ~DTS_ROOT/**/dts/bindings~ directories ~dtsh~ will find if the CMake variable
- ~DTS_ROOT~ is available
-- any module that defines a ~dts_root~ in its build: ~dtsh~ does NOT honor this part of the search path,
- and likely will not until a test case is submitted for investigation
-
-Only the ~ZEPHYR_BASE~ environment variable is required, and will typically suffice to setup an
-appropriate bindings search path.
-
-See also issue [[https://github.com/dottspina/dtsh/issues/1#issuecomment-1278281428][Incomplete Zephyr bindings #1]].
-
-** Zephyr integration
-
-Installing ~dtsh~ into the same Python virtual environment as the ~west~ development environment
-is now strongly discouraged (and should never have been advised to).
-
-Recommended use:
-
-- install Zephyr and generate DTS files with ~west build~ as usual
-- install ~dtsh~ into its own Python virtual environment,
- optionally set ~ZEPHYR_BASE~ to get most DT bindings for free,
- and open DTS files from there
-
-* User's guide
-
-The preferred entry point to the ~dtsh~ documentation should be its manual pages:
-
-- ~man dtsh~: open the shell manual page (mostly similar to this user guide)
-- ~man ~: open the manual page for the command ~~
-
-** The shell
-
-~dtsh~ defines a set of /built-in/ commands that interface with a devicetree and its bindings through a hierarchical file-system metaphor.
-
-Loading of /external commands/ is not (yet) supported.
-
-*** File system metaphor
-
-Within a ~dtsh~ session, a devicetree shows itself as a familiar hierarchical file-system,
-where [[https://devicetree-specification.readthedocs.io/en/stable/devicetree-basics.html#path-names][path names]] /look like/ paths to files or directories, depending on the acting shell command.
-
-A current /working node/ is defined, similar to any shell's current working directory,
-allowing ~dtsh~ to also support relative paths.
-
-A leading ~.~ represents the current working node, and ~..~ its parent.
-The devicetree root node is its own parent.
-
-To designate properties, ~dtsh~ uses ~$~ as a separator between DT path names and [[https://devicetree-specification.readthedocs.io/en/stable/devicetree-basics.html#property-names][property names]]
-(should be safe since ~$~ is an invalid character for both node and property names).
-
-Some commands support filtering or /globbing/ with trailing wild-cards ~*~.
-
-*** The command string
-
-The ~dtsh~ command string is based on the [[https://www.gnu.org/software/libc/manual/html_node/Using-Getopt.html][GNU getopt]] syntax.
-
-**** Synopsis
-
-All built-ins share the same synopsis:
-
-#+begin_example
-CMD [OPTIONS] [PARAMS]
-#+end_example
-
-where:
-
-- ~CMD~: the built-in name, e.g. ~ls~
-- ~OPTIONS~: the options the command is invoked with, e.g. ~-l~
-- ~PARAMS~: the parameters the command is invoked for, e.g. a path name
-
-~OPTIONS~ and ~PARAMS~ are not positional: ~ls -l /soc~ is equivalent to ~ls /soc -l~.
-
-**** Options
-
-An option may support:
-
-- a short name, starting with a single ~-~ (e.g. ~-h~)
-- a long name, starting with ~--~ (e.g. ~--help~)
-
-Short option names can combine: ~-lR~ is equivalent to ~-l -R~.
-
-An Option may also require an argument, e.g. ~find /soc --interrupt 12~.
-
-Options semantic should be consistent across commands, e.g. ~-l~ always means /long format/.
-
-We also try to re-use /well-known/ option names, e.g. ~-r~ for /reverse sort/ or ~-R~ for /recursive/.
-
-ℹ Trigger ~TAB~ completion after a single ~-~ to /pull/ a summary of a command's options, e.g:
-
-#+begin_example
-❯ find -[TAB][TAB]
--c print nodes count
--q quiet, only print nodes count
--l use rich listing format
--f visible columns format string
--h --help print usage summary
---name find by name
---compat find by compatible
---bus find by bus device
---interrupt find by interrupt
---enabled-only search only enabled nodes
---pager page command output
-❯ find -
-#+end_example
-
-*** Output redirection
-
-Command output redirection uses the well-known syntax:
-
-#+begin_example
-CMD [OPTIONS] [PARAMS] > PATH
-#+end_example
-
-where ~PATH~ is the absolute or relative path to the file the command output will be redirected to.
-
-Depending on the extension, the command output may be saved as an HTML page (~.html~), an SVG image (~.svg~),
-or a text file (default).
-
-For example:
-
-#+begin_example
-/
-❯ ls -l soc > soc.html
-
-#+end_example
-
-*** Built-ins
-
-| Built-in | |
-|----------+-------------------------------------------|
-| ~alias~ | print defined aliases |
-| ~chosen~ | print chosen configuration |
-| ~pwd~ | print current working node's path |
-| ~cd~ | change current working node |
-| ~ls~ | list devicetree nodes |
-| ~tree~ | list devicetree nodes in tree-like format |
-| ~cat~ | concatenate and print devicetree content |
-| ~find~ | find devicetree nodes |
-| ~uname~ | print system information |
-| ~man~ | open a manual page |
-
-*** Manual pages
-
-As expected, the ~man~ command will open the manual page for the shell itself (~man dtsh~),
-or one of its built-ins (e.g. ~man ls~).
-
-Additionally, ~man~ can also open a manual page for a [[https://devicetree-specification.readthedocs.io/en/latest/chapter2-devicetree-basics.html#compatible][compatible]], which is essentially a view of its (YAML) bindings: e.g. ~man --compat nordic,nrf-radio~
-
-~man~ should eventually also serve as an entry point to external useful or normative documents,
-e.g. the Devicetree Specifications or the Zephyr project's documentation.
-
-*** System information
-
-*dtsh* may also expose /system/ information, including:
-
-- the Zephyr kernel version, e.g. ~zephyr-3.1.0~, with a link to the corresponding
- release notes when available
-- board information, based on the content of its YAML binding file,
- with a link to the corresponding documentation when the board
- is [[https://docs.zephyrproject.org/latest/boards/index.html][supported by Zephyr]]
-- the configured /toolchain/, either Zephyr SDK or GNU Arm Embedded
-
-Retrieving this information may involve environment variables (e.g. ~ZEPHYR_BASE~),
-CMake cached variables (e.g. ~BOARD_DIR~), and ~git~ or ~GCC~.
-
-Refer to ~man uname~ for details.
-
-*** Find nodes
-
-The ~find~ command permits to search the devicetree by:
-
-- node names
-- compatible strings
-- bus devices
-- interrupt names or numbers
-
-For example, the command line bellow would list all enabled bus devices that generate IRQs :
-
-#+begin_example
-❯ find --enabled-only --bus * --interrupt *
-#+end_example
-
-~find~ is quite versatile and supports a handful of options. Refer to its extensive manual page (~man find~).
-
-*** Format strings
-
-When the ~-l~ flag is set (aka /use long listing format/), the ~ls~ and ~find~ commands accept
-an additional ~-f~ option, the /format string/, that permits to choose the visible node fields.
-
-A format string is a list of specifier characters, each selecting a node field.
-
- | Specifier | Format |
- |-----------+-------------------------------------------|
- | ~N~ | The node name |
- | ~a~ | The unit-address |
- | ~n~ | The node name with the address striped |
- | ~d~ | The description from the node binding |
- | ~p~ | The node path name |
- | ~l~ | The node 'label' property |
- | ~L~ | All known labels for the node |
- | ~s~ | The node 'status' property |
- | ~c~ | The 'compatible' property for the node |
- | ~C~ | The node binding (aka matched compatible) |
- | ~A~ | The node aliases |
- | ~b~ | The bus device information for the node |
- | ~r~ | The node 'reg' property |
- | ~i~ | The interrupts generated by the node |
-
-For example, the format string ~naib~ will refer to the node's name, address,
-IRQs and bus information columns.
-
-** User interface
-
-The ~dtsh~ command line interface paradigms and keybindings should sound familiar.
-
-*** The prompt
-
-The default shell prompt is ❯.
-The line immediately above the prompt shows the current working node's path.
-
-#+begin_example
-/
-❯ pwd
-/
-
-/
-❯ cd /soc/i2c@40003000/bme680@76
-
-/soc/i2c@40003000/bme680@76
-❯ pwd
-/soc/i2c@40003000/bme680@76
-
-#+end_example
-
-Pressing ~C-d~ (aka ~CTRL-D~) at the prompt will exit the ~dtsh~ session.
-
-*** Commands history
-
-Commands history is provided through GNU readline integration.
-
-At the shell prompt, press:
-
-- up arrow (↑) to navigate the commands history backward
-- down arrow (↓) to navigate the commands history forward
-- ~C-r~ (aka ~CTRL-R~) to /reverse search/ the commands history
-
-The history file (typically ~$HOME/.config/dtsh/history~) is saved on exit, and loaded on startup.
-
-*** Auto-completion
-
-Command line auto-completion is provided through GNU readline integration.
-
-Auto-completion is triggered by first pressing the ~TAB~ key twice,
-then once for subsequent completions of the same command line, and may apply to:
-
-- command names (aka built-ins)
-- command options
-- command parameters such as node paths or compatibles
-
-*** The pager
-
-Built-ins that may produce large outputs support the ~--pager~ option: the command's output is then
-/paged/ using the system pager, typically ~less~:
-
-- use up (↑) and down (↓) arrows to navigate line by line
-- use page up (⇑) and down (⇓) to navigate /window/ by /window/
-- press ~g~ go to first line
-- press ~G~ go to last line
-- press ~/~ to enter search mode
-- press ~h~ for help
-- press ~q~ to quit the pager and return to the ~dtsh~ prompt
-
-On the contrary, the ~man~ command uses the pager by default and defines a ~--no-pager~ option to disable it.
-
-*** External links
-
-~dtsh~ commands output may contain links to external documents such as:
-
-- the local YAML binding files, that should open in the system's default text editor
-- the Devicetree specifications or the Zephyr project's documentation,
- that should open in the system's default web browser
-
-How these links will appear in the console, and whether they are /actionable/ or not,
-eventually depend on the terminal and the desktop environment.
-
-⚠ In particular, the environment may assume DTS files are DTS audio streams
-(e.g. the VLC media player could have registered itself for handling the ~.dts~ file extension).
-In this case, the external link won't open in the default text editor,
-possibly without any error message.
-A work-around is to configure the desktop environment to open DTS files with
-a text editor (e.g. with the /Open with/ paradigm).
-
-*** Keybindings
-
-Familiar keybindings are provided through GNU readline integration.
-
-| Keyboard shortcut | |
-|-------------------+----------------------------------------------|
-| ~C-l~ | clear terminal screen |
-| ~C-a~ | move cursor to beginning of command line |
-| ~C-e~ | move cursor to end of command line |
-| ~C-k~ | /kill/ text from cursor to end of command line |
-| ~M-d~ | /kill/ word at cursor |
-| ~C-y~ | /yank/ (paste) the content of the /kill buffer/ |
-| ~C-←~ | move cursor one word backward |
-| ~C-→~ | move cursor one word forward |
-| ~↑~ | navigate the commands history backward |
-| ~↓~ | navigate the commands history forward |
-| ~C-r~ | search the commands history |
-| ~TAB~ | trigger auto-completion |
-
-where:
-
-- e.g. ~C-c~ means hold the ~CTRL~ key, then press ~C~
-- e.g. ~M-d~ means hold the ~Alt~ (/meta/) key, then press ~D~
-
-*** Theme
-
-Colors and such are subjective, and most importantly the rendering will
-eventually depend on the terminal's font and palette,
-possibly resulting in severe accessibility issues, e.g. grey text on white background
-or a weird shell prompt.
-
-In such situations, or to accommodate personal preferences, users can try to override
-~dtsh~ colors (and prompt) by creating a /theme/ file (typically ~$HOME/.config/dtsh/theme~).
-
-Use the [[https://github.com/dottspina/dtsh/blob/main/src/dtsh/theme][default theme]] as template:
-
-#+begin_src sh
-cp src/dtsh/theme ~/.config/dtsh/theme
-#+end_src
-
-** How To
-*** SoC overview
-
-Try ~ls -lR --pager /soc~
-
-*** Board definition
-
-Try ~uname -ml~
-
-*** Compatibles overview
-
-Try ~find / --compat * -l~ to list all nodes that have a ~compatible~ DT property.
-
-ℹ See also the ~TAB~ completion for the ~man --compat~ command.
-
-*** Bus devices overview
-
-Try ~find / --bus * -f pibcd~
-
-Use the ~--enabled-only~ flag to filter out disabled bus devices.
-
-*** Interrupts overview
-
-Try ~find / --interrupt * -f picd~
-
-Use the ~--enabled-only~ flag to filter out disabled IRQs.
-
-*** Commands Cheat Sheet
-
-To list all commands and their short descriptions (press ~TAB~ twice at the prompt):
-
-#+begin_example
-/
-❯[TAB][TAB]
-pwd print current working node's path
-alias print defined aliases
-chosen print chosen configuration
-cd change current working node
-ls list devicetree nodes
-tree list devicetree nodes in tree-like format
-cat concatenate and print devicetree content
-uname print system information
-find find devicetree nodes
-man open a manual page
-#+end_example
-
-Command options list:
-
-#+begin_example
-/
-❯ ls -h
-ls [-d] [-l] [-r] [-R] [--pager] [-h --help] [PATH]
-#+end_example
-
-Command options summary (press ~TAB~ twice after the ~-~ character that starts
-option names):
-
-#+begin_example
-/
-❯ ls -[TAB][TAB]
--d list node itself, not its content
--l use rich listing format
--r reverse order while sorting
--R list node contents recursively
--h --help print usage summary
---pager page command output
-#+end_example
-
-Command manual page: ~man ls~
-
-* Contribute
-
-** Open issues
-
-All kinds of feedback and contribution are encouraged: open an [[https://github.com/dottspina/dtsh/issues/new][issue]] or a [[https://github.com/dottspina/dtsh/pulls][pull request]] with the appropriate [[https://github.com/dottspina/dtsh/issues/labels][label]]
-(if unsure, just ignore labels).
-
-| Label | |
-|-------+--------------------------------------------------------|
-| ~RFC~ | Participate in Request For Comments |
-| ~bug~ | The software does not behave as expected or documented |
-
-*** Request For Comments
-
-This project is still exploring /what could be/:
-
-- an educational tool that would assist students and professors when introducing /devicetrees/
-- an handy debug or discovery tool that would at a glance show how a /board/ is configured,
- which buses and devices are supported and if they are enabled, the memory layout for mapped peripherals and suchlike
-
-To provide feedback regarding theses topics, please open issues with the ~RFC~ label.
-
-*** Report bugs
-
-Bugs are expected, please open issues with the ~bug~ label.
-
-*** Getting Help
-
-Feel free to also open issues:
-
-- when the documentation is lacking, confusing or incorrect
-- to request any kind of help or support
-
-** Hacking dtsh
-
-Hack into ~dtsh~ and contribute [[https://github.com/dottspina/dtsh/pulls][pull requests]] (bug fix, features, documentation, code review).
-
-*** Development mode installation
-
-Install ~dtsh~ in development mode:
-
-#+begin_src sh
-git clone https://github.com/dottspina/dtsh.git
-cd dtsh
-python -m venv .venv
-. .venv/bin/activate
-pip install --upgrade pip setuptools
-pip install -r requirements-dev.txt
-pip install --editable .
-#+end_src
-
-The ~--editable~ option asks ~pip~ to install ~dtsh~ as an editable /working copy/.
-
-*** Unit tests
-
-To run a few unit tests:
-
-#+begin_src sh
-cd dtsh
-. .venv/bin/activate
-python -m pytest tests
-#+end_src
-
-*** Interactive tests
-
-The [[https://github.com/dottspina/dtsh/tree/main/etc/sh][etc/sh]] folder contains a few helper scrips that, while not originally written
-with a public use in mind, may prove helpful in hacking through ~dtsh~.
-
-In particular ~interactive-tests.sh~, that will sequentially run ~dtsh~
-for various boards and configurations:
-
-#+begin_example
-==== UC7: DTS from Zephyr build, Zephyr bindings
- Bindings search path: $ZEPHYR_BASE/dts/bindings
- Toolchain (dtsh): Zephyr SDK
- Application: coap_client
- Board: mimxrt1170_evk_cm7
-Run test [yN]:
-#+end_example
-
-The synopsis is:
-
-#+begin_example
-etc/sh/interactive-tests.sh [ZEPHYR_BASE TOOLCHAIN_BASE]
-#+end_example
-
-Where:
-
-- ~ZEPHYR_BASE~ would be a valid value for the environment variable ~ZEPHYR_BASE~ (sic)
-- ~TOOLCHAIN_BASE~ would be a valid value for ~ZEPHYR_SDK_INSTALL_DIR~ or
- ~$GNUARMEMB_TOOLCHAIN_PATH~ (the script /should/ auto-detect the toolchain variant
- and set ~ZEPHYR_TOOLCHAIN_VARIANT~ accordingly)
-
-When started without parameters, ~interactive-tests.sh~ will default to hard-coded values
-that match the test platform file-system, and won't make sense anywhere else.
-They are easy to change, though.
-
-WARNING:
-
-- tests ~UC3~ to ~UC9~ will install (uninstall) ~dtsh~ into (from) the Python environment of
- the West workspace parent of ~ZEPHYR_BASE~
-- tests ~UC8~ and ~UC9~ are expected to fail if GCC Arm 10 and 11 are not installed at the
- locations determined by the above hard-coded values
-
-*** Notes
-
-While probably not so /pythonesque/, the source code should eventually seem obvious,
-and friendly to hacking and prototyping.
-
-For example, to define a new built-in:
-
-- look for the ~DtshCommand~ and ~DtshCommandOption~ classes ([[https://github.com/dottspina/dtsh/blob/main/src/dtsh/dtsh.py][dtsh.dtsh]] module) to get the basics
-- copy an existing command (e.g. [[https://github.com/dottspina/dtsh/blob/main/src/dtsh/builtin_ls.py][ls]]) as a template, and customize it
-- re-use or improve helpers and views in the [[https://github.com/dottspina/dtsh/blob/main/src/dtsh/tui.py][dtsh.tui]] module to assemble the command output
- (see also the /rich/ [[https://rich.readthedocs.io/en/stable/console.html][Console API]])
-- when ready, register it in the ~dtsh.shell.DevicetreeShell~ constructor
-
-** Try the West command RFC
-
-To test the current status of ~west dtsh~, initialize a West workspace with the Zephyr fork that hosts the RFC/PR:
-
-#+begin_src shell
-
-# Initialize virtual environment as usual.
-mkdir tmp-west-dtsh
-cd tmp-west-dtsh
-python -m venv .venv
-. .venv/bin/activate
-pip install -U pip setuptools
-
-# Install West as usual.
-pip install -U west
-
-# Initialize a West workspace with the Zephyr fork that hosts the RFC/PR.
-west init -m https://github.com/dottspina/zephyr
-cd zephyr
-git fetch --all
-git checkout rfc-dtsh
-west update
-pip install -U -r scripts/requirements.txt
-
-# Configure workspace as usual, e.g.:
-west config --local build.pristine auto
-west config --local build.cmake-args -- -DCMAKE_EXPORT_COMPILE_COMMANDS=ON
-west config --local build.generator "Unix Makefiles"
-west config --local build.board nrf52840dk_nrf52840
-
-# Setup build environment.
-. ./zephyr-env.sh
-export ZEPHYR_TOOLCHAIN_VARIANT=zephyr
-export ZEPHYR_SDK_INSTALL_DIR=/mnt/platform/devemb/zephyr-rtos/sdk/zephyr-sdk-0.16.1
-
-# Build some sample and open the generated DTS with the devicetree shell.
-cd samples/sensor/bme680
-west build
-west dtsh
-#+end_src
-
-* References
-
-More or less introductory references about /devicetrees/.
-
-** Devicetree Specifications
-
-- [[https://devicetree-specification.readthedocs.io/en/latest/][Online Devicetree Specifications]] (latest)
-- [[https://devicetree-specification.readthedocs.io/en/stable/][Online Devicetree Specifications]] (stable)
-
-** Zephyr
-
-- [[https://docs.zephyrproject.org/latest/build/dts/intro.html][Introduction to devicetree]]
-- [[https://docs.zephyrproject.org/latest/build/dts/bindings.html][Devicetree bindings]]
-- [[https://docs.zephyrproject.org/latest/build/dts/api/bindings.html][Bindings index]]
-- [[https://docs.zephyrproject.org/latest/build/dts/api/api.html#zephyr-specific-chosen-nodes][Zephyr-specific chosen nodes]]
-- [[https://docs.zephyrproject.org/latest/build/dts/dt-vs-kconfig.html][Devicetree versus Kconfig]]
-
-** Linux
-
-- [[https://docs.kernel.org/devicetree/index.html][Open Firmware and Devicetree]]
-- [[https://elinux.org/Device_Tree_Usage][Device Tree Usage]]
-- [[https://elinux.org/Device_Tree_Reference][Device Tree Reference]]
-- [[https://elinux.org/Device_Tree_What_It_Is][Device Tree What It Is]]
diff --git a/README.rst b/README.rst
index daf7a80..ee263ef 100644
--- a/README.rst
+++ b/README.rst
@@ -1,47 +1,35 @@
====
-dtsh
+DTSh
====
-:Author: Chris Duf
+:Author: Christophe Dufaza
-**dtsh** is an interactive *shell-like* interface with a devicetree and
-its bindings:
+Shell-like command line interface with Devicetree:
-- browse the devicetree through a familiar hierarchical file-system
- metaphor
-- retrieve nodes and bindings with accustomed command names and command
- line syntax
-- generate simple documentation artifacts by redirecting commands
- output to files (text, HTML, SVG)
-- common command line interface paradigms (auto-completion, history)
- and keybindings
+- browse a devicetree through a hierarchical file system metaphor
+- search for devices, bindings, buses or interrupts with flexible criteria
+- filter, sort and format commands output
+- generate simple documentation artifacts (text, HTML, SVG) by redirecting the output
+ of commands to files
+- *rich* Textual User Interface, command line auto-completion, command history, user themes
::
- $ dtsh build/gzephyr/zephyr.dts
- dtsh (0.1.0a4): Shell-like interface to a devicetree
- Help: man dtsh
+ $ dtsh build/zephyr/zephyr.dts
+ dtsh (0.2rc1): Shell-like interface with Devicetree
How to exit: q, or quit, or exit, or press Ctrl-D
/
- > tree -L 1 -l
- /
- ├── chosen
- ├── aliases
- ├── soc
- ├── pin-controller The nRF pin controller is a singleton node responsible for controlling…
- ├── entropy_bt_hci Bluetooth module that uses Zephyr's Bluetooth Host Controller Interface as…
- ├── cpus
- ├── sw-pwm nRFx S/W PWM
- ├── leds This allows you to define a group of LEDs. Each LED in the group is…
- ├── pwmleds PWM LEDs parent node
- ├── buttons GPIO KEYS parent node
- ├── connector GPIO pins exposed on Arduino Uno (R3) headers…
- └── analog-connector ADC channels exposed on Arduino Uno (R3) headers…
-
-
-This software was created as a Proof of Concept for a:
-
-- simple tool that could assist newcomers to Zephyr in understanding
- what a devicetree is, and how bindings describe and constrain its content
-- an on hand DTS file viewer
+ > cd /soc/flash-controller@4001e000
+
+ /soc/flash-controller@4001e000
+ > tree -l
+ Description
+ ─────────────────────────────────────────────────────────────────
+ flash-controller@4001e000 Nordic NVMC (Non-Volatile Memory Controller)
+ └── flash@0 Flash node
+ └── partitions This binding is used to describe fixed partitions of a flash (or…
+ ├── partition@0 Each child node of the fixed-partitions node represents…
+ ├── partition@c000 Each child node of the fixed-partitions node represents…
+ ├── partition@82000 Each child node of the fixed-partitions node represents…
+ └── partition@f8000 Each child node of the fixed-partitions node represents…
diff --git a/doc/img/buses.png b/doc/img/buses.png
new file mode 100644
index 0000000..0b2261e
Binary files /dev/null and b/doc/img/buses.png differ
diff --git a/doc/img/devicetree-logo.png b/doc/img/devicetree-logo.png
deleted file mode 100644
index 1de6af5..0000000
Binary files a/doc/img/devicetree-logo.png and /dev/null differ
diff --git a/doc/img/devicetree-logo.svg b/doc/img/devicetree-logo.svg
deleted file mode 100644
index a249734..0000000
--- a/doc/img/devicetree-logo.svg
+++ /dev/null
@@ -1,14 +0,0 @@
-
\ No newline at end of file
diff --git a/doc/img/dtsh_home.png b/doc/img/dtsh_home.png
deleted file mode 100644
index 7434ffc..0000000
Binary files a/doc/img/dtsh_home.png and /dev/null differ
diff --git a/doc/img/soc.svg b/doc/img/soc.svg
deleted file mode 100644
index c37f670..0000000
--- a/doc/img/soc.svg
+++ /dev/null
@@ -1,290 +0,0 @@
-
diff --git a/doc/ug/DTSh.pdf b/doc/ug/DTSh.pdf
new file mode 100644
index 0000000..11d95ec
Binary files /dev/null and b/doc/ug/DTSh.pdf differ
diff --git a/etc/sh/interactive-tests.sh b/etc/sh/interactive-tests.sh
deleted file mode 100755
index 255c1a5..0000000
--- a/etc/sh/interactive-tests.sh
+++ /dev/null
@@ -1,428 +0,0 @@
-# Copyright (c) 2022 Chris Duf
-#
-# SPDX-License-Identifier: Apache-2.0
-
-# Helper for starting dtsh with various test configurations.
-
-thisfile=$(readlink -f "$0")
-thisdir=$(dirname "$thisfile")
-DTSH_HOME=$(readlink -f "$thisdir/../..")
-unset thisfile
-unset thisdir
-
-arg_zephyr_base="$1"
-arg_zephyr_sdk="$2"
-
-if [ -n "$arg_zephyr_base" ]; then
- TEST_ZEPHYR_BASE="$arg_zephyr_base"
- TEST_WEST_BASE=$(realpath -m "$arg_zephyr_base/..")
-else
- TEST_WEST_BASE=$(realpath -m /mnt/platform/embedded/zephyr-rtos/workspaces/zephyr-sandbox)
- TEST_ZEPHYR_BASE="$TEST_WEST_BASE/zephyr"
-fi
-if [ -n "$arg_zephyr_sdk" ]; then
- TEST_ZEPHYR_SDK_DIR=$(realpath -m "$arg_zephyr_sdk")
-else
- TEST_ZEPHYR_SDK_DIR=$(realpath -m /mnt/platform/embedded/zephyr-rtos/sdk/zephyr-sdk-0.15)
-fi
-unset arg_zephyr_base
-unset arg_zephyr_sdk
-
-
-TEST_GCCARM10_DIR=$(realpath -m /mnt/platform/embedded/arm-gnu/gcc-arm-none-eabi-10)
-TEST_GCCARM11_DIR=$(realpath -m /mnt/platform/embedded/arm-gnu/gcc-arm-none-eabi-11)
-
-TEST_DIR="$DTSH_HOME/tmp-tests"
-
-TEST_DTS_EDTLIB="$DTSH_HOME/tests/test.dts"
-TEST_BINDINGS_EDTLIB="$DTSH_HOME/tests/bindings"
-
-TEST_PROJECT_SENSOR="$TEST_ZEPHYR_BASE/samples/sensor/bme680"
-TEST_PROJECT_CAN="$TEST_ZEPHYR_BASE/samples/drivers/can/counter"
-TEST_PROJECT_COAP="$TEST_ZEPHYR_BASE/samples/net/sockets/coap_client"
-TEST_PROJECT_USB="$TEST_ZEPHYR_BASE/samples/subsys/usb/testusb"
-TEST_PROJECT_BLE="$TEST_ZEPHYR_BASE/samples/bluetooth/eddystone"
-
-TEST_BOARD_NRF52='nrf52840dk_nrf52840'
-TEST_BOARD_F407GZ='black_f407zg_pro'
-#FIXME: Link to documentation is broken for mimxrt1170_evk_cm7,
-# which has the same documentation page as mimxrt1170_evk.
-TEST_BOARD_NXP='mimxrt1170_evk_cm7'
-TEST_BOARD_NANO='arduino_nano_33_ble'
-
-
-. "$DTSH_HOME/etc/sh/zef"
-. "$DTSH_HOME/etc/sh/dtsh"
-
-
-test_run_yn() {
- echo -n 'Run test [yN]: '
- read yes_no
- case "$yes_no" in
- y|Y)
- return 1
- ;;
- *)
- ;;
- esac
- return 0
-}
-
-
-test_build_zephyr_project() {
- echo '==== Build Zephyr project'
- local arg_project="$1"
- local arg_board="$2"
- if [ -n "$arg_project" ]; then
- arg_project=$(realpath -m "$arg_project")
- else
- echo 'What project ?'
- zef_abort
- fi
- local previous_pwd=$(pwd)
- echo "*** Build Zephyr project: $arg_project"
- cd "$TEST_DIR" || zef_abort
- zef_open "$TEST_WEST_BASE" "$TEST_ZEPHYR_SDK_DIR" || zef_abort
- west build -b "$arg_board" "$arg_project" || zef_abort
- zef_close || zef_abort
- echo
- cd "$previous_pwd" || zef_abort
-}
-
-
-run_unittests() {
- echo '==== Unit tests: run unit tests before interactive sessions ?'
- test_run_yn
- if [ "$?" = 0 ]; then
- return
- fi
- dtsh_unittests
- sleep 2
-}
-
-
-run_interactive_use_case1() {
- echo '==== UC1: DTS and bindings from edtlib unit tests'
- echo ' DTS: tests/test.dts'
- echo ' Bindings search path: tests/bindings'
- test_run_yn
- if [ "$?" = 0 ]; then
- return
- fi
- local test_venv="$TEST_DIR/.venv"
- echo '==== Setup test environment'
- dtsh_clean
- dtsh_venv "$test_venv"
- echo 'done.'
- echo
- "$VIRTUAL_ENV/bin/dtsh" "$TEST_DTS_EDTLIB" "$TEST_BINDINGS_EDTLIB" || zef_abort
- echo 'done.'
- echo
- echo '==== Dispose test environment'
- deactivate
- rm -r "$TEST_DIR"
- echo 'done.'
-}
-
-
-run_interactive_use_case2() {
- echo '==== UC2: DTS from Zephyr build, Zephyr bindings'
- echo ' Bindings search path: $ZEPHYR_BASE/dts/bindings'
- # Only ZEPHYR_BASE is set.
- echo ' Toolchain: unavailable'
- echo " Application: $(basename "$TEST_PROJECT_SENSOR")"
- echo " Board: $TEST_BOARD_NRF52"
- test_run_yn
- if [ "$?" = 0 ]; then
- return
- fi
- local test_venv="$TEST_DIR/.venv"
- if [ -d "$TEST_DIR" ]; then
- rm -rf "$TEST_DIR"
- fi
- mkdir -p "$TEST_DIR"
- test_build_zephyr_project "$TEST_PROJECT_SENSOR" "$TEST_BOARD_NRF52"
- echo 'done.'
- echo
- echo '==== Setup test environment'
- dtsh_clean
- dtsh_venv "$test_venv"
- export ZEPHYR_BASE="$TEST_ZEPHYR_BASE"
- echo 'done.'
- echo
- "$VIRTUAL_ENV/bin/dtsh" "$TEST_DIR/build/zephyr/zephyr.dts" || zef_abort
- echo 'done.'
- echo
- echo '==== Dispose test environment'
- unset ZEPHYR_BASE
- deactivate
- rm -r "$TEST_DIR"
- echo 'done.'
-}
-
-
-run_interactive_use_case3() {
- echo '==== UC3: DTS from Zephyr build, Zephyr bindings'
- echo ' Bindings search path: $ZEPHYR_BASE/dts/bindings'
- echo ' Toolchain: Zephyr SDK'
- echo " Application: $(basename "$TEST_PROJECT_SENSOR")"
- echo " Board: $TEST_BOARD_NRF52"
- test_run_yn
- if [ "$?" = 0 ]; then
- return
- fi
- local test_venv="$TEST_DIR/.venv"
- if [ -d "$TEST_DIR" ]; then
- rm -rf "$TEST_DIR"
- fi
- mkdir -p "$TEST_DIR"
- test_build_zephyr_project "$TEST_PROJECT_SENSOR" "$TEST_BOARD_NRF52"
- echo 'done.'
- echo
- echo '==== Setup test environment'
- dtsh_clean
- zef_open "$TEST_WEST_BASE" "$TEST_ZEPHYR_SDK_DIR" || zef_abort
- pip install "$DTSH_HOME" || zef_abort
- echo 'done.'
- echo
- "$VIRTUAL_ENV/bin/dtsh" "$TEST_DIR/build/zephyr/zephyr.dts" || zef_abort
- echo 'done.'
- echo
- echo '==== Dispose test environment'
- pip uninstall --yes dtsh || zef_abort
- zef_close || zef_abort
- rm -r "$TEST_DIR"
- echo 'done.'
-}
-
-
-run_interactive_use_case4() {
- echo '==== UC4: DTS from Zephyr build, Zephyr bindings'
- echo ' Bindings search path: $ZEPHYR_BASE/dts/bindings'
- echo ' Toolchain (dtsh): Zephyr SDK'
- echo " Application: $(basename "$TEST_PROJECT_CAN")"
- echo " Board: $TEST_BOARD_F407GZ"
- test_run_yn
- if [ "$?" = 0 ]; then
- return
- fi
- local test_venv="$TEST_DIR/.venv"
- if [ -d "$TEST_DIR" ]; then
- rm -rf "$TEST_DIR"
- fi
- mkdir -p "$TEST_DIR"
- test_build_zephyr_project "$TEST_PROJECT_CAN" "$TEST_BOARD_F407GZ"
- echo 'done.'
- echo
- echo '==== Setup test environment'
- dtsh_clean
- zef_open "$TEST_WEST_BASE" "$TEST_ZEPHYR_SDK_DIR" || zef_abort
- pip install "$DTSH_HOME" || zef_abort
- echo 'done.'
- echo
- "$VIRTUAL_ENV/bin/dtsh" "$TEST_DIR/build/zephyr/zephyr.dts" || zef_abort
- echo 'done.'
- echo
- echo '==== Dispose test environment'
- pip uninstall --yes dtsh || zef_abort
- zef_close || zef_abort
- rm -r "$TEST_DIR"
- echo 'done.'
-}
-
-
-run_interactive_use_case5() {
- echo '==== UC5: DTS from Zephyr build, Zephyr bindings'
- echo ' Bindings search path: $ZEPHYR_BASE/dts/bindings'
- echo ' Toolchain (dtsh): Zephyr SDK'
- echo " Application: $(basename "$TEST_PROJECT_COAP")"
- echo " Board: $TEST_BOARD_NXP"
- test_run_yn
- if [ "$?" = 0 ]; then
- return
- fi
- local test_venv="$TEST_DIR/.venv"
- if [ -d "$TEST_DIR" ]; then
- rm -rf "$TEST_DIR"
- fi
- mkdir -p "$TEST_DIR"
- test_build_zephyr_project "$TEST_PROJECT_COAP" "$TEST_BOARD_NXP"
- echo 'done.'
- echo
- echo '==== Setup test environment'
- dtsh_clean
- zef_open "$TEST_WEST_BASE" "$TEST_ZEPHYR_SDK_DIR" || zef_abort
- pip install "$DTSH_HOME" || zef_abort
- echo 'done.'
- echo
- "$VIRTUAL_ENV/bin/dtsh" "$TEST_DIR/build/zephyr/zephyr.dts" || zef_abort
- echo 'done.'
- echo
- echo '==== Dispose test environment'
- pip uninstall --yes dtsh || zef_abort
- zef_close || zef_abort
- rm -r "$TEST_DIR"
- echo 'done.'
-}
-
-
-run_interactive_use_case6() {
- echo '==== UC6: DTS from Zephyr build, Zephyr bindings'
- echo ' Bindings search path: $ZEPHYR_BASE/dts/bindings'
- echo ' Toolchain (dtsh): Zephyr SDK'
- echo " Application: $(basename "$TEST_PROJECT_USB")"
- echo " Board: $TEST_BOARD_NXP"
- test_run_yn
- if [ "$?" = 0 ]; then
- return
- fi
- local test_venv="$TEST_DIR/.venv"
- if [ -d "$TEST_DIR" ]; then
- rm -rf "$TEST_DIR"
- fi
- mkdir -p "$TEST_DIR"
- test_build_zephyr_project "$TEST_PROJECT_USB" "$TEST_BOARD_NXP"
- echo 'done.'
- echo
- echo '==== Setup test environment'
- dtsh_clean
- zef_open "$TEST_WEST_BASE" "$TEST_ZEPHYR_SDK_DIR" || zef_abort
- pip install "$DTSH_HOME" || zef_abort
- echo 'done.'
- echo
- "$VIRTUAL_ENV/bin/dtsh" "$TEST_DIR/build/zephyr/zephyr.dts" || zef_abort
- echo 'done.'
- echo
- echo '==== Dispose test environment'
- pip uninstall --yes dtsh || zef_abort
- zef_close || zef_abort
- rm -r "$TEST_DIR"
- echo 'done.'
-}
-
-
-run_interactive_use_case7() {
- echo '==== UC7: DTS from Zephyr build, Zephyr bindings'
- echo ' Bindings search path: $ZEPHYR_BASE/dts/bindings'
- echo ' Toolchain (dtsh): Zephyr SDK'
- echo " Application: $(basename "$TEST_PROJECT_BLE")"
- echo " Board: $TEST_BOARD_NANO"
- test_run_yn
- if [ "$?" = 0 ]; then
- return
- fi
- local test_venv="$TEST_DIR/.venv"
- if [ -d "$TEST_DIR" ]; then
- rm -rf "$TEST_DIR"
- fi
- mkdir -p "$TEST_DIR"
- test_build_zephyr_project "$TEST_PROJECT_BLE" "$TEST_BOARD_NANO"
- echo 'done.'
- echo
- echo '==== Setup test environment'
- dtsh_clean
- zef_open "$TEST_WEST_BASE" "$TEST_ZEPHYR_SDK_DIR" || zef_abort
- pip install "$DTSH_HOME" || zef_abort
- echo 'done.'
- echo
- "$VIRTUAL_ENV/bin/dtsh" "$TEST_DIR/build/zephyr/zephyr.dts" || zef_abort
- echo 'done.'
- echo
- echo '==== Dispose test environment'
- pip uninstall --yes dtsh || zef_abort
- zef_close || zef_abort
- rm -r "$TEST_DIR"
- echo 'done.'
-}
-
-
-run_interactive_use_case8() {
- echo '==== UC8: DTS from Zephyr build, Zephyr bindings'
- echo ' Bindings search path: $ZEPHYR_BASE/dts/bindings'
- echo ' Toolchain: GCC Arm 10'
- echo " Application: $(basename "$TEST_PROJECT_SENSOR")"
- echo " Board: $TEST_BOARD_NRF52"
- test_run_yn
- if [ "$?" = 0 ]; then
- return
- fi
- local test_venv="$TEST_DIR/.venv"
- if [ -d "$TEST_DIR" ]; then
- rm -rf "$TEST_DIR"
- fi
- mkdir -p "$TEST_DIR"
- test_build_zephyr_project "$TEST_PROJECT_SENSOR" "$TEST_BOARD_NRF52"
- echo 'done.'
- echo
- echo '==== Setup test environment'
- dtsh_clean
- zef_open "$TEST_WEST_BASE" "$TEST_GCCARM10_DIR" || zef_abort
- pip install "$DTSH_HOME" || zef_abort
- echo 'done.'
- echo
- "$VIRTUAL_ENV/bin/dtsh" "$TEST_DIR/build/zephyr/zephyr.dts" || zef_abort
- echo 'done.'
- echo
- echo '==== Dispose test environment'
- pip uninstall --yes dtsh || zef_abort
- zef_close || zef_abort
- rm -r "$TEST_DIR"
- echo 'done.'
-}
-
-
-run_interactive_use_case9() {
- echo '==== UC9: DTS from Zephyr build, Zephyr bindings'
- echo ' Bindings search path: $ZEPHYR_BASE/dts/bindings'
- echo ' Toolchain: GCC Arm 11'
- echo " Application: $(basename "$TEST_PROJECT_SENSOR")"
- echo " Board: $TEST_BOARD_NRF52"
- test_run_yn
- if [ "$?" = 0 ]; then
- return
- fi
- local test_venv="$TEST_DIR/.venv"
- if [ -d "$TEST_DIR" ]; then
- rm -rf "$TEST_DIR"
- fi
- mkdir -p "$TEST_DIR"
- test_build_zephyr_project "$TEST_PROJECT_SENSOR" "$TEST_BOARD_NRF52"
- echo 'done.'
- echo
- echo '==== Setup test environment'
- dtsh_clean
- zef_open "$TEST_WEST_BASE" "$TEST_GCCARM11_DIR" || zef_abort
- pip install "$DTSH_HOME" || zef_abort
- echo 'done.'
- echo
- "$VIRTUAL_ENV/bin/dtsh" "$TEST_DIR/build/zephyr/zephyr.dts" || zef_abort
- echo 'done.'
- echo
- echo '==== Dispose test environment'
- pip uninstall --yes dtsh || zef_abort
- zef_close || zef_abort
- rm -r "$TEST_DIR"
- echo 'done.'
-}
-
-
-clear
-run_unittests
-clear
-run_interactive_use_case1
-clear
-run_interactive_use_case2
-clear
-run_interactive_use_case3
-clear
-run_interactive_use_case4
-clear
-run_interactive_use_case5
-clear
-run_interactive_use_case6
-clear
-run_interactive_use_case7
-clear
-run_interactive_use_case8
-clear
-run_interactive_use_case9
diff --git a/pyproject.toml b/pyproject.toml
index fed528d..1a268f9 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,3 +1,20 @@
[build-system]
requires = ["setuptools"]
build-backend = "setuptools.build_meta"
+
+
+[tool.black]
+# References:
+# - https://black.readthedocs.io/en/stable/usage_and_configuration/
+line-length = 80
+target-version = ['py38']
+include = '\.pyi?$'
+# 'extend-exclude' excludes files or directories in addition to the defaults
+extend-exclude = '''
+# A regex preceded with ^/ will apply only to files and directories
+# in the root of the project.
+(
+ ^/foo.py # exclude a file named foo.py in the root of the project
+ | .*_pb2.py # exclude autogenerated Protocol Buffer files anywhere in the project
+)
+'''
diff --git a/pyrightconfig.json b/pyrightconfig.json
deleted file mode 100644
index a701a4e..0000000
--- a/pyrightconfig.json
+++ /dev/null
@@ -1,25 +0,0 @@
-{
- "include": [
- "src",
- "tests"
- ],
-
- "exclude": [
- "tests/bindings"
- ],
-
- "pythonVersion": "3.8",
- "pythonPlatform": "All",
-
- "venvPath": ".",
- "venv": ".venv",
-
- "executionEnvironments": [
- {
- "root": "tests",
- "extraPaths": [
- "src"
- ]
- }
- ]
-}
diff --git a/pytest.ini b/pytest.ini
deleted file mode 100644
index 52e264c..0000000
--- a/pytest.ini
+++ /dev/null
@@ -1,9 +0,0 @@
-# pytest configuration.
-#
-# See:
-# - https://docs.pytest.org/en/7.3.x/reference/customize.html#pytest-ini
-# - https://docs.pytest.org/en/7.3.x/reference/reference.html#ini-options-ref
-
-[pytest]
-testpaths = tests
-pythonpath = src
diff --git a/requirements-dev.txt b/requirements-dev.txt
index aa65804..a12df06 100644
--- a/requirements-dev.txt
+++ b/requirements-dev.txt
@@ -1,7 +1,6 @@
-# Python requirements for development/tests.
-#
+pycodestyle
+flake8
+pylint
mypy
types-PyYAML
-pyright
-pylint
pytest
diff --git a/requirements-lsp.txt b/requirements-lsp.txt
new file mode 100644
index 0000000..1f7b4b9
--- /dev/null
+++ b/requirements-lsp.txt
@@ -0,0 +1,3 @@
+python-lsp-server[all]
+pylsp-mypy
+python-lsp-black
diff --git a/requirements.txt b/requirements.txt
new file mode 100644
index 0000000..fb49c72
--- /dev/null
+++ b/requirements.txt
@@ -0,0 +1,3 @@
+PyYAML
+rich
+gnureadline ; sys_platform == 'darwin'
diff --git a/setup.cfg b/setup.cfg
new file mode 100644
index 0000000..8236127
--- /dev/null
+++ b/setup.cfg
@@ -0,0 +1,173 @@
+[metadata]
+name = dtsh
+version = 0.2rc1
+author = Christophe Dufaza
+author_email = chris@openmarl.org
+description = Shell-like interface with Zephyr Devicetree
+long_description = file: README.rst
+license = Apache License version 2.0
+url = https://github.com/dottspina/dtsh
+keywords = devicetree, zephyr, dts, embedded
+
+classifiers =
+ Development Status :: 4 - Beta
+ Programming Language :: Python :: 3
+ Intended Audience :: Developers
+ Topic :: Software Development :: Embedded Systems
+ License :: OSI Approved :: Apache Software License
+ Programming Language :: Python :: 3
+ Programming Language :: Python :: 3.8
+ Programming Language :: Python :: 3.9
+ Programming Language :: Python :: 3.10
+ Programming Language :: Python :: 3.11
+ Programming Language :: Python :: 3 :: Only
+
+[options]
+python_requires = >=3.8
+install_requires =
+ PyYAML
+ rich
+ gnureadline ; sys_platform == 'darwin'
+
+packages =
+ dtsh
+ dtsh.builtins
+ dtsh.rich
+ devicetree
+
+package_dir =
+ = src
+
+[options.package_data]
+dtsh =
+ py.typed
+ dtsh.ini
+dtsh.rich =
+ theme.ini
+
+[options.entry_points]
+console_scripts =
+ dtsh = dtsh.cli:run
+
+[options.extras_require]
+# Linters, type hinting, unit tests, etc.
+dev =
+ pycodestyle
+ flake8
+ pylint
+ mypy
+ types-PyYAML
+ pytest
+# IDE/LSP integration.
+lsp =
+ python-lsp-server[all]
+ pylsp-mypy
+ python-lsp-black
+# Package distribution only
+dist =
+ build
+ twine
+
+[tool:pytest]
+pythonpath = src
+testpaths = tests
+
+
+[pycodestyle]
+# References:
+# - https://pycodestyle.pycqa.org/en/latest/intro.html#configuration
+max-line-length = 80
+
+
+[pydocstyle]
+# References:
+# - https://peps.python.org/pep-0257/
+# - http://www.pydocstyle.org/en/stable/usage.html
+# - https://google.github.io/styleguide/pyguide.html#Comments
+#
+# Cannot pass both ignore and convention (unfortunately).
+#ignore = D105
+
+# Relax pydocstyle for test source code.
+convention = google
+match_dir = ^(?!tests|build|\.venv).*
+
+
+[pylint.]
+# References:
+# - https://pylint.readthedocs.io/en/latest/user_guide/usage/run.html
+disable =
+ # invalid-name
+ # Fix: except Exception as e
+ C0103,
+ # too-many-ancestor
+ # Fix: _Loader(YAMLLoader)
+ R0901,
+ # too-many-instance-attributes
+ R0902,
+ # too-few-public-methods
+ # Example: abstract base class DTNodeCriterion
+ R0903,
+ # too-many-public-methods
+ R0904,
+ # too-many-return-statements
+ R0911,
+ # too-many-branches
+ R0912,
+ # too-many-function-args
+ R0913,
+ # too-many-locals
+ R0914,
+ # line-too-long
+ # Example: URL in docstrings
+ C0301,
+ # too-many-lines
+ # Example: dtsh.model module
+ C0302,
+ # missing-function-docstring
+ # C0116,
+ # missing-class-docstring
+ # C0115,
+ # protected-access
+ # W0212,
+ # pointless-statement
+ # W0104
+# To ignore files or directories (base names, not paths):
+# ignore=
+ignore = setup.py
+
+# Zephyr linter configuration.
+min-similarity-lines = 10
+
+
+[flake8]
+# References:
+# - https://flake8.pycqa.org/en/latest/user/configuration.html
+extend-ignore =
+ # line-too-long: we rely on black for this
+ E501
+ # black formatting would fail with "whitespace before ':'"
+ # See https://github.com/psf/black/issues/280
+ E203
+
+
+[mypy]
+# References:
+# - https://mypy.readthedocs.io/en/stable/config_file.html
+mypy_path = src:tests
+exclude = tests/res
+python_version = 3.8
+packages = dtsh
+
+
+[pylsp-mypy]
+# References:
+# - https://github.com/python-lsp/pylsp-mypy
+enabled = true
+dmypy = false
+live_mode = true
+strict = true
+
+
+[pep8]
+aggressive = 3
diff --git a/setup.py b/setup.py
index de42962..c87d899 100644
--- a/setup.py
+++ b/setup.py
@@ -1,172 +1,10 @@
-"""Project configuration (setuptools).
-"""
+# Copyright (c) 2023 Christophe Dufaza
+#
+# SPDX-License-Identifier: Apache-2.0
-# Always prefer setuptools over distutils
-from setuptools import setup, find_packages
-import pathlib
+"""Required to bootstrap setipt.cfg in some situations."""
-here = pathlib.Path(__file__).parent.resolve()
+from setuptools import setup # type: ignore
-long_description = (here / "README.rst").read_text(encoding="utf-8")
-
-setup(
- # There are some restrictions on what makes a valid project name
- # specification here:
- # https://packaging.python.org/specifications/core-metadata/#name
- #
- name="dtsh",
-
- # Versions should comply with PEP 440:
- # https://www.python.org/dev/peps/pep-0440/
- #
- # For a discussion on single-sourcing the version across setup.py and the
- # project code, see
- # https://packaging.python.org/guides/single-sourcing-package-version/
- #
- # See also: https://peps.python.org/pep-0440/
- #
- version="0.1.0b2",
-
- # This is a one-line description or tagline of what your project does. This
- # corresponds to the "Summary" metadata field:
- # https://packaging.python.org/specifications/core-metadata/#summary
- #
- description="Shell-like interface with Zephyr devicetree and bindings",
-
- # This field corresponds to the "Description" metadata field:
- # https://packaging.python.org/specifications/core-metadata/#description-optional
- #
- long_description=long_description,
-
- # Denotes that our long_description is in Markdown; valid values are
- # text/plain, text/x-rst, and text/markdown
- #
- # Optional if long_description is written in reStructuredText (rst) but
- # required for plain-text or Markdown; if unspecified, "applications should
- # attempt to render [the long_description] as text/x-rst; charset=UTF-8 and
- # fall back to text/plain if it is not valid rst" (see link below)
- #
- # This field corresponds to the "Description-Content-Type" metadata field:
- # https://packaging.python.org/specifications/core-metadata/#description-content-type-optional
- #
- #long_description_content_type="text/markdown",
-
- # This field corresponds to the "Home-Page" metadata field:
- # https://packaging.python.org/specifications/core-metadata/#home-page-optional
- #
- url="https://github.com/dottspina/dtsh",
-
- author="Chris Duf",
- author_email="chris@openmarl.org",
-
- license="Apache License version 2.0",
-
- # Classifiers help users find your project by categorizing it.
- #
- # For a list of valid classifiers, see https://pypi.org/classifiers/
- #
- classifiers=[
- # How mature is this project? Common values are
- # 3 - Alpha
- # 4 - Beta
- # 5 - Production/Stable
- "Development Status :: 4 - Beta",
- # Indicate who your project is intended for
- "Intended Audience :: Developers",
- "Topic :: Software Development :: Embedded Systems",
- # Pick your license as you wish
- "License :: OSI Approved :: Apache Software License",
- # Specify the Python versions you support here. In particular, ensure
- # that you indicate you support Python 3. These classifiers are *not*
- # checked by 'pip install'. See instead 'python_requires' below.
- "Programming Language :: Python :: 3",
- "Programming Language :: Python :: 3.8",
- "Programming Language :: Python :: 3.9",
- "Programming Language :: Python :: 3.10",
- "Programming Language :: Python :: 3 :: Only",
- ],
-
- # Note that this is a list of additional keywords, separated
- # by commas, to be used to assist searching for the distribution in a
- # larger catalog.
- #
- keywords="devicetree, zephyr, dts, embedded",
-
- package_dir={"": "src"},
- packages=find_packages(where="src"),
-
- # Specify which Python versions you support. In contrast to the
- # 'Programming Language' classifiers above, 'pip install' will check this
- # and refuse to install the project if the version does not match. See
- # https://packaging.python.org/guides/distributing-packages-using-setuptools/#python-requires
- python_requires=">=3.8, <4",
-
- # This field lists other packages that your project depends on to run.
- # Any package you put here will be installed by pip when your project is
- # installed, so they must be valid existing projects.
- #
- # For an analysis of "install_requires" vs pip's requirements files see:
- # https://packaging.python.org/discussions/install-requires-vs-requirements/
- #
- # Requirements for both devicetree and dtsh.
- install_requires=[
- "PyYAML>=5.1", "rich", "Pygments",
- # Preferred GNU readline support for macOS.
- "gnureadline;sys_platform=='darwin'",
- ],
-
- # List additional groups of dependencies here (e.g. development
- # dependencies). Users will be able to install these using the "extras"
- # syntax, for example:
- #
- # $ pip install sampleproject[dev]
- #
- # Similar to `install_requires` above, these must be valid existing
- # projects.
- #
- # Optional.
- extras_require={
- "dev": ["mypy", "types-PyYAML", "pyright", "pylint", "pytest"],
- "test": ["pytest"],
- "dist": ["build", "twine"],
- },
-
- # If there are data files included in your packages that need to be
- # installed, specify them here.
- package_data={
- "dtsh": ["theme"],
- },
-
- # Although 'package_data' is the preferred approach, in some case you may
- # need to place data files outside of your packages. See:
- # http://docs.python.org/distutils/setupscript.html#installing-additional-files
- #
- # In this case, 'data_file' will be installed into '/my_data'
- #
- # Optional.
- # data_files=[("my_data", ["data/data_file"])], # Optional
-
- # To provide executable scripts, use entry points in preference to the
- # "scripts" keyword. Entry points provide cross-platform support and allow
- # `pip` to create the appropriate form of executable for the target
- # platform.
- #
- entry_points={
- "console_scripts": [
- "dtsh=dtsh.cli:run",
- ],
- },
-
- # List additional URLs that are relevant to your project as a dict.
- #
- # This field corresponds to the "Project-URL" metadata fields:
- # https://packaging.python.org/specifications/core-metadata/#project-url-multiple-use
- #
- project_urls={
- # 'Documentation': 'https://packaging.python.org/tutorials/distributing-packages/',
- "Bug Reports": "https://github.com/dottspina/dtsh/issues",
- # "Funding": "https://donate.pypi.org",
- # "Say Thanks!": "http://saythanks.io/to/example",
- "Source": "https://github.com/dottspina/dtsh",
- },
-)
+if __name__ == "__main__":
+ setup()
diff --git a/src/dtsh/__init__.py b/src/dtsh/__init__.py
index e69de29..c8394a3 100644
--- a/src/dtsh/__init__.py
+++ b/src/dtsh/__init__.py
@@ -0,0 +1,5 @@
+# Copyright (c) 2023 Christophe Dufaza
+#
+# SPDX-License-Identifier: Apache-2.0
+
+"""Core devicetree shell API."""
diff --git a/src/dtsh/autocomp.py b/src/dtsh/autocomp.py
index d07f0f1..324821c 100644
--- a/src/dtsh/autocomp.py
+++ b/src/dtsh/autocomp.py
@@ -1,135 +1,766 @@
-# Copyright (c) 2022 Chris Duf
+# Copyright (c) 2023 Christophe Dufaza
#
# SPDX-License-Identifier: Apache-2.0
-"""Auto-completion with GNU readline for devicetree shells."""
+"""Completion logic and base display callbacks for GNU Readline integration.
-from typing import cast, Any, Dict, List
+Unit tests and examples: tests/test_dtsh_autocomp.py
+"""
-from devicetree.edtlib import Node, Binding, Property
-from dtsh.dtsh import Dtsh, DtshCommand, DtshAutocomp
+from typing import cast, List, Set
+import os
-class DevicetreeAutocomp(DtshAutocomp):
- """Devicetree shell commands auto-completion support with GNU readline.
- """
+from dtsh.model import DTPath, DTNode, DTBinding
+from dtsh.rl import DTShReadline
+from dtsh.io import DTShOutput
+from dtsh.config import DTShConfig
+from dtsh.shell import (
+ DTSh,
+ DTShCommand,
+ DTShOption,
+ DTShArg,
+ DTShCommandNotFoundError,
+ DTPathNotFoundError,
+)
- # Maps completion state (strings) and model (objects).
- #
- _autocomp_state: Dict[str, Any]
- # Autocomp mode.
- #
- _mode: int
+_dtshconf: DTShConfig = DTShConfig.getinstance()
- def __init__(self, shell: Dtsh) -> None:
- """Initialize the completion engine.
+
+class RlStateDTShCommand(DTShReadline.CompleterState):
+ """RL completer state for DTSh commands."""
+
+ def __init__(self, rlstr: str, cmd: DTShCommand) -> None:
+ """Initialize completer state.
+
+ Args:
+ rlstr: The command name to substitute the RL completion scope with.
+ cmd: The corresponding command.
+ """
+ super().__init__(rlstr, cmd)
+
+ @property
+ def cmd(self) -> DTShCommand:
+ """The corresponding command."""
+ return cast(DTShCommand, self._item)
+
+
+class RlStateDTShOption(DTShReadline.CompleterState):
+ """RL completer state for DTSh command options."""
+
+ def __init__(self, rlstr: str, opt: DTShOption) -> None:
+ """Initialize completer state.
+
+ Args:
+ rlstr: The option name to substitute the RL completion scope with.
+ opt: The corresponding option.
+ """
+ super().__init__(rlstr, opt)
+
+ @property
+ def opt(self) -> DTShOption:
+ """The corresponding option."""
+ return cast(DTShOption, self._item)
+
+
+class RlStateDTPath(DTShReadline.CompleterState):
+ """RL completer state for Devicetree paths."""
+
+ def __init__(self, rlstr: str, node: DTNode) -> None:
+ """Initialize completer state.
+
+ Args:
+ rlstr: The absolute or relative Devicetree path to substitute
+ the RL completion scope with.
+ node: The corresponding Devicetree node.
+ """
+ super().__init__(rlstr, node)
+
+ @property
+ def node(self) -> DTNode:
+ """The corresponding Devicetree node."""
+ return cast(DTNode, self._item)
+
+
+class RlStateCompatStr(DTShReadline.CompleterState):
+ """RL completer state for compatible strings."""
+
+ _bindings: Set[DTBinding]
+
+ def __init__(self, rlstr: str, bindings: Set[DTBinding]) -> None:
+ """Initialize completer state.
+
+ Args:
+ rlstr: The compatible string to substitute the RL completion scope with.
"""
- self._dtsh = shell
- self._mode = DtshAutocomp.MODE_ANY
- self._autocomp_state = {}
+ super().__init__(rlstr, None)
+ self._bindings = bindings
+
+ @property
+ def compatstr(self) -> str:
+ """The corresponding compatible string value."""
+ return self._rlstr
@property
- def count(self) -> int:
- """Implements DtshAutocomp.count().
+ def bindings(self) -> Set[DTBinding]:
+ """All bindings for this compatible (e.g. for different buses)."""
+ return self._bindings
+
+
+class RlStateDTVendor(DTShReadline.CompleterState):
+ """RL completer state for device or device class vendors."""
+
+ def __init__(self, rlstr: str, vendor: str) -> None:
+ """Initialize completer state.
+
+ Args:
+ rlstr: The vendor prefix to substitute the RL completion scope with.
+ vendor: The corresponding vendor name.
"""
- return len(self._autocomp_state)
+ super().__init__(rlstr, vendor)
@property
- def hints(self) -> List[str]:
- """Implements DtshAutocomp.hints().
+ def prefix(self) -> str:
+ """The corresponding vendor prefix."""
+ return self._rlstr
+
+ @property
+ def vendor(self) -> str:
+ """The corresponding vendor name."""
+ return cast(str, self._item)
+
+
+class RlStateDTBus(DTShReadline.CompleterState):
+ """RL completer state for Devicetree bus protocols."""
+
+ def __init__(self, rlstr: str) -> None:
+ """Initialize completer state.
+
+ Args:
+ rlstr: The bus protocol to substitute the RL completion scope with.
"""
- return list(self._autocomp_state.keys())
+ super().__init__(rlstr, None)
@property
- def model(self) -> List[Any]:
- """Implements DtshAutocomp.model().
+ def proto(self) -> str:
+ """The bus protocol."""
+ return self._rlstr
+
+
+class RlStateDTAlias(DTShReadline.CompleterState):
+ """RL completer state for aliased nodes."""
+
+ def __init__(self, rlstr: str, node: DTNode) -> None:
+ """Initialize completer state.
+
+ Args:
+ rlstr: The alias name to substitute the RL completion scope with.
+ node: The aliased node.
"""
- return list(self._autocomp_state.values())
+ super().__init__(rlstr, node)
@property
- def mode(self) -> int:
- """Implements DtshAutocomp.mode().
+ def alias(self) -> str:
+ """The corresponding alias name."""
+ return self._rlstr
+
+ @property
+ def node(self) -> DTNode:
+ """The aliased node."""
+ return cast(DTNode, self._item)
+
+
+class RlStateDTChosen(DTShReadline.CompleterState):
+ """RL completer state for chosen nodes."""
+
+ def __init__(self, rlstr: str, node: DTNode) -> None:
+ """Initialize completer state.
+
+ Args:
+ rlstr: The parameter name to substitute the RL completion scope with.
+ node: The chosen node.
+ """
+ super().__init__(rlstr, node)
+
+ @property
+ def chosen(self) -> str:
+ """The chosen parameter name."""
+ return self._rlstr
+
+ @property
+ def node(self) -> DTNode:
+ """The chosen node."""
+ return cast(DTNode, self._item)
+
+
+class RlStateDTLabel(DTShReadline.CompleterState):
+ """RL completer state for labeled nodes."""
+
+ def __init__(self, rlstr: str, node: DTNode) -> None:
+ """Initialize completer state.
+
+ Args:
+ rlstr: The label candidate, prefixed with "&" when completing
+ path parameters, without the leading "&" otherwise.
+ node: The labeled node.
+ """
+ super().__init__(rlstr, node)
+
+ @property
+ def label(self) -> str:
+ """The label (without the leading "&")."""
+ return self._rlstr[1:] if self._rlstr.startswith("&") else self._rlstr
+
+ @property
+ def node(self) -> DTNode:
+ """The labeled node."""
+ return cast(DTNode, self._item)
+
+
+class RlStateFsEntry(DTShReadline.CompleterState):
+ """RL completer state for file-system paths."""
+
+ def __init__(self, rlstr: str, dirent: os.DirEntry[str]) -> None:
+ """Initialize completer state.
+
+ Args:
+ rlstr: The file-system path to substitute the RL completion scope with.
+ dirent: The corresponding file or directory.
+ """
+ super().__init__(rlstr, dirent)
+
+ @property
+ def dirent(self) -> os.DirEntry[str]:
+ """The corresponding file or directory."""
+ return cast(os.DirEntry[str], self._item)
+
+
+class RlStateEnum(DTShReadline.CompleterState):
+ """RL completer state for enumerated values."""
+
+ def __init__(self, rlstr: str, brief: str) -> None:
+ """Initialize completer state.
+
+ Args:
+ rlstr: The value string to substitute the RL completion scope with.
+ brief: The value semantic.
+ """
+ super().__init__(rlstr, brief)
+
+ @property
+ def value(self) -> str:
+ """The enumerated value."""
+ return self._rlstr
+
+ @property
+ def brief(self) -> str:
+ """The corresponding description."""
+ return cast(str, self._item)
+
+
+class DTShAutocomp:
+ """Base completion logic and display callbacks for GNU readline integration."""
+
+ # The shell this will auto-complete the command line of.
+ _dtsh: DTSh
+
+ @staticmethod
+ def complete_dtshcmd(
+ cs_txt: str, sh: DTSh
+ ) -> List[DTShReadline.CompleterState]:
+ """Complete command name.
+
+ Args:
+ cs_txt: The command name to complete.
+ sh: The context shell.
+
+ Returns:
+ The commands that are valid completer states.
"""
- return self._mode
+ return sorted(
+ RlStateDTShCommand(cmd.name, cmd)
+ for cmd in sh.commands
+ if cmd.name.startswith(cs_txt)
+ )
- def reset(self) -> None:
- """Implements DtshAutocomp.reset().
+ @staticmethod
+ def complete_dtshopt(
+ cs_txt: str, cmd: DTShCommand
+ ) -> List[DTShReadline.CompleterState]:
+ """Complete option name.
+
+ Args:
+ cs_txt: The option name to complete, starting with "--" or "-".
+ cmd: The command to search for matching options.
+
+ Returns:
+ The options that are valid completer states.
"""
- self._mode = DtshAutocomp.MODE_ANY
- self._autocomp_state.clear()
+ states: List[DTShReadline.CompleterState] = []
+
+ if cs_txt.startswith("--"):
+ # Only options with a long name.
+ prefix = cs_txt[2:]
+ states.extend(
+ sorted(
+ [
+ RlStateDTShOption(f"--{opt.longname}", opt)
+ for opt in cmd.options
+ if opt.longname and opt.longname.startswith(prefix)
+ ],
+ key=lambda x: "-"
+ if x.rlstr == "--help"
+ else x.rlstr.lower(),
+ )
+ )
+ elif cs_txt.startswith("-"):
+ if cs_txt == "-":
+ # All options, those with a short name first.
+ states.extend(
+ sorted(
+ [
+ RlStateDTShOption(f"-{opt.shortname}", opt)
+ for opt in cmd.options
+ if opt.shortname
+ ],
+ key=lambda x: "-"
+ if x.rlstr == "-h"
+ else x.rlstr.lower(),
+ )
+ )
+ # Then options with only a long name.
+ states.extend(
+ sorted(
+ [
+ RlStateDTShOption(f"--{opt.longname}", opt)
+ for opt in cmd.options
+ if not opt.shortname
+ ],
+ key=lambda x: x.rlstr.lower(),
+ )
+ )
+ else:
+ # Unique match, assuming the completion scope is "-".
+ opt = cmd.option(cs_txt)
+ if opt:
+ states.append(RlStateDTShOption(f"-{opt.shortname}", opt))
+
+ return states
+
+ @staticmethod
+ def complete_dtpath(
+ cs_txt: str, sh: DTSh
+ ) -> List[DTShReadline.CompleterState]:
+ """Complete Devicetree path.
+
+ Args:
+ cs_txt: The absolute or relative Devicetree path to complete.
+ sh: The context shell.
- def autocomplete(self,
- cmdline: str,
- prefix: str,
- cursor: int = 0) -> List[str]: # pyright: ignore reportUnusedVariable
- """Implements DtshAutocomp.autocomplete().
+ Returns:
+ The Devicetree paths that are valid completer states.
"""
- self.reset()
- cmdline_vstr = cmdline.lstrip().split()
- if len(cmdline_vstr) == 0:
- self._autocomp_empty_cmdline()
- elif prefix and (len(cmdline_vstr) == 1):
- self._autocomp_with_commands(prefix)
+ if cs_txt.startswith("&"):
+ parts = DTPath.split(cs_txt)
+ if len(parts) == 1:
+ # Auto-complete with labels only for the first path component.
+ return DTShAutocomp.complete_dtlabel(cs_txt, sh)
+
+ dirname = DTPath.dirname(cs_txt)
+ if (dirname == ".") and not cs_txt.startswith("./"):
+ # We'll search the current working branch
+ # because we complete a relative DT path.
+ # Though, we don't want to include the "." path component
+ # into the substitution strings if it's not actually part
+ # of the completion scope.
+ dirname = ""
+
+ states: List[DTShReadline.CompleterState] = []
+ try:
+ dirnode = sh.node_at(dirname)
+ except DTPathNotFoundError:
+ # Won't complete invalid path.
+ pass
else:
- cmd_name = cmdline_vstr[0]
- cmd = self._dtsh.builtin(cmd_name)
- if cmd:
- if prefix.startswith('-'):
- self._autocomp_with_options(cmd, prefix)
- else:
- self._autocomp_with_params(cmd, prefix)
-
- return self.hints
-
- def _autocomp_empty_cmdline(self) -> None:
- self._mode = DtshAutocomp.MODE_DTSH_CMD
- for cmd in self._dtsh.builtins:
- self._autocomp_state[cmd.name] = cmd
-
- def _autocomp_with_commands(self, prefix: str) -> None:
- self._mode = DtshAutocomp.MODE_DTSH_CMD
- for cmd in self._dtsh.builtins:
- if cmd.name.startswith(prefix) and (len(cmd.name) > len(prefix)):
- self._autocomp_state[cmd.name] = cmd
-
- def _autocomp_with_options(self, cmd: DtshCommand, prefix: str) -> None:
- self._mode = DtshAutocomp.MODE_DTSH_OPT
- for opt in cmd.autocomplete_option(prefix):
- # When building the options hints for rl_completion_matches(),
- # we must answer the longest possible hints for the given prefix:
- # we'll use the option's long name when it does not have any short
- # name or the prefix starts with '--', its short name otherwise.
- # The syntactic characters '-' are included in the hints since
- # they're part of the prefix.
- if opt.shortname and (not prefix.startswith('--')):
- self._autocomp_state[f'-{opt.shortname}'] = opt
- elif opt.longname:
- self._autocomp_state[f'--{opt.longname}'] = opt
-
- def _autocomp_with_params(self, cmd:DtshCommand, prefix: str) -> None:
- self._mode, model = cmd.autocomplete_param(prefix)
- if self._mode == DtshAutocomp.MODE_DT_NODE:
- for node in cast(List[Node], model):
- hint = node.path
- if node.children:
- # Prepare auto-completion state for TABing
- # the node's children enumeration.
- # See readline_completions_hook(() in dtsh.session.
- hint += '/'
- self._autocomp_state[hint] = node
- elif self._mode == DtshAutocomp.MODE_DT_PROP:
- for prop in cast(List[Property], model):
- hint = f'{prop.node.path}${prop.name}'
- self._autocomp_state[hint] = prop
- elif self._mode == DtshAutocomp.MODE_DT_BINDING:
- for binding in cast(List[Binding], model):
- self._autocomp_state[binding.compatible] = binding
- elif self._mode == DtshAutocomp.MODE_DTSH_CMD:
- for cmd in cast(List[DtshCommand], model):
- self._autocomp_state[cmd.name] = cmd
+ prefix = DTPath.basename(cs_txt)
+ states.extend(
+ # Intelligently join and
+ # to get a valid substitution string.
+ RlStateDTPath(DTPath.join(dirname, child.name), child)
+ for child in dirnode.children
+ if child.name.startswith(prefix)
+ )
+
+ return sorted(states)
+
+ @staticmethod
+ def complete_dtcompat(
+ cs_txt: str, sh: DTSh
+ ) -> List[DTShReadline.CompleterState]:
+ """Complete compatible string.
+
+ Args:
+ cs_txt: The compatible string to complete.
+ sh: The context shell.
+
+ Returns:
+ The compatible string values that are valid completer states,
+ associated with the relevant bindings.
+ """
+ states: List[DTShReadline.CompleterState] = []
+ for compatible in sorted(
+ (
+ compat
+ for compat in sh.dt.compatible_strings
+ if compat.startswith(cs_txt)
+ )
+ ):
+ bindings: Set[DTBinding] = set()
+ # 1st, try the natural API, with unknown bus.
+ binding = sh.dt.get_compatible_binding(compatible)
+ if binding:
+ # Exact single match (binding that does not expect
+ # a bus of appearance).
+ bindings.add(binding)
+ else:
+ # The compatible string is associated with a bus of appearance:
+ # - look for nodes specified by a binding whose compatible string
+ # matches our search
+ # - for each node, add its binding to those associated with
+ # the compatible string candidate
+ for node in sh.dt.get_compatible_devices(compatible):
+ if node.binding:
+ bindings.add(node.binding)
+
+ states.append(RlStateCompatStr(compatible, bindings))
+
+ return states
+
+ @staticmethod
+ def complete_dtvendor(
+ cs_txt: str, sh: DTSh
+ ) -> List[DTShReadline.CompleterState]:
+ """Complete vendor prefix.
+
+ Args:
+ cs_txt: The vendor prefix to complete.
+ sh: The context shell.
+
+ Returns:
+ The vendors that are valid completer states.
+ """
+ return sorted(
+ RlStateDTVendor(vendor.prefix, vendor.name)
+ for vendor in sh.dt.vendors
+ if vendor.prefix.startswith(cs_txt)
+ )
+
+ @staticmethod
+ def complete_dtbus(
+ cs_txt: str, sh: DTSh
+ ) -> List[DTShReadline.CompleterState]:
+ """Complete bus protocol.
+
+ Args:
+ cs_txt: The bus protocol to complete.
+ sh: The context shell.
+
+ Returns:
+ The bus protocols that are valid completer states.
+ """
+ return sorted(
+ RlStateDTBus(proto)
+ for proto in sh.dt.bus_protocols
+ if proto.startswith(cs_txt)
+ )
+
+ @staticmethod
+ def complete_dtalias(
+ cs_txt: str, sh: DTSh
+ ) -> List[DTShReadline.CompleterState]:
+ """Complete alias name.
+
+ Args:
+ cs_txt: The alias name to complete.
+ sh: The context shell.
+
+ Returns:
+ The aliased nodes that are valid completer states.
+ """
+ return sorted(
+ RlStateDTAlias(alias, node)
+ for alias, node in sh.dt.aliased_nodes.items()
+ if alias.startswith(cs_txt)
+ )
+
+ @classmethod
+ def complete_dtchosen(
+ cls, cs_txt: str, sh: DTSh
+ ) -> List[DTShReadline.CompleterState]:
+ """Complete chosen parameter name.
+
+ Args:
+ cs_txt: The chosen parameter name to complete.
+ sh: The context shell.
+
+ Returns:
+ The chosen nodes that are valid completer states.
+ """
+ return sorted(
+ RlStateDTChosen(chosen, node)
+ for chosen, node in sh.dt.chosen_nodes.items()
+ if chosen.startswith(cs_txt)
+ )
+
+ @classmethod
+ def complete_dtlabel(
+ cls, cs_txt: str, sh: DTSh
+ ) -> List[DTShReadline.CompleterState]:
+ """Complete devicetree label.
+
+ Args:
+ cs_txt: The devicetree label to complete,
+ with or without the leading "&".
+ If present, it's retained within the completion state.
+ sh: The context shell.
+
+ Returns:
+ The labeled nodes that are valid completer states.
+ """
+ # The label pattern is shifted when the completion scope starts with "&".
+ i_label = 1 if cs_txt.startswith("&") else 0
+
+ if cs_txt.endswith("/"):
+ # We assume we're completing a devicetree path like "&label/":
+ # the path to the node with label "label" is the only match.
+ label = cs_txt[i_label:-1]
+ try:
+ node = sh.dt.labeled_nodes[label]
+ return [RlStateDTPath(node.path, node)]
+ except KeyError:
+ return []
+
+ # Complete with matching labels.
+ pattern = cs_txt[i_label:]
+ states: List[RlStateDTLabel] = [
+ # "&" is retained within the completion state
+ # when it's present in the completion scope.
+ RlStateDTLabel(f"&{label}" if (i_label > 0) else label, node)
+ for label, node in sh.dt.labeled_nodes.items()
+ if label.startswith(pattern)
+ ]
+
+ if len(states) == 1:
+ # Exact match: convert completion state to path.
+ return [RlStateDTPath(states[0].node.path, states[0].node)]
+ return sorted(states)
+
+ @staticmethod
+ def complete_fspath(cs_txt: str) -> List[DTShReadline.CompleterState]:
+ """Complete file-system path.
+
+ Args:
+ cs_txt: The file-system path to complete.
+
+ Returns:
+ The file-system paths that are valid completer states.
+ """
+ if cs_txt.startswith("~"):
+ # We'll always expand "~" in both the auto-completion input
+ # and the completion states.
+ cs_txt = cs_txt.replace("~", os.path.expanduser("~"), 1)
+
+ dirname = os.path.dirname(cs_txt)
+ if dirname:
+ dirpath = os.path.abspath(dirname)
else:
- for completion in model:
- self._autocomp_state[str(completion)] = completion
+ dirpath = os.getcwd()
+
+ if not os.path.isdir(dirpath):
+ return []
+
+ basename = os.path.basename(cs_txt)
+ fs_entries = [
+ entry
+ for entry in os.scandir(dirpath)
+ if entry.name.startswith(basename)
+ ]
+ if _dtshconf.pref_fs_hide_dotted:
+ # Hide commonly hidden files and directories on POSIX-like systems.
+ fs_entries = [
+ entry for entry in fs_entries if not entry.name.startswith(".")
+ ]
+
+ fs_entries.sort(key=lambda entry: entry.name)
+
+ # Directories 1st.
+ states: List[DTShReadline.CompleterState] = [
+ # Append "/" to completion matches for directories.
+ RlStateFsEntry(
+ f"{os.path.join(dirname, entry.name)}{os.path.sep}", entry
+ )
+ for entry in fs_entries
+ if entry.is_dir()
+ ]
+ # Then files.
+ states.extend(
+ RlStateFsEntry(os.path.join(dirname, entry.name), entry)
+ for entry in fs_entries
+ if entry.is_file()
+ )
+
+ return states
+
+ def __init__(self, sh: DTSh) -> None:
+ """Initialize the auto-completion helper.
+
+ Args:
+ sh: The context shell.
+ """
+ self._dtsh = sh
+
+ def complete(
+ self, cs_txt: str, rlbuf: str, cs_begin: int, cs_end: int
+ ) -> List[DTShReadline.CompleterState]:
+ """Auto-complete a DTSh command line.
+
+ Derived classes may override this method to post-process
+ the completions, e.g.:
+ - append a space to completion matches, exiting the completion process
+ when there remains only a single match
+ - pretend there's no completion when there's a single match,
+ also to exit the completion process
+ - append "/" to matched directories when completing file system entries
+
+ Implements DTShReadline.CompletionCallback.
+
+ Args:
+ cs_txt: The input text to complete (aka the RL completion scope
+ content).
+ rlbuf: The readline input buffer content (aka the current content
+ of the command string).
+ begin: Beginning of completion scope, index of the first character
+ in the completion scope).
+ end: End of the completion scope, index of the character immediately
+ after the completion scope, such that (end - begin) is the
+ length of the completion scope. This is also the input caret
+ index wrt the RL buffer.
+
+ Returns:
+ The list of candidate completions.
+ """
+ if (cs_end < len(rlbuf)) and (rlbuf[cs_end] != " "):
+ # Some auto-completion providers (e.g. zsh) have an option
+ # to auto-complete the command line regardless of the caret position.
+ # This sometimes results in a confusing user experience
+ # when not completing past the end of a word.
+ # DTSh will always answer there's no completion on such situations.
+ return []
+
+ # Auto-completion is asking for possible command names
+ # when the command line before the completion scope
+ # contains only spaces.
+ ante_cs_txt = rlbuf[:cs_begin].strip()
+ if not ante_cs_txt:
+ return DTShAutocomp.complete_dtshcmd(cs_txt, self._dtsh)
+
+ try:
+ cmd, _, redir2 = self._dtsh.parse_cmdline(rlbuf)
+ except DTShCommandNotFoundError:
+ # Won't auto-complete a command line deemed to fail.
+ return []
+
+ if redir2:
+ # Auto-completion is asking for possible redirection file paths
+ # when we're past the last redirection operator.
+ if cs_begin > rlbuf.rfind(">"):
+ return DTShAutocomp.complete_fspath(cs_txt)
+
+ # At this point, auto-completion may be asking for:
+ # - either a command's option name
+ # - or a command's argument possible values
+ # - or a command's parameter possible values
+
+ if cs_txt.startswith("-"):
+ # We assume possible argument and parameter values
+ # won't start with "-".
+ return DTShAutocomp.complete_dtshopt(cs_txt, cmd)
+
+ # Auto-completion is asking for possible argument values
+ # when the completion scope immediately follows a command's
+ # argument name.
+ ante_opts = ante_cs_txt.split()
+ if ante_opts:
+ last_opt = cmd.option(ante_opts[-1])
+ if isinstance(last_opt, DTShArg):
+ return last_opt.autocomp(cs_txt, self._dtsh)
+
+ # Eventually, try to auto-complete with parameter values.
+ if cmd.param:
+ return cmd.param.autocomp(cs_txt, self._dtsh)
+
+ return []
+
+ def display(
+ self,
+ out: DTShOutput,
+ states: List[DTShReadline.CompleterState],
+ ) -> None:
+ """Default DTSh callback for displaying the completion candidates.
+
+ The completion model is typically produced by DtshAutocomp.complete().
+
+ Derived classes may override this method
+ to provide an alternative (aka rich) completion matches display.
+
+ Implements DTShReadline.DisplayCallback.
+
+ Args:
+ out: Where to display these completions.
+ completions: The completions to display.
+ """
+ for state in states:
+ if isinstance(state, RlStateDTShCommand):
+ cmd = state.cmd
+ out.write(f"{cmd.name} {cmd.brief}")
+
+ elif isinstance(state, RlStateDTShOption):
+ opt = state.opt
+ out.write(f"{opt.usage} {opt.brief}")
+
+ elif isinstance(state, RlStateDTPath):
+ node = state.node
+ out.write(node.name)
+
+ elif isinstance(state, RlStateCompatStr):
+ compat = state.compatstr
+ out.write(compat)
+
+ elif isinstance(state, RlStateDTVendor):
+ out.write(f"{state.prefix} {state.vendor}")
+
+ elif isinstance(state, RlStateDTBus):
+ proto = state.proto
+ out.write(proto)
+
+ elif isinstance(state, RlStateDTAlias):
+ out.write(state.alias)
+
+ elif isinstance(state, RlStateDTChosen):
+ out.write(state.chosen)
+
+ elif isinstance(state, RlStateFsEntry):
+ dirent = state.dirent
+ if dirent.is_dir():
+ out.write(f"{dirent.name}{os.path.sep}")
+ else:
+ out.write(dirent.name)
+
+ elif isinstance(state, RlStateEnum):
+ out.write(f"{state.value} {state.brief}")
+
+ else:
+ out.write(state.rlstr)
diff --git a/src/dtsh/builtin_alias.py b/src/dtsh/builtin_alias.py
deleted file mode 100644
index a9cfa16..0000000
--- a/src/dtsh/builtin_alias.py
+++ /dev/null
@@ -1,134 +0,0 @@
-# Copyright (c) 2022 Chris Duf
-#
-# SPDX-License-Identifier: Apache-2.0
-
-"""Built-in 'alias' command."""
-
-
-from typing import Tuple, List
-
-from rich.table import Table
-
-from dtsh.dtsh import Dtsh, DtshCommand, DtshAutocomp, DtshVt
-from dtsh.dtsh import DtshError, DtshCommandUsageError
-from dtsh.dtsh import DtshCommandFlagLongFmt
-from dtsh.tui import DtshTui
-
-
-class DtshBuiltinAlias(DtshCommand):
- """Print defined aliases.
-
-DESCRIPTION
-The `alias` command prints the aliases defined
-in the `/aliases` node of this devicetree.
-
-EXAMPLES
-
-```
-/
-❯ alias
-led0 → /leds/led_0
-led1 → /leds/led_1
-led2 → /leds/led_2
-led3 → /leds/led_3
-pwm-led0 → /pwmleds/pwm_led_0
-sw0 → /buttons/button_0
-sw1 → /buttons/button_1
-sw2 → /buttons/button_2
-sw3 → /buttons/button_3
-bootloader-led0 → /leds/led_0
-mcuboot-button0 → /buttons/button_0
-mcuboot-led0 → /leds/led_0
-watchdog0 → /soc/watchdog@40010000
-spi-flash0 → /soc/qspi@40029000/mx25r6435f@0
-
-/
-❯ alias watchdog0 -l
-watchdog0 → /soc/watchdog@40010000 nordic,nrf-wdt
-```
-"""
- def __init__(self, shell: Dtsh):
- super().__init__(
- 'alias',
- "print defined aliases",
- True,
- [
- DtshCommandFlagLongFmt(),
- ]
- )
- self._dtsh = shell
-
- @property
- def usage(self) -> str:
- """Overrides DtshCommand.usage().
- """
- return super().usage + ' [ALIAS]'
-
- def parse_argv(self, argv: List[str]) -> None:
- """Overrides DtshCommand.parse_argv().
- """
- super().parse_argv(argv)
- if len(self._params) > 1:
- raise DtshCommandUsageError(self, 'too many parameters')
-
- def execute(self, vt: DtshVt) -> None:
- """Implements DtshCommand.execute().
- """
- if self._params:
- arg_aliases = [self._params[0]]
- else:
- arg_aliases = list(self._dtsh.dt_aliases.keys())
-
- if self.with_pager:
- vt.pager_enter()
- if self.with_longfmt:
- grid = self._mk_grid_aliases_rich(arg_aliases)
- else:
- grid = self._mk_grid_aliases(arg_aliases)
- vt.write(grid)
- if self.with_pager:
- vt.pager_exit()
-
- def autocomplete_param(self, prefix: str) -> Tuple[int, List]:
- """Overrides DtshCommand.autocomplete_param().
- """
- completions: List[str] = []
- for alias in self._dtsh.dt_aliases:
- if alias.startswith(prefix) and (len(alias) > len(prefix)):
- completions.append(alias)
- return (DtshAutocomp.MODE_ANY, completions)
-
- def _mk_grid_aliases(self, arg_aliases: List[str]) -> Table:
- grid = DtshTui.mk_grid(3)
- for alias in arg_aliases:
- try:
- node = self._dtsh.dt_aliases[alias]
- except KeyError:
- raise DtshError(f'no such alias: {arg_aliases[0]}')
- txt_alias = DtshTui.mk_txt(alias)
- txt_arrow = DtshTui.mk_txt(DtshTui.WCHAR_ARROW)
- txt_path = DtshTui.mk_txt(node.path)
- if node.status != 'okay':
- DtshTui.txt_dim(txt_alias)
- DtshTui.txt_dim(txt_arrow)
- DtshTui.txt_dim(txt_path)
- grid.add_row(txt_alias, txt_arrow, txt_path)
- return grid
-
- def _mk_grid_aliases_rich(self, arg_aliases: List[str]) -> Table:
- grid = DtshTui.mk_grid(4)
- for alias in arg_aliases:
- try:
- node = self._dtsh.dt_aliases[alias]
- except KeyError:
- raise DtshError(f'no such alias: {arg_aliases[0]}')
- txt_alias = DtshTui.mk_txt(alias, DtshTui.style(DtshTui.STYLE_DT_ALIAS))
- txt_arrow = DtshTui.mk_txt(DtshTui.WCHAR_ARROW)
- txt_path = DtshTui.mk_txt_node_path(node.path)
- txt_binding = DtshTui.mk_txt_node_binding(node, True, True)
- if node.status != 'okay':
- DtshTui.txt_dim(txt_alias)
- DtshTui.txt_dim(txt_arrow)
- DtshTui.txt_dim(txt_path)
- grid.add_row(txt_alias, txt_arrow, txt_path, txt_binding)
- return grid
diff --git a/src/dtsh/builtin_cat.py b/src/dtsh/builtin_cat.py
deleted file mode 100644
index a1bb294..0000000
--- a/src/dtsh/builtin_cat.py
+++ /dev/null
@@ -1,131 +0,0 @@
-# Copyright (c) 2022 Chris Duf
-#
-# SPDX-License-Identifier: Apache-2.0
-
-"""Built-in 'cat' command."""
-
-
-from typing import Tuple, List
-
-from dtsh.dtsh import Dtsh, DtshError, DtshVt, DtshCommand, DtshAutocomp
-from dtsh.dtsh import DtshCommandUsageError
-from dtsh.tui import DtNodeView, DtPropertyView
-
-
-class DtshBuiltinCat(DtshCommand):
- """Concatenate and print devicetree content.
-
-DESCRIPTION
-The `cat` command will concatenate and print devicetree content at `PATH`.
-
-Think Linux `/proc` file systems, e.g. `cat /proc/cpuinfo`.
-
-`cat` supports the `$` character as a separator between a node's path and
-a property name: `PATH := [$]`
-
-Set the **--pager** option to page the command's output using the system pager.
-
-EXAMPLES
-```
-/
-❯ cat /soc/usbd@40027000
-Node
- Path: /soc/usbd@40027000
- Name: usbd
- Unit address: 0x40027000
- Compatible: nordic,nrf-usbd
- Status: okay
-
-Description
- Nordic nRF52 USB device controller
-
-
-Depends-on
- soc
- interrupt-controller@e000e100 arm,v7m-nvic
-
-Required-by
- There's no other node that directly depends on this
- node.
-
-[...]
-
-/
-❯ cat /soc/i2c@40003000$interrupts
-Property
- Name: interrupts
- Type: array
- Required: True
- Default:
- Value: [3, 1]
-
-Description
- interrupts for device
-```
-"""
-
- def __init__(self, shell: Dtsh) -> None:
- super().__init__(
- 'cat',
- 'concatenate and print devicetree content',
- True
- )
- self._dtsh = shell
-
- @property
- def usage(self) -> str:
- """Overrides DtshCommand.usage().
- """
- return super().usage + ' PATH'
-
- def parse_argv(self, argv: List[str]) -> None:
- """Overrides Dtsh.parse_argv().
- """
- super().parse_argv(argv)
- if len(self._params) == 0:
- raise DtshCommandUsageError(self, 'what do you want to cat?')
- if len(self._params) > 1:
- raise DtshCommandUsageError(self, 'too many parameters')
-
- def execute(self, vt: DtshVt) -> None:
- """Implements DtshCommand.execute().
- """
- if self._params:
- arg_path = self._dtsh.realpath(self._params[0])
- else:
- arg_path = self._dtsh.pwd
-
- i_prop = arg_path.rfind('$')
- if i_prop != -1:
- node = self._dtsh.path2node(arg_path[:i_prop])
- prop = node.props.get(arg_path[i_prop+1:])
- if prop is None:
- raise DtshError(f'no such property: {arg_path[i_prop+1:]}')
- view = DtPropertyView(prop)
- else:
- node = self._dtsh.path2node(arg_path)
- view = DtNodeView(node, self._dtsh)
-
- view.show(vt, self.with_pager)
-
- def autocomplete_param(self, prefix: str) -> Tuple[int, List]:
- """Overrides DtshCommand.autocomplete_param().
- """
- # := //.../
- # := [@]
- # may contain alphanum, and: , . _ + -
- #
- # Property names may additionaly contain ? and #.
- #
- # AFAICT, the $ does not appear in the DT Specifications,
- # we'll is it as separator between a node's path and a property name.
- i_prop = prefix.rfind('$')
-
- if i_prop != -1:
- comps = DtshAutocomp.autocomplete_with_properties(prefix[:i_prop],
- prefix[i_prop+1:],
- self._dtsh)
- return (DtshAutocomp.MODE_DT_PROP, comps)
-
- return (DtshAutocomp.MODE_DT_NODE,
- DtshAutocomp.autocomplete_with_nodes(prefix, self._dtsh))
diff --git a/src/dtsh/builtin_cd.py b/src/dtsh/builtin_cd.py
deleted file mode 100644
index 8068a5b..0000000
--- a/src/dtsh/builtin_cd.py
+++ /dev/null
@@ -1,68 +0,0 @@
-# Copyright (c) 2022 Chris Duf
-#
-# SPDX-License-Identifier: Apache-2.0
-
-"""Built-in 'cd' command."""
-
-
-from typing import List, Tuple
-from dtsh.dtsh import Dtsh, DtshVt, DtshCommand, DtshAutocomp
-from dtsh.dtsh import DtshCommandUsageError
-
-
-class DtshBuiltinCd(DtshCommand):
- """Change current working node.
-
-DESCRIPTION
- The `cd` command changes the shell current working node to `PATH`.
-
-If `PATH` is unspecified, `cd` will change the current working node
-to the devicetree's root.
-
-EXAMPLES
-```
-/
-❯ cd /soc/flash-controller@4001e000/
-
-/soc/flash-controller@4001e000
-❯ cd
-
-/
-❯
-```
-"""
- def __init__(self, shell: Dtsh) -> None:
- super().__init__(
- 'cd',
- 'change current working node'
- )
- self._dtsh = shell
-
- @property
- def usage(self) -> str:
- """Overrides DtshCommand.usage().
- """
- return super().usage + ' [PATH]'
-
- def parse_argv(self, argv: List[str]) -> None:
- """Overrides Dtsh.parse_argv().
- """
- super().parse_argv(argv)
- if len(self._params) > 1:
- raise DtshCommandUsageError(self, 'too many parameters')
-
- def execute(self, vt: DtshVt) -> None:
- """Implements DtshCommand.execute().
- """
- if self._params:
- arg_path = self._dtsh.realpath(self._params[0])
- else:
- arg_path = '/'
-
- self._dtsh.cd(arg_path)
-
- def autocomplete_param(self, prefix: str) -> Tuple[int, List]:
- """Overrides DtshCommand.autocomplete_param().
- """
- return (DtshAutocomp.MODE_DT_NODE,
- DtshAutocomp.autocomplete_with_nodes(prefix, self._dtsh))
diff --git a/src/dtsh/builtin_chosen.py b/src/dtsh/builtin_chosen.py
deleted file mode 100644
index a37049e..0000000
--- a/src/dtsh/builtin_chosen.py
+++ /dev/null
@@ -1,130 +0,0 @@
-# Copyright (c) 2022 Chris Duf
-#
-# SPDX-License-Identifier: Apache-2.0
-
-"""Built-in 'chosen' command."""
-
-
-from typing import List, Tuple
-
-from rich.table import Table
-
-from dtsh.dtsh import Dtsh, DtshCommand, DtshAutocomp, DtshVt
-from dtsh.dtsh import DtshError, DtshCommandUsageError
-from dtsh.dtsh import DtshCommandFlagLongFmt
-from dtsh.tui import DtshTui
-
-
-class DtshBuiltinChosen(DtshCommand):
- """Print chosen system configuration.
-
-DESCRIPTION
-The `chosen` command prints the /system configuration choices/
- defined in the `/chosen` node of this devicetree.
-
-EXAMPLES
-```
-/
-❯ chosen
-zephyr,entropy → /soc/random@4000d000
-zephyr,flash-controller → /soc/flash-controller@4001e000
-zephyr,console → /soc/uart@40002000
-zephyr,shell-uart → /soc/uart@40002000
-zephyr,uart-mcumgr → /soc/uart@40002000
-zephyr,bt-mon-uart → /soc/uart@40002000
-zephyr,bt-c2h-uart → /soc/uart@40002000
-zephyr,sram → /soc/memory@20000000
-zephyr,flash → /soc/flash-controller@4001e000/flash@0
-zephyr,code-partition → /soc/flash-controller@4001e000/flash@0/partitions/partition@c000
-zephyr,ieee802154 → /soc/radio@40001000/ieee802154
-
-/
-❯ chosen zephyr,entropy -l
-zephyr,entropy → /soc/random@4000d000 nordic,nrf-rng
-```
-"""
- def __init__(self, shell: Dtsh):
- super().__init__(
- 'chosen',
- "print chosen configuration",
- True,
- [
- DtshCommandFlagLongFmt(),
- ]
- )
- self._dtsh = shell
-
- @property
- def usage(self) -> str:
- """Overrides DtshCommand.usage().
- """
- return super().usage + ' [CHOICE]'
-
- def parse_argv(self, argv: List[str]) -> None:
- """Overrides DtshCommand.parse_argv().
- """
- super().parse_argv(argv)
- if len(self._params) > 1:
- raise DtshCommandUsageError(self, 'too many parameters')
-
- def execute(self, vt: DtshVt) -> None:
- """Implements DtshCommand.execute().
- """
- if self._params:
- arg_chosen = [self._params[0]]
- else:
- arg_chosen = list(self._dtsh.dt_chosen.keys())
-
- if self.with_pager:
- vt.pager_enter()
- if self.with_longfmt:
- grid = self._mk_grid_chosen_rich(arg_chosen)
- else:
- grid = self._mk_grid_chosen(arg_chosen)
- vt.write(grid)
- if self.with_pager:
- vt.pager_exit()
-
- def autocomplete_param(self, prefix: str) -> Tuple[int, List]:
- """Overrides DtshCommand.autocomplete_param().
- """
- completions: List[str] = []
- for choice in self._dtsh.dt_chosen:
- if choice.startswith(prefix) and (len(choice) > len(prefix)):
- completions.append(choice)
- return (DtshAutocomp.MODE_ANY, completions)
-
- def _mk_grid_chosen(self, arg_chosen: List[str]) -> Table:
- grid = DtshTui.mk_grid(3)
- for choice in arg_chosen:
- try:
- node = self._dtsh.dt_chosen[choice]
- except KeyError:
- raise DtshError(f'no such configuration choice: {arg_chosen[0]}')
- txt_choice = DtshTui.mk_txt(choice)
- txt_arrow = DtshTui.mk_txt(DtshTui.WCHAR_ARROW)
- txt_path = DtshTui.mk_txt(node.path)
- if node.status != 'okay':
- DtshTui.txt_dim(txt_choice)
- DtshTui.txt_dim(txt_arrow)
- DtshTui.txt_dim(txt_path)
- grid.add_row(txt_choice, txt_arrow, txt_path)
- return grid
-
- def _mk_grid_chosen_rich(self, arg_chosen: List[str]) -> Table:
- grid = DtshTui.mk_grid(4)
- for choice in arg_chosen:
- try:
- node = self._dtsh.dt_chosen[choice]
- except KeyError:
- raise DtshError(f'no such configuration choice: {arg_chosen[0]}')
- txt_choice = DtshTui.mk_txt(choice, DtshTui.style(DtshTui.STYLE_DT_ALIAS))
- txt_arrow = DtshTui.mk_txt(DtshTui.WCHAR_ARROW)
- txt_path = DtshTui.mk_txt_node_path(node.path)
- txt_binding = DtshTui.mk_txt_node_binding(node, True, True)
- if node.status != 'okay':
- DtshTui.txt_dim(txt_choice)
- DtshTui.txt_dim(txt_arrow)
- DtshTui.txt_dim(txt_path)
- grid.add_row(txt_choice, txt_arrow, txt_path, txt_binding)
- return grid
diff --git a/src/dtsh/builtin_find.py b/src/dtsh/builtin_find.py
deleted file mode 100644
index 822ec26..0000000
--- a/src/dtsh/builtin_find.py
+++ /dev/null
@@ -1,733 +0,0 @@
-# Copyright (c) 2022 Chris Duf
-#
-# SPDX-License-Identifier: Apache-2.0
-
-"""Built-in 'find' command."""
-
-
-import re
-
-from abc import abstractmethod
-from typing import Tuple, List, Union
-
-from devicetree.edtlib import Node
-
-from dtsh.dtsh import DtshCommand, DtshCommandOption, Dtsh, DtshAutocomp, DtshVt
-from dtsh.dtsh import DtshCommandFlagLongFmt, DtshCommandArgLongFmt
-from dtsh.dtsh import DtshCommandUsageError
-from dtsh.tui import LsNodeTable, DtshTui
-
-
-class FindCriterion(object):
- """Interface for search criteria.
- """
-
- # Plain text pattern (used only for plain text search, w/o wildcard)
- _pattern: str
-
- def __init__(self, pattern: str) -> None:
- """Initialize criterion.
-
- Arguments:
- pattern - the search pattern
- """
- self._pattern = pattern
-
- @abstractmethod
- def match(self, node: Node) -> bool:
- """Returns True if the node does match this criterion.
- """
-
-
-class DtshBuiltinFind(DtshCommand):
- """Find devicetree nodes.
-
-DESCRIPTION
-The `find` command will search `PATH` for devicetree nodes, where `PATH` is either:
-
-- an absolute path to a devicetree node, e.g. `/soc`
-- a relative path to a devicetree node, e.g. `soc`
-
-`PATH` supports simple path substitution:
-
-- a leading `.` is interpreted as the current working node
-- a leading `..` is interpreted as the current working node's parent
-
-If `PATH` is unspecified, `find` will search the current working node.
-
-The `find` command always search recursively.
-
-The search supports multiple criteria:
-
-- **--name**: match nodes by names
-- **--compat**: match nodes by compatible strings
-- **--bus**: match nodes by *bus device*
-- **--interrupt**: match nodes by interrupts
-- **--enabled-only**: search only enabled (aka *okay*) nodes
-
-Criteria are additive (logical `AND`), such that:
-
- find --bus * --interrupt *
-
-will match all bus devices that generate IRQs.
-
-Without any criterion, the `find` command will behave like its
-POSIX homonym: the search will match all nodes at `PATH`.
-
-Criteria are defined in the next sections.
-
-By default, `find` will only print the found node paths: use the **-l** option to
-enable a more detailed (aka *rich*) output.
-
-The **-f ** option allows to specify the visible columns with a
-format string.
-
-Valid column specifiers are:
-
- | Specifier | Format | DTSpec |
- |-----------|-------------------------------------------|---------|
- | `N` | The node name | 2.2.1 |
- | `a` | The unit-address | |
- | `n` | The node name with the address striped | |
- | `d` | The description from the node binding | |
- | `p` | The node path name | 2.2.3 |
- | `l` | The node 'label' property | |
- | `L` | All known labels for the node | |
- | `s` | The node 'status' property | 2.3.4 |
- | `c` | The 'compatible' property for the node | 2.3.1 |
- | `C` | The node binding (aka matched compatible) | |
- | `A` | The node aliases | |
- | `b` | The bus device information for the node | |
- | `r` | The node 'reg' property | 2.3.6 |
- | `i` | The interrupts generated by the node | 2.4.1.1 |
-
-Use the **-c** options to count the matched nodes.
-Add the *quiet* flag (**-q**) to only print the nodes count,
-not the nodes themselves.
-
-Set the **--pager** option to page the command's output using the system pager.
-
-**Search by name**
-
-Match nodes by the `node-name` component
-([DTSpec 2.2.1](https://devicetree-specification.readthedocs.io/en/stable/devicetree-basics.html#node-names)).
-
-If the search pattern does not contain any wildcard,
-defaults to plain text search.
-
-If the search pattern contains `*` wildcards,
-a regular expression type search is assumed
-where the wildcards are replaced by a character class appropriate for node names.
-
-Then:
-
-- `*` will match any node-name
-- `*` will match a node-name if it starts with ``
-- `*` will match a node-name if it ends with ``
-- `*` will match a node-name if it starts with ``
- and ends with ``
-
-**Search by compatible**
-
-Match nodes by `compatible` string
-([DTSpec 2.3.1](https://devicetree-specification.readthedocs.io/en/stable/devicetree-basics.html#compatible)).
-
-If the search pattern does not contain any wildcard,
-defaults to plain text search.
-
-If the search pattern contains `*` wildcards,
-a regular expression type search is assumed
-where the wildcards are replaced by a character class appropriate for
-compatible strings.
-
-Then:
-
-- `*` will match any compatible string
-- `*` will match a compatible string if it starts with ``
-- `*` will match a compatible string if it ends with ``
-- `*` will match a compatible string if it starts with
- `` and ends with ``
-
-**Search by bus device**
-
-Match nodes that provide or appear on a *bus device*, e.g. `i2c` or `spi`.
-
-If the search pattern does not contain any wildcard,
-defaults to plain text search.
-
-If the search pattern contains `*` wildcards,
-a regular expression type search is assumed
-where the wildcards are replaced by a character class appropriate for
-bus devices.
-
-Then:
-
-- `*` will match any bus device
-- `*` will match a bus device if it starts with ``
-- `*` will match a bus device if it ends with ``
-- `*` will match a bus device if it starts with ``
- and ends with ``
-
-**Search by IRQ**
-
-Match nodes by interrupt numbers or names.
-
-If the search pattern successfully converts to an integer,
-a search by IRQ number is assumed.
-
-If the conversion fails, a search by interrupt name is assumed.
-
-If the pattern equals to `*`, the search will match all interrupts: this permits
-to *find* all nodes that generate IRQs.
-
-If the search pattern does not contain any wildcard,
-defaults to plain text search.
-
-If the search pattern contains `*` wildcards,
-a regular expression type search is assumed
-where the wildcards are replaced by a character class appropriate for IRQ names.
-
-Then:
-
-- `*` will match any IRQ name
-- `*` will match an IRQ name if it starts with ``
-- `*` will match an IRQ name if it ends with ``
-- `*` will match an IRQ name if it starts with ``
- and ends with ``
-
-EXAMPLES
-
-1. All nodes
-
-Without any criterion, the search will match all devicetree nodes.
-
-Count the devicetree nodes:
-
-```
-/
-❯ find -cq
-
-132 nodes.
-```
-
-Dump the enabled nodes:
-
-```
-/
-❯ find -c --enabled-only
-/
-/chosen
-/aliases
-/soc
-/soc/interrupt-controller@e000e100
-/soc/ficr@10000000
-/soc/uicr@10001000
-...
-/buttons/button_3
-/connector
-/analog-connector
-
-119 nodes.
-```
-
-2. Find nodes by name
-
-To find nodes which name contains `gpio` (*plain text* search):
-
-```
-/
-❯ find --name gpio -l
-Path Aliases Labels
-─────────────────────────────────────
-/soc/gpiote@40006000 gpiote
-/soc/gpio@50000000 gpio0
-/soc/gpio@50000300 gpio1
-```
-
-To find nodes which name ends with `gpio` (RE search):
-
-```
-/
-❯ find --name *gpio -l
-Path Aliases Labels
-─────────────────────────────────────
-/soc/gpio@50000000 gpio0
-/soc/gpio@50000300 gpio1
-```
-
-3. Find nodes by compatible strings
-
-To find nodes that may involve a TWI *compatible* driver:
-
-```
-❯ find --compat twi -l
-Path Compatible Description
-──────────────────────────────────────────────────────────────────────
-/soc/i2c@40003000 nordic,nrf-twi Nordic nRF family TWI (TWI master)…
-/soc/i2c@40004000 nordic,nrf-twi Nordic nRF family TWI (TWI master)…
-```
-
-4. Find nodes by bus devices
-
-To find all enabled bus devices:
-
-```
-/
-❯ find --enabled-only --bus * -l
-Path Bus
-────────────────────────────────────────
-/soc/uart@40002000 uart
-/soc/i2c@40003000 i2c
-/soc/spi@40004000 spi
-/soc/usbd@40027000 usb
-/soc/uart@40028000 uart
-/soc/qspi@40029000 qspi
-/soc/qspi@40029000/mx25r6435f@0 on qspi
-/soc/spi@4002f000 spi
-```
-
-5. Find nodes by interrupts
-
-To find all nodes that generate IRQs:
-
-```
-❯ find --interrupt * -l
-Path Interrupts
-──────────────────────────────────────
-/soc/clock@40000000 IRQ_0/1
-/soc/power@40000000 IRQ_0/1
-/soc/radio@40001000 IRQ_1/1
-```
-
-To find nodes by interrupt number:
-
-```
-/
-❯ find --interrupt 28 -l
-Path Interrupts
-─────────────────────────────
-/soc/pwm@4001c000 IRQ_28/1
-```
-
-6. Custom search and *reporting*
-
-To dump all enabled bus devices that generate IRQs,
-configuring the table columns with the `-f` option:
-
-```
-/
-❯ find -c --enabled-only --bus * --interrupt * -f naibcd
-Name Address Interrupts Bus Compatible Description
-───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
-uart 0x40002000 IRQ_2/1 uart nordic,nrf-uarte Nordic nRF family UARTE (UART with EasyDMA)
-i2c 0x40003000 IRQ_3/1 i2c nordic,nrf-twi Nordic nRF family TWI (TWI master)…
-spi 0x40004000 IRQ_4/1 spi nordic,nrf-spi Nordic nRF family SPI (SPI master)
-usbd 0x40027000 IRQ_39/1 usb nordic,nrf-usbd Nordic nRF52 USB device controller
-uart 0x40028000 IRQ_40/1 uart nordic,nrf-uarte Nordic nRF family UARTE (UART with EasyDMA)
-qspi 0x40029000 IRQ_41/1 qspi nordic,nrf-qspi Properties defining the interface for the Nordic QSPI peripheral…
-spi 0x4002f000 IRQ_47/1 spi nordic,nrf-spim Nordic nRF family SPIM (SPI master with EasyDMA)
-
-7 nodes.
-```
-
-To keep this information *at-hand*,
-just rely on `dtsh` command output redirection, e.g:
-
-```
-/
-❯ find -c --enabled-only --bus * --interrupt * -f naibcd > interrupts.txt
-```
-"""
- # Search criteria.
- _criteria: List[FindCriterion]
-
- # Nodes matched during the last search.
- _found: List[Node]
-
- def __init__(self, shell: Dtsh) -> None:
- super().__init__(
- 'find',
- 'find devicetree nodes',
- True,
- [
- DtshCommandOption('find by name', None, 'name', 'pattern'),
- DtshCommandOption('find by compatible', None, 'compat', 'pattern'),
- DtshCommandOption('find by bus device', None, 'bus', 'pattern'),
- DtshCommandOption('find by interrupt', None, 'interrupt', 'pattern'),
- DtshCommandOption('search only enabled nodes', None, 'enabled-only', None),
- DtshCommandOption('print nodes count', 'c', None, None),
- DtshCommandOption('quiet, only print nodes count', 'q', None, None),
- DtshCommandFlagLongFmt(),
- DtshCommandArgLongFmt()
- ]
- )
- self._dtsh = shell
- self._criteria = []
- self._found = []
-
- @property
- def usage(self) -> str:
- """Overrides DtshCommand.usage().
- """
- return super().usage + ' [PATH]'
-
- @property
- def arg_pattern_name(self) -> Union[str, None]:
- return self.arg_value('--name')
-
- @property
- def arg_pattern_compat(self) -> Union[str, None]:
- return self.arg_value('--compat')
-
- @property
- def arg_pattern_bus(self) -> Union[str, None]:
- return self.arg_value('--bus')
-
- @property
- def arg_pattern_irq(self) -> Union[str, None]:
- return self.arg_value('--interrupt')
-
- @property
- def with_only_enabled(self) -> bool:
- return self.with_flag('--enabled-only')
-
- @property
- def with_node_count(self) -> bool:
- return self.with_flag('-c')
-
- @property
- def with_quiet(self) -> bool:
- return self.with_flag('-q')
-
- def reset(self) -> None:
- """Overrides DtshCommand.reset().
- """
- super().reset()
- self._found.clear()
- self._criteria.clear()
-
- def parse_argv(self, argv: List[str]) -> None:
- """Overrides DtshCommand.parse_argv().
- """
- super().parse_argv(argv)
- if len(self._params) > 1:
- raise DtshCommandUsageError(self, 'too many parameters')
- if self.arg_pattern_name:
- self._criteria.append(FindByNameCriterion(self.arg_pattern_name))
- if self.arg_pattern_compat:
- self._criteria.append(FindByCompatCriterion(self.arg_pattern_compat))
- if self.arg_pattern_bus:
- self._criteria.append(FindByBusCriterion(self.arg_pattern_bus))
- if self.arg_pattern_irq:
- self._criteria.append(FindByIrqCriterion(self.arg_pattern_irq))
-
- def execute(self, vt: DtshVt) -> None:
- """Implements DtshCommand.execute().
- """
- if self._params:
- arg_path = self._dtsh.realpath(self._params[0])
- else:
- arg_path = self._dtsh.pwd
-
- self._find_nodes(self._dtsh.path2node(arg_path))
- # Like the POSIX find command, does not touch stdout if no match.
- if len(self._found) > 0:
-
- if self.with_pager:
- vt.pager_enter()
-
- if not self.with_quiet:
- longfmt = self.arg_longfmt
- if self.with_longfmt and not longfmt:
- longfmt = self._mk_default_longmt()
-
- if longfmt:
- self._write_found_long(vt, longfmt)
- else:
- self._write_found_default(vt)
-
- if self.with_node_count:
- vt.write()
- vt.write(f"{len(self._found)} nodes.")
-
- if self.with_pager:
- vt.pager_exit()
-
- def autocomplete_param(self, prefix: str) -> Tuple[int, List]:
- """Overrides DtshCommand.autocomplete_param().
- """
- return (DtshAutocomp.MODE_DT_NODE,
- DtshAutocomp.autocomplete_with_nodes(prefix, self._dtsh))
-
- def _find_nodes(self, root: Node) -> None:
- if self.with_only_enabled and not Dtsh.is_node_enabled(root):
- return
- if self._match_criteria(root):
- self._found.append(root)
- for node in root.children.values():
- self._find_nodes(node)
-
- def _match_criteria(self, node: Node) -> bool:
- for criterion in self._criteria:
- if not criterion.match(node):
- return False
- return True
-
- def _write_found_default(self, vt: DtshVt) -> None:
- for node in self._found:
- vt.write(node.path)
-
- def _write_found_long(self, vt: DtshVt, longfmt: str) -> None:
- ls_table = LsNodeTable(self._dtsh, longfmt)
- for node in self._found:
- ls_table.add_node_row(node)
- vt.write(ls_table.as_view())
-
- def _mk_default_longmt(self) -> str:
- longfmt = 'p'
- if self.arg_pattern_name:
- longfmt += 'A'
- longfmt += 'L'
- if self.arg_pattern_irq:
- longfmt += 'i'
- if self.arg_pattern_bus:
- longfmt += 'b'
- if self.arg_pattern_compat:
- longfmt += 'c'
- longfmt += 'd'
- return longfmt
-
-class FindByNameCriterion(FindCriterion):
- """Find nodes by names.
-
- Match nodes by the node-name component (DTSpec 2.2.1).
-
- If the search pattern does not contain any wildcard,
- defaults to plain text search.
-
- If the search pattern contains '*' wildcards,
- a regular expression type search is assumed
- where '*' are replaced by the character class for node names
- (see CLASS_NODE_NAME).
-
- Then:
- - * will match any node-name
- - * will match a node-name if it starts with
- - * will match a node-name if it ends with
- - * will match a node-name if it starts with
- and ends with
- """
-
- # Character class for node names (DTSpec 2.2.1).
- CLASS_NODE_NAME = r'[\w,.+\-]*'
-
- # Pattern for regular expression type search, None for plain text search.
- _re: Union[re.Pattern, None]
-
- def __init__(self, pattern: str) -> None:
- """Initialize criterion.
-
- Arguments:
- pattern - the search pattern
- """
- super().__init__(pattern)
- self._re = None
- if pattern.find('*') != -1:
- pattern = pattern.replace('*', FindByNameCriterion.CLASS_NODE_NAME)
- re_expr = f'^{pattern}$'
- self._re = re.compile(re_expr)
-
- def match(self, node: Node) -> bool:
- name = DtshTui.get_node_nick(node)
- if self._re:
- # RE type search.
- return self._re.match(name) is not None
- # Plain text type search.
- return name.find(self._pattern) != -1
-
-
-class FindByCompatCriterion(FindCriterion):
- """Find nodes by compatible string.
-
- Match nodes by the 'compatible' property value (DTSpec 2.3.1).
-
- If the search pattern does not contain any wildcard,
- defaults to plain text search.
-
- If the search pattern contains '*' wildcards,
- a regular expression type search is assumed
- where '*' are replaced by the character class for 'compatible' values
- (see CLASS_COMPATIBLE).
-
- Then:
- - * will match any compatible string
- - * will match a compatible if it starts with
- - * will match a compatible if it ends with
- - * will match a compatible if it starts with
- and ends with
- """
-
- # Character class for 'compatible' property (DTSpec 2.3.1).
- CLASS_COMPATIBLE = r'[a-z0-9\-,]*'
-
- # Pattern for regular expression type search, None for plain text search.
- _re: Union[re.Pattern, None]
-
- def __init__(self, pattern: str) -> None:
- """Initialize criterion.
-
- Arguments:
- pattern - the search pattern
- """
- super().__init__(pattern)
- self._re = None
- if pattern.find('*') != -1:
- pattern = pattern.replace('*', FindByCompatCriterion.CLASS_COMPATIBLE)
- re_expr = f'^{pattern}$'
- self._re = re.compile(re_expr)
-
- def match(self, node: Node) -> bool:
- for compat in node.compats:
- if self._re:
- # RE type search.
- if self._re.match(compat) is not None:
- return True
- else:
- # Plain text type search.
- if compat.find(self._pattern) != -1:
- return True
- return False
-
-
-class FindByBusCriterion(FindCriterion):
- """Find nodes by bus device.
-
- If the search pattern does not contain any wildcard,
- defaults to plain text search.
-
- If the search pattern contains '*' wildcards,
- a regular expression type search is assumed
- where '*' are replaced by the character class for bus devices
- (see CLASS_BUS).
-
- Then:
- - * will match any bus device
- - * will match a bus device if it starts with
- - * will match a bus device if it ends with
- - * will match a bus device if it starts with
- and ends with
- """
-
- # Character class for bus name (alphanumeric ? lowercase ?).
- CLASS_BUS = r'[\w]*'
-
- # Pattern for regular expression type search, None for plain text search.
- _re: Union[re.Pattern, None]
-
- def __init__(self, pattern: str) -> None:
- """Initialize criterion.
-
- Arguments:
- pattern - the search pattern
- """
- super().__init__(pattern)
- self._re = None
- if pattern.find('*') != -1:
- pattern = pattern.replace('*', FindByBusCriterion.CLASS_BUS)
- re_expr = f'^{pattern}$'
- self._re = re.compile(re_expr)
-
- def match(self, node: Node) -> bool:
- for bus in node.buses:
- if self._re:
- if self._re.match(bus) is not None:
- return True
- else:
- if bus.find(self._pattern) != -1:
- return True
- for bus in node.on_buses:
- if self._re:
- if self._re.match(bus) is not None:
- return True
- else:
- if bus.find(self._pattern) != -1:
- return True
- return False
-
-
-class FindByIrqCriterion(FindCriterion):
- """Find nodes by interrupts.
-
- If the search pattern successfully converts to an integer,
- a search by IRQ number is assumed.
-
- If the conversion fails, a search by interrupt name is assumed.
-
- If the search pattern does not contain any wildcard,
- defaults to plain text search.
-
- If the search pattern contains '*' wildcards,
- a regular expression type search is assumed
- where '*' are replaced by the character class for IRQ names
- (see CLASS_IRQ_NAME).
-
- Then:
- - * will match any IRQ name
- - * will match an IRQ name if it starts with
- - * will match an IRQ name if it ends with
- - * will match an IRQ name if it starts with
- and ends with
- """
-
- # Character class for IRQ names (empiric ?).
- CLASS_IRQ_NAME = r'[\w\-]*'
-
- # Search by IRQ number, None for search by IRQ name.
- _irq_num: Union[int, None]
-
- # Search by IRQ name: pattern for regular expression type search,
- # None for plain text search.
- _re: Union[re.Pattern, None]
-
- def __init__(self, pattern: str) -> None:
- """Initialize criterion.
-
- Arguments:
- pattern - the search pattern
- """
- super().__init__(pattern)
- self._irq_num = None
- self._re = None
-
- # We do NOT actually configure the criterion filters when
- # the pattern equals to '*': this allows to use find to list all
- # interrupts, including interrupts without name.
- if pattern != '*':
- try:
- self._irq_num = int(pattern)
- except ValueError:
- if pattern.find('*') != -1:
- pattern = pattern.replace('*', FindByIrqCriterion.CLASS_IRQ_NAME)
- self._re = re.compile(f'^{pattern}$')
-
- def match(self, node: Node) -> bool:
- for irq in node.interrupts:
- if self._pattern == '*':
- # Allows find to list all interrupts with '--interrupt *'.
- return True
- if self._irq_num is not None:
- # Search by IRQ number
- if irq.data.get('irq') == self._irq_num:
- return True
- else:
- # Search by IRQ name
- if self._re is not None:
- # RE type search
- if irq.name and (self._re.match(irq.name) is not None):
- return True
- else:
- # Plain text search
- if irq.name:
- if irq.name.find(self._pattern) != -1:
- return True
- return False
diff --git a/src/dtsh/builtin_ls.py b/src/dtsh/builtin_ls.py
deleted file mode 100644
index 41598d4..0000000
--- a/src/dtsh/builtin_ls.py
+++ /dev/null
@@ -1,276 +0,0 @@
-# Copyright (c) 2022 Chris Duf
-#
-# SPDX-License-Identifier: Apache-2.0
-
-"""Built-in 'ls' command."""
-
-
-from typing import Tuple, Dict, List
-from devicetree.edtlib import Node
-
-from dtsh.dtsh import DtshCommand, DtshCommandOption, Dtsh, DtshAutocomp, DtshVt
-from dtsh.dtsh import DtshCommandFlagLongFmt, DtshCommandArgLongFmt
-from dtsh.dtsh import DtshCommandUsageError
-from dtsh.tui import DtNodeListView
-
-
-class DtshBuiltinLs(DtshCommand):
- """List devicetree nodes.
-
-DESCRIPTION
-The `ls` command will list devicetree nodes at `PATH`, which is either:
-
-- an absolute path to a devicetree node, e.g. `/soc`
-- a relative path to a devicetree node, e.g. `soc`
-- a glob pattern filtering devicetree nodes, e.g. `soc/uart*`
-
-`PATH` supports simple path substitution:
-
-- a leading `.` is interpreted as the current working node
-- a leading `..` is interpreted as the current working node's parent
-
-If `PATH` is unspecified, `ls` will list the current working node.
-
-By default, `ls` will enumerate the immediate children of the devicetree node(s)
-at `PATH`: use the **-R** option to enumerate the children recursively,
-the **-d** option to list the node itself without its children.
-
-By default, `ls` will only print the nodes path: use the **-l** option to
-enable a more detailed (aka *rich*) output.
-
-The **-f ** option allows to specify the visible columns with a
-format string.
-
-Valid column specifiers are:
-
- | Specifier | Format | DTSpec |
- |-----------|-------------------------------------------|---------|
- | `N` | The node name | 2.2.1 |
- | `a` | The unit-address | |
- | `n` | The node name with the address striped | |
- | `d` | The description from the node binding | |
- | `p` | The node path name | 2.2.3 |
- | `l` | The node 'label' property | |
- | `L` | All known labels for the node | |
- | `s` | The node 'status' property | 2.3.4 |
- | `c` | The 'compatible' property for the node | 2.3.1 |
- | `C` | The node binding (aka matched compatible) | |
- | `A` | The node aliases | |
- | `b` | The bus device information for the node | |
- | `r` | The node 'reg' property | 2.3.6 |
- | `i` | The interrupts generated by the node | 2.4.1.1 |
-
-By default, nodes should be sorted by ascending unit address: use the **-r**
-option to reverse the sort order.
-
-Set the **--pager** option to page the command's output using the system pager.
-
-EXAMPLES
-Assuming the current working node is the devicetree's root:
-
-1. default to `ls /`:
-
-```
-/
-❯ ls
-/chosen
-/aliases
-/soc
-/pin-controller
-/entropy_bt_hci
-/cpus
-/sw-pwm
-/leds
-/pwmleds
-/buttons
-/connector
-/analog-connector
-```
-
-2. same with rich output:
-
-```
-❯ ls -l
-/:
-Name Addr Labels Alias Compatible Description
-────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
-chosen
-aliases
-soc nordic,nRF52840-QIAA nordic,nRF52840 nordic,nRF52 simple-bus
-pin-controller pinctrl nordic,nrf-pinctrl The nRF pin controller is a singleton node responsible for controlling…
-entropy_bt_hci rng_hci zephyr,bt-hci-entropy Bluetooth module that uses Zephyr's Bluetooth Host Controller Interface as…
-cpus
-sw-pwm sw_pwm nordic,nrf-sw-pwm nRFx S/W PWM
-leds gpio-leds This allows you to define a group of LEDs. Each LED in the group is…
-pwmleds pwm-leds PWM LEDs parent node
-buttons gpio-keys GPIO KEYS parent node
-connector arduino_header arduino-header-r3 GPIO pins exposed on Arduino Uno (R3) headers…
-analog-connector arduino_adc arduino,uno-adc ADC channels exposed on Arduino Uno (R3) headers…
-```
-
-Globing:
-
-1. *for all* wild-card:
-
-```
-/
-❯ ls *
-/chosen:
-
-/aliases:
-
-/soc:
-/soc/interrupt-controller@e000e100
-/soc/timer@e000e010
-/soc/ficr@10000000
-/soc/uicr@10001000
-/soc/memory@20000000
-/soc/clock@40000000
-/soc/power@40000000
-/soc/radio@40001000
-/soc/uart@40002000
-/soc/i2c@40003000
-/soc/spi@40003000
-
-[...]
-
-/buttons:
-/buttons/button_0
-/buttons/button_1
-/buttons/button_2
-/buttons/button_3
-
-/connector:
-
-/analog-connector:
-```
-
-2. filter wild-card:
-
-```
-/
-❯ ls /soc/gpio* -ld
-Name Address Labels Aliases Compatible Description
-────────────────────────────────────────────────────────────────────────
-gpiote 0x40006000 gpiote nordic,nrf-gpiote NRF5 GPIOTE node
-gpio 0x50000000 gpio0 nordic,nrf-gpio NRF5 GPIO node
-gpio 0x50000300 gpio1 nordic,nrf-gpio NRF5 GPIO node
-````
-
-Set a format string to specify the visible columns:
-
-````
-/
-❯ ls -l --format pbi /soc/
-/soc:
-Path Bus Interrupts
-────────────────────────────────────────────────────
-/soc/interrupt-controller@e000e100
-/soc/timer@e000e010
-/soc/ficr@10000000
-/soc/uicr@10001000
-/soc/memory@20000000
-/soc/clock@40000000 IRQ_0/1
-/soc/power@40000000 IRQ_0/1
-/soc/radio@40001000 IRQ_1/1
-/soc/uart@40002000 uart IRQ_2/1
-/soc/i2c@40003000 i2c IRQ_3/1
-/soc/spi@40003000 spi IRQ_3/1
-/soc/i2c@40004000 i2c IRQ_4/1
-/soc/spi@40004000 spi IRQ_4/1
-/soc/nfct@40005
-```
-"""
- def __init__(self, shell: Dtsh) -> None:
- super().__init__(
- 'ls',
- 'list devicetree nodes',
- True,
- [
- DtshCommandOption('list node itself, not its content', 'd', None, None),
- DtshCommandOption('reverse order while sorting', 'r', None, None),
- DtshCommandOption('list node contents recursively', 'R', None, None),
- DtshCommandFlagLongFmt(),
- DtshCommandArgLongFmt()
- ]
- )
- self._dtsh = shell
-
- @property
- def usage(self) -> str:
- """Overrides DtshCommand.usage().
- """
- return super().usage + ' [PATH]'
-
- @property
- def with_no_content(self) -> bool:
- return self.with_flag('-d')
-
- @property
- def with_recursive(self) -> bool:
- return self.with_flag('-R')
-
- @property
- def with_reverse(self) -> bool:
- return self.with_flag('-r')
-
- def parse_argv(self, argv: List[str]) -> None:
- """Overrides DtshCommand.parse_argv().
- """
- super().parse_argv(argv)
- if len(self._params) > 1:
- raise DtshCommandUsageError(self, 'too many parameters')
-
- def execute(self, vt: DtshVt) -> None:
- """Implements DtshCommand.execute().
- """
- if self._params:
- arg_path = self._dtsh.realpath(self._params[0])
- else:
- arg_path = self._dtsh.pwd
-
- if arg_path.endswith('*'):
- # Globing.
- roots = self._dtsh.ls(arg_path)
- else:
- roots = [
- self._dtsh.path2node(arg_path)
- ]
-
- if self.with_reverse:
- roots.reverse()
-
- node_map: Dict[str, List[Node]] = {}
- for root in roots:
- if self.with_no_content:
- node_map[root.path] = []
- else:
- if self.with_recursive:
- self._follow_node_content(root, node_map)
- else:
- node_map[root.path] = self._dtsh.ls(root.path)
-
- if self.with_reverse:
- for _, contents in node_map.items():
- contents.reverse()
-
- view = DtNodeListView(node_map,
- self._dtsh,
- self.with_no_content,
- self.with_longfmt,
- self.arg_longfmt)
- view.show(vt, self.with_pager)
-
- def autocomplete_param(self, prefix: str) -> Tuple[int, List]:
- """Overrides DtshCommand.autocomplete_param().
- """
- return (DtshAutocomp.MODE_DT_NODE,
- DtshAutocomp.autocomplete_with_nodes(prefix, self._dtsh))
-
- def _follow_node_content(self,
- parent: Node,
- node_map: Dict[str, List[Node]]) -> None:
- node_map[parent.path] = []
- for _, node in parent.children.items():
- node_map[parent.path].append(node)
- self._follow_node_content(node, node_map)
diff --git a/src/dtsh/builtin_man.py b/src/dtsh/builtin_man.py
deleted file mode 100644
index bab37a7..0000000
--- a/src/dtsh/builtin_man.py
+++ /dev/null
@@ -1,149 +0,0 @@
-# Copyright (c) 2022 Chris Duf
-#
-# SPDX-License-Identifier: Apache-2.0
-
-"""Built-in 'man' command."""
-
-from typing import Tuple, List
-
-from devicetree.edtlib import Binding
-
-from dtsh.rl import readline
-from dtsh.dtsh import Dtsh, DtshCommand, DtshCommandOption, DtshAutocomp, DtshVt
-from dtsh.dtsh import DtshError, DtshCommandUsageError, DtshCommandFailedError
-from dtsh.man import DtshManPageBinding, DtshManPageBuiltin, DtshManPageDtsh
-
-
-class DtshBuiltinMan(DtshCommand):
- """Print current working node's path.
-
-DESCRIPTION
-The `man` command opens the *reference* manual page `PAGE`,
-where `PAGES`:
-
-- is either a devicetree shell built-in (e.g. `tree`)
-- or a [*compatible*](https://devicetree-specification.readthedocs.io/en/latest/chapter2-devicetree-basics.html#compatible)
- specification if the **--compat** option is set
-
-By default, `man` will page its output: use the **--no-pager** option to
-disable the pager.
-
-EXAMPLES
-To open the `ls` shell built-in's manual page:
-
-```
-/
-❯ man ls
-
-```
-
-To open a the manual page for a DT compatible (ARMv7-M NVIC):
-
-```
-/
-❯ man --compat arm,v7m-nvic
-
-```
-"""
- def __init__(self, shell: Dtsh):
- super().__init__(
- 'man',
- "open a manual page",
- # Won't support the --pager option, since enabled by default for
- # man pages (see --no-pager).
- False,
- [
- DtshCommandOption("page for a DT compatible", None, 'compat', None),
- DtshCommandOption('no pager', None, 'no-pager', None),
- ]
- )
- self._dtsh = shell
-
- @property
- def usage(self) -> str:
- """Overrides DtshCommand.usage().
- """
- return super().usage + ' [PAGE]'
-
- @property
- def with_compat(self) -> bool:
- return self.with_flag('--compat')
-
- @property
- def with_no_pager(self) -> bool:
- return self.with_flag('--no-pager')
-
- def parse_argv(self, argv: List[str]) -> None:
- """Overrides DtshCommand.parse_argv().
- """
- super().parse_argv(argv)
- if len(self._params) == 0:
- raise DtshCommandUsageError(self, 'what manual page do you want?')
- if len(self._params) > 1:
- raise DtshCommandUsageError(self, 'too many parameters')
-
- def execute(self, vt: DtshVt) -> None:
- """Implements DtshCommand.execute().
- """
- arg_page = self._params[0]
- man_page = None
-
- if self.with_compat:
- binding = self._dtsh.dt_bindings.get(arg_page)
- if binding:
- man_page = DtshManPageBinding(binding)
- else:
- builtin = self._dtsh.builtin(arg_page)
- if builtin:
- man_page = DtshManPageBuiltin(builtin)
-
- if (not man_page) and (arg_page == 'dtsh'):
- man_page = DtshManPageDtsh()
-
- if man_page is not None:
- man_page.show(vt, self.with_no_pager)
- else:
- raise DtshCommandFailedError(self, f'page not found: {arg_page}')
-
- def autocomplete_param(self, prefix: str) -> Tuple[int, List]:
- """Overrides DtshCommand.autocomplete_param().
- """
- # 1st, complete according to flags.
- cmdline = readline.get_line_buffer()
- cmdline_vstr = cmdline.split()
- if len(cmdline_vstr) > 1:
- argv = cmdline_vstr[1:]
- try:
- self.parse_argv(argv)
- except DtshError:
- # Dry parsing of incomplete command line.
- pass
- if self.with_compat:
- completions = self._autocomplete_dt_binding(prefix)
- if completions:
- return (DtshAutocomp.MODE_DT_BINDING, completions)
-
- # Then, try command name (default).
- completions = self._autocomplete_dtsh_cmd(prefix)
- if completions:
- return (DtshAutocomp.MODE_DTSH_CMD, completions)
-
- return (DtshAutocomp.MODE_ANY, [])
-
- def _autocomplete_dtsh_cmd(self, prefix: str) -> List[DtshCommand]:
- completions: List[DtshCommand] = []
- if prefix.find('/') == -1:
- for cmd in self._dtsh.builtins:
- if (not prefix) or (cmd.name.startswith(prefix) and (len(cmd.name) > len(prefix))):
- completions.append(cmd)
- return completions
-
- def _autocomplete_dt_binding(self, prefix: str) -> List[Binding]:
- completions: List[Binding] = []
- for compat, binding in self._dtsh.dt_bindings.items():
- if prefix:
- if compat.startswith(prefix) and (len(compat) > len(prefix)):
- completions.append(binding)
- else:
- completions.append(binding)
- return completions
diff --git a/src/dtsh/builtin_pwd.py b/src/dtsh/builtin_pwd.py
deleted file mode 100644
index bcbcbda..0000000
--- a/src/dtsh/builtin_pwd.py
+++ /dev/null
@@ -1,57 +0,0 @@
-# Copyright (c) 2022 Chris Duf
-#
-# SPDX-License-Identifier: Apache-2.0
-
-"""Built-in 'pwd' command."""
-
-
-from typing import List
-
-from dtsh.dtsh import Dtsh, DtshCommand, DtshVt
-from dtsh.dtsh import DtshCommandUsageError
-
-
-class DtshBuiltinPwd(DtshCommand):
- """Print current working node's path.
-
-DESCRIPTION
-The `pwd` command prints the current working node's path.
-
-The current working node's path is also part of the shell multi-line prompt.
-
-EXAMPLES
-
-```
-/
-❯ pwd
-/
-
-/
-❯ cd soc
-
-/soc
-❯ pwd
-/soc
-
-/soc
-❯
-```
-"""
- def __init__(self, shell: Dtsh):
- super().__init__(
- 'pwd',
- "print current working node's path"
- )
- self._dtsh = shell
-
- def parse_argv(self, argv: List[str]) -> None:
- """Overrides DtshCommand.parse_argv().
- """
- super().parse_argv(argv)
- if len(self._params) > 0:
- raise DtshCommandUsageError(self, 'too many parameters')
-
- def execute(self, stdout: DtshVt) -> None:
- """Implements DtshCommand.execute().
- """
- stdout.write(self._dtsh.pwd)
diff --git a/src/dtsh/builtin_tree.py b/src/dtsh/builtin_tree.py
deleted file mode 100644
index 363681e..0000000
--- a/src/dtsh/builtin_tree.py
+++ /dev/null
@@ -1,179 +0,0 @@
-# Copyright (c) 2022 Chris Duf
-#
-# SPDX-License-Identifier: Apache-2.0
-
-"""Built-in 'tree' command."""
-
-
-from typing import Tuple, List, Union
-
-from dtsh.dtsh import Dtsh, DtshVt, DtshCommand, DtshCommandOption, DtshAutocomp
-from dtsh.dtsh import DtshCommandFlagLongFmt
-from dtsh.dtsh import DtshCommandUsageError
-
-from dtsh.tui import DtNodeTreeView
-
-
-class DtshBuiltinTree(DtshCommand):
- """List devicetree nodes in tree-like format.
-
-DESCRIPTION
-The `tree` command list devicetree nodes at `PATH`, which is either:
-
-- an absolute path to a devicetree node, e.g. `/soc`
-- a relative path to a devicetree node, e.g. `soc`
-
-`PATH` supports simple path substitution:
-
-- a leading `.` is interpreted as the current working node
-- a leading `..` is interpreted as the current working node's parent
-
-If `PATH` is unspecified, `tree` will list the current working node.
-
-The `tree` command list nodes hierarchically as trees.
-
-By default, `tree` will recursively walk through all not `disabled` branches: use
-the **-L** option to set a maximum tree depth.
-
-By default, `tree` will only print the nodes path: use the **-l** option to
-enable a more detailed (aka *rich*) output.
-
-Set the **--pager** option to page the command's output using the system pager.
-
-EXAMPLES
-Assuming the current working node is the devicetree's root:
-
-1. default to `tree /`, unlimited depth:
-
-```
-/
-❯ tree
-/
-├── chosen
-├── aliases
-├── soc
-│ ├── interrupt-controller@e000e100
-│ ├── timer@e000e010
-│ ├── ficr@10000000
-│ ├── uicr@10001000
-│ ├── memory@20000000
-│ ├── clock@40000000
-│ ├── power@40000000
-
-[...]
-
-│ ├── acl@4001e000
-│ ├── flash-controller@4001e000
-│ │ └── flash@0
-│ │ └── partitions
-│ │ ├── partition@0
-│ │ ├── partition@c000
-│ │ ├── partition@73000
-│ │ ├── partition@da000
-│ │ └── partition@f8000
-
-[...]
-
-├── connector
-└── analog-connector
-```
-
-2. Example of rich output with a tree depth of 2:
-
-```
-/
-❯ tree -L 2 -l
-/
-├── chosen
-├── aliases
-├── soc
-│ ├── 0xe000e100 interrupt-controller ARMv7-M NVIC (Nested Vectored Interrupt Controller)
-│ ├── 0xe000e010 timer
-│ ├── 0x10000000 ficr Nordic FICR (Factory Information Configuration Registers)
-│ ├── 0x10001000 uicr Nordic UICR (User Information Configuration Registers)
-│ ├── 0x20000000 memory Generic on-chip SRAM description
-│ ├── 0x40000000 clock Nordic nRF clock control node
-│ ├── 0x40000000 power Nordic nRF power control node
-│ ├── 0x40001000 radio Nordic nRF family RADIO peripheral…
-│ ├── 0x40002000 uart Nordic nRF family UARTE (UART with EasyDMA)
-│ ├── 0x40003000 i2c Nordic nRF family TWI (TWI master)…
-│ ├── 0x40003000 spi Nordic nRF family SPI (SPI master)
-│ ├── 0x40004000 i2c Nordic nRF family TWI (TWI master)…
-
-[...]
-
-├── connector GPIO pins exposed on Arduino Uno (R3) headers…
-└── analog-connector ADC channels exposed on Arduino Uno (R3) headers…
-```
-"""
-
- # Maximum display depth, 0 to follow all non disabled nodes.
- _level: int
-
- def __init__(self, shell: Dtsh):
- super().__init__(
- 'tree',
- 'list devicetree nodes in tree-like format',
- True,
- [
- DtshCommandOption('max display depth of the tree', 'L', 'depth', 'level'),
- DtshCommandFlagLongFmt(),
- ]
- )
- self._dtsh = shell
- self._level = 0
-
- @property
- def usage(self) -> str:
- """Overrides DtshCommand.usage().
- """
- return super().usage + ' [PATH]'
-
- @property
- def arg_level(self) -> Union[str, None]:
- """Maximum display depth, 0 to follow all non disabled nodes.
- """
- return self.arg_value('-L')
-
- def reset(self) -> None:
- """Overrides DtshCommand.reset().
- """
- super().reset()
- self._level = 0
-
- def parse_argv(self, argv: List[str]) -> None:
- """Overrides DtshCommand.parse_argv().
- """
- super().parse_argv(argv)
- if len(self._params) > 1:
- raise DtshCommandUsageError(self, 'too many parameters')
- if self.arg_level is not None:
- try:
- self._level = int(self.arg_level)
- except ValueError:
- raise DtshCommandUsageError(
- self,
- f"'{self.arg_level}' is not a valid level"
- )
-
- def execute(self, vt: DtshVt) -> None:
- """Implements DtshCommand.execute().
- """
- if self._params:
- arg_path = self._dtsh.realpath(self._params[0])
- else:
- arg_path = self._dtsh.pwd
-
- root = self._dtsh.path2node(arg_path)
-
- view = DtNodeTreeView(root,
- self._dtsh,
- self._level,
- self.with_longfmt)
- view.show(vt, self.with_pager)
-
- def autocomplete_param(self, prefix: str) -> Tuple[int, List]:
- """Overrides DtshCommand.autocomplete_param().
- """
- return (DtshAutocomp.MODE_DT_NODE,
- DtshAutocomp.autocomplete_with_nodes(prefix, self._dtsh))
diff --git a/src/dtsh/builtin_uname.py b/src/dtsh/builtin_uname.py
deleted file mode 100644
index a8395fa..0000000
--- a/src/dtsh/builtin_uname.py
+++ /dev/null
@@ -1,436 +0,0 @@
-# Copyright (c) 2022 Chris Duf
-#
-# SPDX-License-Identifier: Apache-2.0
-
-"""Built-in 'uname' command."""
-
-
-from typing import List, Union
-
-import os
-
-from rich.table import Table
-from rich.text import Text
-
-from dtsh.dtsh import Dtsh, DtshCommand, DtshCommandOption, DtshUname, DtshVt
-from dtsh.dtsh import DtshCommandFlagLongFmt
-from dtsh.dtsh import DtshCommandUsageError
-from dtsh.systools import GitHub, YamlFile
-from dtsh.tui import DtshTui, DtshTuiBulletList, DtshTuiForm, DtshTuiMemo, DtshTuiYaml
-
-
-class DtshBuiltinUname(DtshCommand):
- """Print current working node's path.
-
-DESCRIPTION
-The `uname` command will print *system* information, including:
-
-- `kernel-version` (option **-v**): the Zephyr kernel version,
- e.g. `zephyr-3.1.0`; clicking the version string should open
- the corresponding release notes in the system default browser
-- `machine` (option **-m**): based on the content of the board binding file,
- e.g. `nrf52840dk_nrf52840.yaml`; links to the board's DTS file and
- its Zephyr documentation
- (if [supported](https://docs.zephyrproject.org/latest/boards/index.html))
- should also show up
-- `toolchain` (non *standard* option **-t**): build toolchain variant and
- version, based on the currently configured Zephyr command line developement
- environment (e.g. after sourcing `$ZEPHYR_BASE/zephyr-env.sh`);
- clicking the toolchain name or version should open related information
- in the system default browser
-
-Retrieving this information may involve environment variables (e.g. `ZEPHYR_BASE`
-or `ZEPHYR_TOOLCHAIN_VARIANT`), CMake cached variables, invoking `git` or GCC.
-
- | | Environment variables | CMake cache | git | GCC |
- |---------------+--------------------------+--------------+-----+-----|
- | Zephyr kernel | ZEPHYR_BASE | | x | |
- | Toolchain | ZEPHYR_TOOLCHAIN_VARIANT | | | |
- | | ZEPHYR_SDK_INSTALL_DIR | | x | |
- | | GNUARMEMB_TOOLCHAIN_PATH | | | x |
- | Board | BOARD | BOARD_DIR | | |
- | | | CACHED_BOARD | | |
-
-By default, `uname` will print brief system information: `kernel-version - machine`.
-
-The **-l** option will enable a more detailed (aka *rich*) output.
-
-To filter the printed information, explicitly set the **-v**, **-m** and
-**-t** options.
-
-Use the **-a** option to request all information.
-
-Set the **--pager** option to page the command's output using the system pager
-(only with **-l**).
-
-EXAMPLES
-Default brief system information:
-
-```
-/
-❯ uname
-Zephyr v3.1.0 - nrf52840dk_nrf52840
-```
-
-Filter detailed board (`machine`) information:
-
- /
- ❯ uname -tl
- BOARD
- Board directory: $ZEPHYR_BASE/boards/arm/nrf52840dk_nrf52840
- Name: nRF52840-DK-NRF52840 (Supported Boards)
- Board: nrf52840dk_nrf52840 (DTS)
-
- nrf52840dk_nrf52840.yaml
-
- identifier: nrf52840dk_nrf52840
- name: nRF52840-DK-NRF52840
- type: mcu
- arch: arm
- ram: 256
- flash: 1024
- toolchain:
- - zephyr
- - gnuarmemb
- - xtools
- supported:
- - adc
- - arduino_gpio
- - arduino_i2c
- - arduino_spi
- - ble
- - counter
- - gpio
- - i2c
- - i2s
- - ieee802154
- - pwm
- - spi
- - usb_cdc
- - usb_device
- - watchdog
- - netif:openthread
-
-Filter detailed toolchain information:
-
- /
- ❯ uname -tl
- TOOLCHAIN
- Path: /mnt/platform/zephyr-rtos/SDKs/zephyr-sdk-0.15.1
- Variant: Zephyr SDK
- Version: v0.15.1
-"""
- def __init__(self, shell: Dtsh):
- super().__init__(
- 'uname',
- "print system information",
- True,
- [
- DtshCommandOption('print Zephyr kernel version',
- 'v',
- 'kernel-version',
- None),
- DtshCommandOption('print board',
- 'm',
- 'machine',
- None),
- DtshCommandOption('print toolchain',
- 't',
- 'toolchain',
- None),
- DtshCommandOption("print all information",
- 'a',
- 'all',
- None),
- DtshCommandFlagLongFmt(),
- ]
- )
- self._dtsh = shell
-
- @property
- def with_kernel_version(self) -> bool:
- return self.with_flag('-v')
-
- @property
- def with_machine(self) -> bool:
- return self.with_flag('-m')
-
- @property
- def with_toolchain(self) -> bool:
- return self.with_flag('-t')
-
- @property
- def with_all(self) -> bool:
- return self.with_flag('-a')
-
- def parse_argv(self, argv: List[str]) -> None:
- """Overrides DtshCommand.parse_argv().
- """
- super().parse_argv(argv)
- if len(self._params) > 0:
- raise DtshCommandUsageError(self, 'too many parameters')
-
- def execute(self, vt: DtshVt) -> None:
- """Implements DtshCommand.execute().
- """
- if self.with_longfmt:
- self._uname_long(vt)
- else:
- self._uname_brief(vt)
-
- def _uname_brief(self, vt: DtshVt) -> None:
- msg = ""
- no_with = not (
- self.with_flag('-v')
- or self.with_flag('-m')
- or self.with_flag('-t')
- )
- if self.with_all or no_with or self.with_kernel_version:
- if self._dtsh.uname.zephyr_kernel_tags:
- msg += f"Zephyr {self._dtsh.uname.zephyr_kernel_tags[0]}"
- elif self._dtsh.uname.zephyr_kernel_rev:
- msg += f"Zephyr {self._dtsh.uname.zephyr_kernel_rev}"
- else:
- msg += "Unknown"
- if self.with_all or no_with or self.with_machine:
- if msg:
- msg += " - "
- if self._dtsh.uname.board:
- msg += f"{self._dtsh.uname.board}"
- else:
- msg += "Unknown"
- if self.with_all or self.with_toolchain:
- if msg:
- if self._dtsh.uname.zephyr_toolchain:
- msg += f" ({self._dtsh.uname.zephyr_toolchain})"
- else:
- msg += " (Unknown)"
- else:
- if self._dtsh.uname.zephyr_toolchain:
- msg += f"{self._dtsh.uname.zephyr_toolchain}"
- else:
- msg += "Unknown"
- vt.write(msg)
-
- def _uname_long(self, vt: DtshVt) -> None:
- view = DtshTuiMemo()
- # When no explicit choice, and long format,
- # we'll show all available info.
- def_all = not (self.with_flag('-v')
- or self.with_flag('-m')
- or self.with_flag('-t'))
-
- if self.with_all or def_all or self.with_kernel_version:
- view.add_entry("zephyr kernel", self._mk_layout_zephyr_kernel())
-
- if self.with_all or def_all or self.with_toolchain:
- if self._dtsh.uname.zephyr_toolchain:
- content = ZephyrToolchainForm(self._dtsh.uname).as_renderable()
- else:
- content = None
- view.add_entry("toolchain", content)
-
- if self.with_all or def_all or self.with_machine:
- view.add_entry("board", self._mk_layout_board())
-
- view.show(vt, self.with_pager)
-
- def _mk_layout_zephyr_kernel(self) -> Union[Table, None]:
- if self._dtsh.uname.zephyr_base:
- layout = DtshTui.mk_grid(1)
- layout.add_row(ZephyrKernelForm(self._dtsh.uname).as_renderable())
- layout.add_row()
- if self._dtsh.uname.dt_binding_dirs:
- r_list = DtshTuiBulletList("Bindings search path:")
- for path in self._dtsh.uname.dt_binding_dirs:
- if path.startswith(self._dtsh.uname.zephyr_base):
- path = path.replace(self._dtsh.uname.zephyr_base, "$ZEPHYR_BASE")
- r_list.add_item(path)
- layout.add_row(r_list.as_renderable())
- else:
- r_warn = DtshTui.mk_txt_warn("Empty bindings search path !")
- layout.add_row(r_warn)
- return layout
- return None
-
- def _mk_layout_board(self) -> Union[Table, None]:
- if self._dtsh.uname.board:
- layout = DtshTui.mk_grid(1)
- layout.add_row(ZephyrBoardForm(self._dtsh.uname).as_renderable())
- if self._dtsh.uname.board_binding_file:
- if os.path.isfile(self._dtsh.uname.board_binding_file):
- w_yaml = DtshTuiYaml(self._dtsh.uname.board_binding_file,
- with_title=True)
- layout.add_row()
- layout.add_row(w_yaml.as_renderable())
- return layout
- return None
-
-
-class ZephyrKernelForm(DtshTuiForm):
- """Simple form for Zephyr's path, revision and tags.
- """
-
- def __init__(self, uname: DtshUname) -> None:
- """Initialize the form.
-
- Arguments:
- uname -- dtsh system-like information
- """
- super().__init__()
- if not uname.zephyr_base:
- # We won't get far without ZEPHYR_BASE.
- return
- gh = GitHub()
- self.add_field('Path', uname.zephyr_base)
- if uname.zephyr_kernel_tags:
- # Tags or version.
- version = uname.zephyr_kernel_version
- if version:
- r_version = DtshTui.mk_txt_link(
- version,
- gh.get_tag(version),
- style='dtsh.zephyr'
- )
- tags = uname.zephyr_kernel_tags.copy()
- tags.remove(version)
- if tags:
- r_tags = DtshTui.mk_txt(f" ({', '.join(tags)})")
- r_version.append_text(r_tags)
- self.add_field_rich("Version", r_version)
- else:
- self.add_field("Tags", ', '.join(uname.zephyr_kernel_tags))
- if uname.zephyr_kernel_rev:
- r_revision = DtshTui.mk_txt_link(
- uname.zephyr_kernel_rev,
- gh.get_commit(uname.zephyr_kernel_rev),
- style='default' if uname.zephyr_kernel_version else 'dtsh.commit'
- )
- else:
- # Show revision field even when information is unavailable.
- r_revision = DtshTui.mk_txt_dim("Unknown")
- self.add_field_rich("Revision", r_revision)
-
-
-class ZephyrToolchainForm(DtshTuiForm):
- """Simple form for Zephyr's path, revision and tags.
- """
-
- def __init__(self, uname: DtshUname) -> None:
- """Initialize the form.
-
- Requires: zephyr_toolchain
- Optional:
- - zephyr_sdk_version, zephyr_sdk_dir
- or
- - gnuarm_version, gnuarm_dir
-
- Arguments:
- uname -- dtsh system-like information
- """
- super().__init__()
- if not uname.zephyr_toolchain:
- # We won't get far if we don't know the toolchain variant.
- return
-
- r_version = None
- if uname.zephyr_toolchain == 'zephyr':
- # Zephyr SDK toolchain.
- self.add_field('Path', uname.zephyr_sdk_dir)
- r_variant = DtshTui.mk_txt_link(
- "Zephyr SDK",
- "https://docs.zephyrproject.org/latest/develop/toolchains/zephyr_sdk.html",
- style='dtsh.zephyr'
- )
- if uname.zephyr_sdk_version:
- sdk_version = f"v{uname.zephyr_sdk_version}"
- r_version = DtshTui.mk_txt_link(
- sdk_version,
- f"https://github.com/zephyrproject-rtos/sdk-ng/releases/tag/{sdk_version}",
- style='dtsh.zephyr'
- )
- else:
- r_version = DtshTui.mk_txt_dim("Unknown")
-
- elif uname.zephyr_toolchain == 'gnuarmemb':
- # GNU Arm Embedded toolchain.
- self.add_field('Path', uname.gnuarm_dir)
- r_variant = DtshTui.mk_txt_link(
- "GNU Arm Embedded",
- "https://developer.arm.com/Tools%20and%20Software/GNU%20Toolchain",
- style='dtsh.gnuarmemb'
- )
- if uname.gnuarm_version:
- r_version = DtshTui.mk_txt(uname.gnuarm_version)
- else:
- r_version = DtshTui.mk_txt_dim("Unknown")
- else:
- r_variant = DtshTui.mk_txt_dim("Unknown")
-
- self.add_field_rich("Variant", r_variant)
- if r_version:
- self.add_field_rich("Version", r_version)
-
-
-class ZephyrBoardForm(DtshTuiForm):
- """Simple form for Zephyr's path, revision and tags.
- """
-
- def __init__(self, uname: DtshUname) -> None:
- """Initialize the form.
-
- Requires: uname.board
- Optional: uname.bord_dir
-
- Arguments:
- uname -- dtsh system-like information
- """
- super().__init__()
- if not uname.board_dir:
- # We won't get far if without at least the directory.
- return
-
- board_dir = uname.board_dir
- if uname.zephyr_base and board_dir.startswith(uname.zephyr_base):
- board_dir = board_dir.replace(uname.zephyr_base, "$ZEPHYR_BASE")
- self.add_field("Board directory", board_dir)
-
- # Remaining fields depend on the YAML file content.
- if not uname.board_binding_file:
- return
- # Remaining fields depend on BOARD (e.g. nrf52840dk_nrf52840).
- if not uname.board:
- return
-
- yaml_file = YamlFile(uname.board_binding_file)
-
- r_name = None
- board_name = yaml_file.get('name')
- if board_name:
- r_name = DtshTui.mk_txt_bold(board_name)
- if uname.zephyr_base and uname.board_dir.startswith(uname.zephyr_base):
- # We then assume it's a Zephyr board with online doc.
- arch = yaml_file.get('arch')
- if arch:
- url = f'https://docs.zephyrproject.org/latest/boards/{arch}/{uname.board}/doc/index.html'
- r_www = DtshTui.mk_txt_link(
- "Supported Boards",
- url,
- style='dtsh.zephyr'
- )
- r_name = Text().append_text(r_name)
- r_name.append_text(DtshTui.mk_txt(' ('))
- r_name.append_text(r_www)
- r_name.append_text(DtshTui.mk_txt(')'))
- else:
- r_name = DtshTui.mk_txt_dim("Unknown")
- self.add_field_rich("Name", r_name)
-
- r_board = DtshTui.mk_txt(uname.board, style='dtsh.board')
- if uname.board_dts_file:
- r_dts = DtshTui.mk_txt("DTS")
- DtshTui.txt_update_link_file(r_dts, uname.board_dts_file)
- r_board.append_text(DtshTui.mk_txt(' ('))
- r_board.append_text(r_dts)
- r_board.append_text(DtshTui.mk_txt(')'))
- self.add_field_rich("Board", r_board)
diff --git a/src/dtsh/builtins/__init__.py b/src/dtsh/builtins/__init__.py
new file mode 100644
index 0000000..7799b57
--- /dev/null
+++ b/src/dtsh/builtins/__init__.py
@@ -0,0 +1,5 @@
+# Copyright (c) 2023 Christophe Dufaza
+#
+# SPDX-License-Identifier: Apache-2.0
+
+"""Built-in devicetree shell commands."""
diff --git a/src/dtsh/builtins/alias.py b/src/dtsh/builtins/alias.py
new file mode 100644
index 0000000..e27eae0
--- /dev/null
+++ b/src/dtsh/builtins/alias.py
@@ -0,0 +1,73 @@
+# Copyright (c) 2023 Christophe Dufaza
+#
+# SPDX-License-Identifier: Apache-2.0
+
+"""Devicetree shell built-in "aliases".
+
+List aliased nodes.
+
+Unit tests and examples: tests/test_dtsh_builtin_alias.py
+"""
+
+
+from typing import Sequence, Mapping
+
+from dtsh.model import DTNode
+from dtsh.io import DTShOutput
+from dtsh.shell import DTSh
+from dtsh.shellutils import DTShFlagEnabledOnly, DTShParamAlias
+
+from dtsh.rich.shellutils import DTShCommandLongFmt
+from dtsh.rich.modelview import ViewNodeAkaList
+
+
+class DTShBuiltinAlias(DTShCommandLongFmt):
+ """Devicetree shell built-in 'alias'."""
+
+ def __init__(self) -> None:
+ super().__init__(
+ "alias",
+ "list aliased nodes",
+ [
+ DTShFlagEnabledOnly(),
+ ],
+ DTShParamAlias(),
+ )
+
+ def execute(self, argv: Sequence[str], sh: DTSh, out: DTShOutput) -> None:
+ """Overrides DTShCommand.execute()."""
+ super().execute(argv, sh, out)
+
+ param_alias = self.with_param(DTShParamAlias).alias
+
+ alias2node: Mapping[str, DTNode]
+ if param_alias:
+ # Aliased nodes that match the alias parameter.
+ alias2node = {
+ alias: node
+ for alias, node in sh.dt.aliased_nodes.items()
+ if alias.find(param_alias) != -1
+ }
+ else:
+ # All aliased nodes.
+ alias2node = sh.dt.aliased_nodes
+
+ if self.with_flag(DTShFlagEnabledOnly):
+ # Filter out aliased nodes which are disabled.
+ alias2node = {
+ alias: node
+ for alias, node in alias2node.items()
+ if node.enabled
+ }
+
+ # Silently output nothing if no matched aliased nodes.
+ if alias2node:
+ if self.has_longfmt:
+ # Format output (unordered list view).
+ # Default format string: "Path", "Binding".
+ view = ViewNodeAkaList(alias2node, self.get_longfmt("pC"))
+ out.write(view)
+ else:
+ # POSIX-like symlinks (link -> file).
+ for alias, node in alias2node.items():
+ out.write(f"{alias} -> {node.path}")
diff --git a/src/dtsh/builtins/cd.py b/src/dtsh/builtins/cd.py
new file mode 100644
index 0000000..09fece9
--- /dev/null
+++ b/src/dtsh/builtins/cd.py
@@ -0,0 +1,39 @@
+# Copyright (c) 2023 Christophe Dufaza
+#
+# SPDX-License-Identifier: Apache-2.0
+
+"""Devicetree shell built-in "cd".
+
+Change the current working branch.
+
+Unit tests and examples: tests/test_dtsh_builtin_cd.py
+"""
+
+
+from typing import Sequence
+
+from dtsh.io import DTShOutput
+from dtsh.shell import DTSh, DTShCommand, DTPathNotFoundError, DTShCommandError
+from dtsh.shellutils import DTShParamDTPath
+
+
+class DTShBuiltinCd(DTShCommand):
+ """Devicetree shell built-in "cd"."""
+
+ def __init__(self) -> None:
+ super().__init__(
+ "cd",
+ "change the current working branch",
+ [],
+ DTShParamDTPath(),
+ )
+
+ def execute(self, argv: Sequence[str], sh: DTSh, out: DTShOutput) -> None:
+ """Overrides DTShCommand.execute()."""
+ super().execute(argv, sh, out)
+
+ param_path = self.with_param(DTShParamDTPath).path
+ try:
+ sh.cd(param_path)
+ except DTPathNotFoundError as e:
+ raise DTShCommandError(self, e.msg) from e
diff --git a/src/dtsh/builtins/chosen.py b/src/dtsh/builtins/chosen.py
new file mode 100644
index 0000000..43d54ff
--- /dev/null
+++ b/src/dtsh/builtins/chosen.py
@@ -0,0 +1,73 @@
+# Copyright (c) 2023 Christophe Dufaza
+#
+# SPDX-License-Identifier: Apache-2.0
+
+"""Devicetree shell built-in "chosen".
+
+List chosen nodes.
+
+Unit tests and examples: tests/test_dtsh_builtin_chosen.py
+"""
+
+
+from typing import Sequence, Mapping
+
+from dtsh.model import DTNode
+from dtsh.io import DTShOutput
+from dtsh.shell import DTSh
+from dtsh.shellutils import DTShFlagEnabledOnly, DTShParamChosen
+
+from dtsh.rich.shellutils import DTShCommandLongFmt
+from dtsh.rich.modelview import ViewNodeAkaList
+
+
+class DTShBuiltinChosen(DTShCommandLongFmt):
+ """Devicetree shell built-in "chosen"."""
+
+ def __init__(self) -> None:
+ super().__init__(
+ "chosen",
+ "list chosen nodes",
+ [
+ DTShFlagEnabledOnly(),
+ ],
+ DTShParamChosen(),
+ )
+
+ def execute(self, argv: Sequence[str], sh: DTSh, out: DTShOutput) -> None:
+ """Overrides DTShCommand.execute()."""
+ super().execute(argv, sh, out)
+
+ param_chosen = self.with_param(DTShParamChosen).chosen
+
+ chosen2node: Mapping[str, DTNode]
+ if param_chosen:
+ # Chosen nodes that match the chosen parameter.
+ chosen2node = {
+ chosen: node
+ for chosen, node in sh.dt.chosen_nodes.items()
+ if chosen.find(param_chosen) != -1
+ }
+ else:
+ # All chosen nodes.
+ chosen2node = sh.dt.chosen_nodes
+
+ if self.with_flag(DTShFlagEnabledOnly):
+ # Filter out chosen nodes which are disabled.
+ chosen2node = {
+ chosen: node
+ for chosen, node in chosen2node.items()
+ if node.enabled
+ }
+
+ # Silently output nothing if no matched chosen nodes.
+ if chosen2node:
+ if self.has_longfmt:
+ # Format output (unordered list view).
+ # Default format string: "Path", "Binding".
+ view = ViewNodeAkaList(chosen2node, self.get_longfmt("NC"))
+ out.write(view)
+ else:
+ # POSIX-like symlinks (link -> file).
+ for choice, node in chosen2node.items():
+ out.write(f"{choice} -> {node.path}")
diff --git a/src/dtsh/builtins/find.py b/src/dtsh/builtins/find.py
new file mode 100644
index 0000000..b7b68e4
--- /dev/null
+++ b/src/dtsh/builtins/find.py
@@ -0,0 +1,297 @@
+# Copyright (c) 2023 Christophe Dufaza
+#
+# SPDX-License-Identifier: Apache-2.0
+
+"""Devicetree shell built-in "find".
+
+Search for nodes with multiple criteria.
+
+Unit tests and examples: tests/test_dtsh_builtin_find.py
+"""
+
+
+from typing import List, Sequence, Dict, Mapping, Tuple
+
+from dtsh.model import DTWalkable, DTNode, DTNodeCriterion, DTNodeCriteria
+from dtsh.modelutils import DTWalkableComb
+from dtsh.io import DTShOutput
+from dtsh.shell import DTSh, DTShError, DTShCommandError
+from dtsh.shellutils import (
+ DTShFlagReverse,
+ DTShFlagEnabledOnly,
+ DTShFlagPager,
+ DTShFlagRegex,
+ DTShFlagIgnoreCase,
+ DTShFlagCount,
+ DTShFlagTreeLike,
+ DTShFlagLogicalOr,
+ DTShFlagLogicalNot,
+ DTShArgOrderBy,
+ DTShArgCriterion,
+ DTSH_ARG_NODE_CRITERIA,
+ DTShParamDTPaths,
+)
+
+from dtsh.rich.shellutils import DTShCommandLongFmt
+from dtsh.rich.modelview import (
+ SketchMV,
+ ViewNodeList,
+ ViewNodeTreePOSIX,
+ ViewNodeTwoSided,
+)
+from dtsh.rich.text import TextUtil
+
+
+class DTShBuiltinFind(DTShCommandLongFmt):
+ """Devicetree shell built-in "find"."""
+
+ def __init__(self) -> None:
+ super().__init__(
+ "find",
+ "search branches for nodes",
+ [
+ DTShFlagLogicalOr(),
+ DTShFlagLogicalNot(),
+ DTShFlagRegex(),
+ DTShFlagIgnoreCase(),
+ DTShFlagEnabledOnly(),
+ DTShFlagCount(),
+ DTShFlagReverse(),
+ DTShFlagTreeLike(),
+ DTShFlagPager(),
+ DTShArgOrderBy(),
+ *DTSH_ARG_NODE_CRITERIA,
+ ],
+ DTShParamDTPaths(),
+ )
+
+ def execute(self, argv: Sequence[str], sh: DTSh, out: DTShOutput) -> None:
+ """Overrides DTShCommand.execute()."""
+ super().execute(argv, sh, out)
+
+ # Expand path parameter.
+ path_expansions: Sequence[DTSh.PathExpansion] = self.with_param(
+ DTShParamDTPaths
+ ).expand(self, sh)
+
+ if self.with_flag(DTShFlagPager):
+ out.pager_enter()
+
+ # What to do here depends on the appropriate model kind:
+ # - either a simple list of found nodes
+ # - or a virtual subtree defined with the found nodes as its leaves
+ if self.with_flag(DTShFlagTreeLike):
+ self._find_tree(path_expansions, sh, out)
+ else:
+ self._find_nodes(path_expansions, sh, out)
+
+ if self.with_flag(DTShFlagPager):
+ out.pager_exit()
+
+ def _find_nodes(
+ self,
+ path_expansions: Sequence[DTSh.PathExpansion],
+ sh: DTSh,
+ out: DTShOutput,
+ ) -> None:
+ # Get model, mapping pathways to the nodes found there.
+ path2node: Mapping[str, DTNode] = self._get_path2node(
+ path_expansions, sh
+ )
+ if not path2node:
+ return
+ count = len(path2node)
+
+ if self.has_longfmt:
+ # Formatted output (list view).
+ self._output_nodes_longfmt(path2node, count, out)
+ else:
+ # POSIX-like.
+ self._output_nodes_raw(path2node, count, out)
+
+ def _get_path2node(
+ self,
+ path_expansions: Sequence[DTSh.PathExpansion],
+ sh: DTSh,
+ ) -> Mapping[str, DTNode]:
+ # Collect criterion chain.
+ criteria = self._get_criteria()
+
+ path2node: Dict[str, DTNode] = {}
+ for expansion in path_expansions:
+ for branch in self.sort(expansion.nodes):
+ for node in branch.find(
+ criteria,
+ order_by=self.arg_sorter,
+ reverse=self.flag_reverse,
+ enabled_only=self.flag_enabled_only,
+ ):
+ path = sh.pathway(node, expansion.prefix)
+ path2node[path] = node
+
+ return path2node
+
+ def _output_nodes_raw(
+ self, path2node: Mapping[str, DTNode], count: int, out: DTShOutput
+ ) -> None:
+ # Output paths of found nodes.
+ for path in path2node:
+ out.write(path)
+ if self.with_flag(DTShFlagCount):
+ out.write()
+ self._output_count_raw(count, out)
+
+ def _output_nodes_longfmt(
+ self, path2node: Mapping[str, DTNode], count: int, out: DTShOutput
+ ) -> None:
+ # Output found nodes as formatted list.
+ sketch = self.get_sketch(SketchMV.Layout.LIST_VIEW)
+ cols = self.get_longfmt(sketch.default_fmt)
+ view = ViewNodeList(cols, sketch)
+ view.extend(path2node.values())
+ out.write(view)
+ if self.with_flag(DTShFlagCount):
+ out.write()
+ self._output_count_longftm(count, out)
+
+ def _find_tree(
+ self,
+ path_expansions: Sequence[DTSh.PathExpansion],
+ sh: DTSh,
+ out: DTShOutput,
+ ) -> None:
+ # Get model, mapping pathways to a subtree containing
+ # the nodes found there as its leaves.
+ count: int
+ path2walkable: Mapping[str, DTWalkable]
+ count, path2walkable = self._get_path2walkable(path_expansions, sh)
+ if not path2walkable:
+ return
+
+ if self.has_longfmt:
+ # Formatted output.
+ self._output_treelike_longfmt(path2walkable, count, out)
+ else:
+ # POSIX-like.
+ self._output_treelike_raw(path2walkable, count, out)
+
+ def _output_treelike_raw(
+ self,
+ path2walkable: Mapping[str, DTWalkable],
+ count: int,
+ out: DTShOutput,
+ ) -> None:
+ # Tree-like views (POSIX output).
+ N = len(path2walkable)
+ for i, (path, comb) in enumerate(path2walkable.items()):
+ view = ViewNodeTreePOSIX(path, comb)
+ view.do_layout(
+ self.arg_sorter,
+ self.flag_reverse,
+ self.flag_enabled_only,
+ )
+ out.write(view)
+
+ if i != N - 1:
+ out.write()
+
+ if self.with_flag(DTShFlagCount):
+ out.write()
+ self._output_count_raw(count, out)
+
+ def _output_treelike_longfmt(
+ self,
+ path2walkable: Mapping[str, DTWalkable],
+ count: int,
+ out: DTShOutput,
+ ) -> None:
+ # Tree-like views (2-sided).
+ sketch = self.get_sketch(SketchMV.Layout.TWO_SIDED)
+ cols = self.get_longfmt(sketch.default_fmt)
+
+ N = len(path2walkable)
+ for i, (_, comb) in enumerate(path2walkable.items()):
+ view = ViewNodeTwoSided(comb, cols)
+ view.do_layout(
+ self.arg_sorter,
+ self.flag_reverse,
+ self.flag_enabled_only,
+ )
+ out.write(view)
+
+ if i != N - 1:
+ out.write()
+
+ if self.with_flag(DTShFlagCount):
+ out.write()
+ self._output_count_longftm(count, out)
+
+ def _get_path2walkable(
+ self,
+ path_expansions: Sequence[DTSh.PathExpansion],
+ sh: DTSh,
+ ) -> Tuple[int, Mapping[str, DTWalkable]]:
+ # Collect criterion chain.
+ criteria = self._get_criteria()
+
+ # One tree per root: for each expanded path, map its pathway
+ # to a subtree containing the nodes found there.
+ path2walkable: Dict[str, DTWalkable] = {}
+ count: int = 0
+ for expansion in path_expansions:
+ for branch in self.sort(expansion.nodes):
+ path = sh.pathway(branch, expansion.prefix)
+ nodes = list(
+ branch.find(
+ criteria,
+ order_by=self.arg_sorter,
+ reverse=self.flag_reverse,
+ enabled_only=self.flag_enabled_only,
+ )
+ )
+ if nodes:
+ count += len(nodes)
+ comb = DTWalkableComb(branch, nodes)
+ path2walkable[path] = comb
+
+ return (count, path2walkable)
+
+ def _output_count_longftm(self, count: int, out: DTShOutput) -> None:
+ out.write(
+ TextUtil.assemble("Found ", TextUtil.bold(str(count))),
+ "nodes.",
+ )
+
+ def _output_count_raw(self, count: int, out: DTShOutput) -> None:
+ out.write(f"Found: {count}")
+
+ def _get_criteria(self) -> DTNodeCriteria:
+ # All defined command arguments that may participate in the search.
+ args_criterion: List[DTShArgCriterion] = [
+ self.with_arg(type(option))
+ for option in self.options
+ if isinstance(option, DTShArgCriterion)
+ ]
+
+ try:
+ arg_criteria: List[DTNodeCriterion] = [
+ criterion
+ for criterion in (
+ arg_criterion.get_criterion(
+ re_strict=self.with_flag(DTShFlagRegex),
+ ignore_case=self.with_flag(DTShFlagIgnoreCase),
+ )
+ for arg_criterion in args_criterion
+ )
+ if criterion
+ ]
+
+ return DTNodeCriteria(
+ arg_criteria,
+ ored_chain=self.with_flag(DTShFlagLogicalOr),
+ negative_chain=self.with_flag(DTShFlagLogicalNot),
+ )
+
+ except DTShError as e:
+ # Invalid criterion pattern or expression.
+ raise DTShCommandError(self, e.msg) from e
diff --git a/src/dtsh/builtins/ls.py b/src/dtsh/builtins/ls.py
new file mode 100644
index 0000000..a4a0e54
--- /dev/null
+++ b/src/dtsh/builtins/ls.py
@@ -0,0 +1,217 @@
+# Copyright (c) 2023 Christophe Dufaza
+#
+# SPDX-License-Identifier: Apache-2.0
+
+"""Devicetree shell built-in "ls".
+
+List branch contents.
+
+Unit tests and examples: tests/test_dtsh_builtin_ls.py
+"""
+
+
+from typing import Sequence, Dict, Mapping
+
+from dtsh.model import DTNode
+from dtsh.io import DTShOutput
+from dtsh.shell import DTSh
+from dtsh.shellutils import (
+ DTShFlagReverse,
+ DTShFlagEnabledOnly,
+ DTShFlagPager,
+ DTShFlagRecursive,
+ DTShFlagNoChildren,
+ DTShArgOrderBy,
+ DTShArgFixedDepth,
+ DTShParamDTPaths,
+)
+
+from dtsh.rich.shellutils import DTShCommandLongFmt
+from dtsh.rich.text import TextUtil
+from dtsh.rich.modelview import DTModelView, SketchMV, ViewNodeList
+
+
+class DTShBuiltinLs(DTShCommandLongFmt):
+ """Devicetree shell built-in "ls"."""
+
+ def __init__(self) -> None:
+ super().__init__(
+ "ls",
+ "list branch contents",
+ [
+ DTShFlagNoChildren(),
+ DTShFlagReverse(),
+ DTShFlagRecursive(),
+ DTShFlagEnabledOnly(),
+ DTShFlagPager(),
+ DTShArgOrderBy(),
+ DTShArgFixedDepth(),
+ ],
+ DTShParamDTPaths(),
+ )
+
+ def execute(self, argv: Sequence[str], sh: DTSh, out: DTShOutput) -> None:
+ """Overrides DTShCommand.execute()."""
+ super().execute(argv, sh, out)
+
+ # Expand path parameter.
+ path_expansions: Sequence[DTSh.PathExpansion] = self.with_param(
+ DTShParamDTPaths
+ ).expand(self, sh)
+
+ if self.with_flag(DTShFlagPager):
+ out.pager_enter()
+
+ # What to do here depends on the appropriate model kind,
+ # "files" (nodes) or "directories" (branches).
+ if self.with_flag(DTShFlagNoChildren):
+ # List nodes, not branche(s) contents ("-d").
+ self._ls_nodes(path_expansions, sh, out)
+ else:
+ # List branches as directories.
+ self._ls_contents(path_expansions, sh, out)
+
+ if self.with_flag(DTShFlagPager):
+ out.pager_exit()
+
+ def _ls_nodes(
+ self,
+ path_expansions: Sequence[DTSh.PathExpansion],
+ sh: DTSh,
+ out: DTShOutput,
+ ) -> None:
+ # Get the nodes to list as "files".
+ path2node: Mapping[str, DTNode] = self._get_path2node(
+ path_expansions, sh
+ )
+ if not path2node:
+ return
+
+ if self.has_longfmt:
+ # Formatted output.
+ self._output_nodes_longfmt(path2node, out)
+ else:
+ # POSIX-like.
+ self._output_nodes_raw(path2node, out)
+
+ def _get_path2node(
+ self,
+ path_expansions: Sequence[DTSh.PathExpansion],
+ sh: DTSh,
+ ) -> Mapping[str, DTNode]:
+ path2node: Dict[str, DTNode] = {}
+ for expansion in path_expansions:
+ for node in self.sort(expansion.nodes):
+ path = sh.pathway(node, expansion.prefix)
+ path2node[path] = node
+ return path2node
+
+ def _output_nodes_raw(
+ self, path2node: Mapping[str, DTNode], out: DTShOutput
+ ) -> None:
+ for path in path2node:
+ out.write(path)
+
+ def _output_nodes_longfmt(
+ self, path2node: Mapping[str, DTNode], out: DTShOutput
+ ) -> None:
+ sketch = self.get_sketch(SketchMV.Layout.LIST_VIEW)
+ cols = self.get_longfmt(sketch.default_fmt)
+
+ lv_nodes = ViewNodeList(cols, sketch)
+ lv_nodes.extend(path2node.values())
+ out.write(lv_nodes)
+
+ def _ls_contents(
+ self,
+ path_expansions: Sequence[DTSh.PathExpansion],
+ sh: DTSh,
+ out: DTShOutput,
+ ) -> None:
+ # Get the branches to list the contents of as "directories".
+ path2contents: Mapping[str, Sequence[DTNode]] = self._get_path2contents(
+ path_expansions, sh
+ )
+ if not path2contents:
+ return
+
+ if self.has_longfmt:
+ # Formatted output.
+ self._output_contents_longfmt(path2contents, sh, out)
+ else:
+ # POSIX-like.
+ self._output_contents_raw(path2contents, out)
+
+ def _get_path2contents(
+ self,
+ path_expansions: Sequence[DTSh.PathExpansion],
+ sh: DTSh,
+ ) -> Mapping[str, Sequence[DTNode]]:
+ mode_recursive = (
+ self.with_flag(DTShFlagRecursive)
+ or self.with_arg(DTShArgFixedDepth).isset
+ )
+
+ path2contents: Dict[str, Sequence[DTNode]] = {}
+ for expansion in path_expansions:
+ for node in self.sort(expansion.nodes):
+ if mode_recursive:
+ for branch in node.walk(
+ order_by=self.arg_sorter,
+ reverse=self.flag_reverse,
+ enabled_only=self.flag_enabled_only,
+ fixed_depth=self.with_arg(DTShArgFixedDepth).depth,
+ ):
+ path = sh.pathway(branch, expansion.prefix)
+ path2contents[path] = self.sort(branch.children)
+ else:
+ path = sh.pathway(node, expansion.prefix)
+ # Filter out disabled nodes if asked to.
+ contents = self.prune(node.children)
+ # Sort contents if asked to.
+ path2contents[path] = self.sort(contents)
+
+ return path2contents
+
+ def _output_contents_raw(
+ self, path2contents: Mapping[str, Sequence[DTNode]], out: DTShOutput
+ ) -> None:
+ N = len(path2contents)
+ for i, (dirpath, contents) in enumerate(path2contents.items()):
+ if N > 1:
+ out.write(f"{dirpath}:")
+
+ for node in contents:
+ out.write(node.name)
+
+ if i != N - 1:
+ # Insert empty line between "directories".
+ out.write()
+
+ def _output_contents_longfmt(
+ self,
+ path2contents: Mapping[str, Sequence[DTNode]],
+ sh: DTSh,
+ out: DTShOutput,
+ ) -> None:
+ N = len(path2contents)
+ sketch = self.get_sketch(SketchMV.Layout.LIST_VIEW)
+ cols = self.get_longfmt(sketch.default_fmt)
+
+ for i, (dirpath, contents) in enumerate(path2contents.items()):
+ if N > 1:
+ tv_dirpath = DTModelView.mk_path(dirpath)
+ if not sh.node_at(dirpath).enabled:
+ TextUtil.disabled(tv_dirpath)
+ out.write(tv_dirpath, ":", sep="")
+
+ if contents:
+ # Output branch contents as list view.
+ lv_nodes = ViewNodeList(cols, sketch)
+ lv_nodes.left_indent(1)
+ lv_nodes.extend(contents)
+ out.write(lv_nodes)
+
+ if i != N - 1:
+ # Insert empty line between "directories".
+ out.write()
diff --git a/src/dtsh/builtins/pwd.py b/src/dtsh/builtins/pwd.py
new file mode 100644
index 0000000..2f7ca72
--- /dev/null
+++ b/src/dtsh/builtins/pwd.py
@@ -0,0 +1,31 @@
+# Copyright (c) 2023 Christophe Dufaza
+#
+# SPDX-License-Identifier: Apache-2.0
+
+"""Devicetree shell built-in "pwd".
+
+Print current working branch.
+
+Unit tests and examples: tests/test_dtsh_builtin_pwd.py
+"""
+
+
+from typing import Sequence
+
+from dtsh.io import DTShOutput
+from dtsh.shell import DTSh, DTShCommand
+
+
+class DTShBuiltinPwd(DTShCommand):
+ """Devicetree shell built-in "pwd"."""
+
+ def __init__(self) -> None:
+ """Command definition."""
+ super().__init__(
+ "pwd", "print path of current working branch", [], None
+ )
+
+ def execute(self, argv: Sequence[str], sh: DTSh, out: DTShOutput) -> None:
+ """Overrides DTShCommand.execute()."""
+ super().execute(argv, sh, out)
+ out.write(sh.pwd)
diff --git a/src/dtsh/builtins/tree.py b/src/dtsh/builtins/tree.py
new file mode 100644
index 0000000..56b311f
--- /dev/null
+++ b/src/dtsh/builtins/tree.py
@@ -0,0 +1,120 @@
+# Copyright (c) 2023 Christophe Dufaza
+#
+# SPDX-License-Identifier: Apache-2.0
+
+"""Devicetree shell built-in "tree".
+
+List nodes in tree-like format.
+
+Unit tests and examples: tests/test_dtsh_builtin_tree.py
+"""
+
+
+from typing import Sequence, Mapping, Dict
+
+from dtsh.model import DTNode
+from dtsh.io import DTShOutput
+from dtsh.shell import DTSh
+from dtsh.shellutils import (
+ DTShFlagReverse,
+ DTShFlagEnabledOnly,
+ DTShFlagPager,
+ DTShArgOrderBy,
+ DTShArgFixedDepth,
+ DTShParamDTPaths,
+)
+
+from dtsh.rich.modelview import (
+ SketchMV,
+ ViewNodeTreePOSIX,
+ ViewNodeTwoSided,
+)
+from dtsh.rich.shellutils import DTShCommandLongFmt
+
+
+class DTShBuiltinTree(DTShCommandLongFmt):
+ """Devicetree shell built-in "tree"."""
+
+ def __init__(self) -> None:
+ super().__init__(
+ "tree",
+ "list branch contents in tree-like format",
+ [
+ DTShFlagReverse(),
+ DTShFlagEnabledOnly(),
+ DTShFlagPager(),
+ DTShArgOrderBy(),
+ DTShArgFixedDepth(),
+ ],
+ DTShParamDTPaths(),
+ )
+
+ def execute(self, argv: Sequence[str], sh: DTSh, out: DTShOutput) -> None:
+ """Overrides DTShCommand.execute()."""
+ super().execute(argv, sh, out)
+
+ # Expand path parameter: can't be empty.
+ path_expansions: Sequence[DTSh.PathExpansion] = self.with_param(
+ DTShParamDTPaths
+ ).expand(self, sh)
+ # Get the model, mapping the branches to list to their expected pathways.
+ # pathway -> node.
+ path2branch: Mapping[str, DTNode] = self._get_path2branch(
+ path_expansions, sh
+ )
+
+ if self.with_flag(DTShFlagPager):
+ out.pager_enter()
+
+ N = len(path2branch)
+ for i, (path, branch) in enumerate(path2branch.items()):
+ if self.has_longfmt:
+ # Formatted output (2sided view).
+ self._output_longfmt(branch, out)
+ else:
+ # POSIX-like simple tree.
+ self._output_raw(path, branch, out)
+
+ if i != N - 1:
+ # Insert empty line between trees.
+ out.write()
+
+ if self.with_flag(DTShFlagPager):
+ out.pager_exit()
+
+ def _get_path2branch(
+ self,
+ path_expansions: Sequence[DTSh.PathExpansion],
+ sh: DTSh,
+ ) -> Mapping[str, DTNode]:
+ path2branch: Dict[str, DTNode] = {}
+ for expansion in path_expansions:
+ for branch in self.sort(expansion.nodes):
+ path = sh.pathway(branch, expansion.prefix)
+ path2branch[path] = branch
+
+ return path2branch
+
+ def _output_longfmt(self, branch: DTNode, out: DTShOutput) -> None:
+ # Formatted branch output (2-sided view).
+ sketch = self.get_sketch(SketchMV.Layout.TWO_SIDED)
+ cells = self.get_longfmt(sketch.default_fmt)
+ view_2sided = ViewNodeTwoSided(branch, cells)
+ view_2sided.do_layout(
+ self.arg_sorter,
+ self.flag_reverse,
+ self.flag_enabled_only,
+ self.with_arg(DTShArgFixedDepth).depth,
+ )
+ out.write(view_2sided)
+
+ def _output_raw(self, path: str, branch: DTNode, out: DTShOutput) -> None:
+ # Branch as tree (POSIX output).
+ tree = ViewNodeTreePOSIX(path, branch)
+ tree.do_layout(
+ self.arg_sorter,
+ self.flag_reverse,
+ self.flag_enabled_only,
+ self.with_arg(DTShArgFixedDepth).depth,
+ )
+ out.write(tree)
diff --git a/src/dtsh/cli.py b/src/dtsh/cli.py
index 73a816f..40e5c42 100644
--- a/src/dtsh/cli.py
+++ b/src/dtsh/cli.py
@@ -1,29 +1,150 @@
-# Copyright (c) 2022 Chris Duf
+# Copyright (c) 2023 Christophe Dufaza
#
# SPDX-License-Identifier: Apache-2.0
-"""Shell-like CLI to a devicetree."""
+"""Devicetree shell CLI.
+Run the devicetree shell without West.
+"""
+from typing import cast, Optional, List
+
+import argparse
+import os
import sys
-from dtsh.dtsh import DtshError
-from dtsh.session import DevicetreeShellSession
+from dtsh.config import DTShConfig
+from dtsh.shell import DTShError
+from dtsh.rich.theme import DTShTheme
+from dtsh.rich.session import DTShRichSession
+
+
+class DTShCliArgv:
+ """Command line arguments parser."""
+
+ _parser: argparse.ArgumentParser
+ _argv: argparse.Namespace
+
+ def __init__(self) -> None:
+ self._parser = argparse.ArgumentParser(
+ prog="dtsh",
+ description="shell-like interface with Devicetree",
+ # See e.g. https://github.com/zephyrproject-rtos/zephyr/issues/53495
+ allow_abbrev=False,
+ )
+
+ grp_open_dts = self._parser.add_argument_group("open a DTS file")
+ grp_open_dts.add_argument(
+ "-b",
+ "--bindings",
+ help="directory to search for binding files",
+ action="append",
+ metavar="DIR",
+ )
+ grp_open_dts.add_argument(
+ "dts", help="path to the DTS file", nargs="?", metavar="DTS"
+ )
+
+ grp_user_files = self._parser.add_argument_group("user files")
+ grp_user_files.add_argument(
+ "-u",
+ "--user-files",
+ help="initialize per-user configuration files and exit",
+ action="store_true",
+ )
+ grp_user_files.add_argument(
+ "--preferences",
+ help="load additional preferences file",
+ nargs=1,
+ metavar="FILE",
+ )
+ grp_user_files.add_argument(
+ "--theme",
+ help="load additional styles file",
+ nargs=1,
+ metavar="FILE",
+ )
+ self._argv = self._parser.parse_args()
-def run():
- dt_src_path = sys.argv[1] if len(sys.argv) > 1 else None
- dt_bindings_path = sys.argv[2:] if len(sys.argv) > 2 else None
+ @property
+ def binding_dirs(self) -> Optional[List[str]]:
+ """Directories to search for binding files."""
+ if self._argv.bindings:
+ return cast(List[str], self._argv.bindings)
+ return None
+ @property
+ def dts(self) -> str:
+ """Path to the Devicetree source file."""
+ if self._argv.dts:
+ return str(self._argv.dts)
+ return os.path.join(os.path.abspath("build"), "zephyr", "zephyr.dts")
+
+ @property
+ def user_files(self) -> bool:
+ """Initialize user files and exit."""
+ return bool(self._argv.user_files)
+
+ @property
+ def preferences(self) -> Optional[str]:
+ """Additional preferences file."""
+ return (
+ str(self._argv.preferences[0]) if self._argv.preferences else None
+ )
+
+ @property
+ def theme(self) -> Optional[str]:
+ """Additional styles file."""
+ return str(self._argv.theme[0]) if self._argv.theme else None
+
+
+def _load_preference_file(path: str) -> None:
try:
- DevicetreeShellSession.open(dt_src_path, dt_bindings_path).run()
- except DtshError as e:
- print(f'{str(e)}\n')
- if e.cause:
- print(f'{str(e.cause)}\n')
- # -EINVAL
+ DTShConfig.getinstance().load_ini_file(path)
+ except DTShConfig.Error as e:
+ print(e, file=sys.stderr)
+ print(f"Failed to load preferences file: {path}", file=sys.stderr)
sys.exit(-22)
+def _load_theme_file(path: str) -> None:
+ try:
+ DTShTheme.getinstance().load_theme_file(path)
+ except DTShConfig.Error as e:
+ print(e, file=sys.stderr)
+ print(f"Failed to load styles file: {path}", file=sys.stderr)
+ sys.exit(-22)
+
+
+def run() -> None:
+ """Open a devicetree shell session and run its interactive loop."""
+ argv = DTShCliArgv()
+
+ if argv.user_files:
+ # Initialize per-user configuration files and exit.
+ ret = DTShConfig.getinstance().init_user_files()
+ sys.exit(ret)
+
+ if argv.preferences:
+ # Load additional preference file.
+ _load_preference_file(argv.preferences)
+
+ if argv.theme:
+ # Load additional styles file.
+ _load_theme_file(argv.theme)
+
+ session = None
+ try:
+ session = DTShRichSession.create(argv.dts, argv.binding_dirs)
+ except DTShError as e:
+ print(e.msg, file=sys.stderr)
+ print("Failed to initialize devicetree", file=sys.stderr)
+ sys.exit(-22)
+
+ if session:
+ session.run()
+
+
if __name__ == "__main__":
run()
diff --git a/src/dtsh/config.py b/src/dtsh/config.py
index d2a37e1..2383352 100644
--- a/src/dtsh/config.py
+++ b/src/dtsh/config.py
@@ -1,125 +1,560 @@
-# Copyright (c) 2022 Chris Duf
+# Copyright (c) 2023 Christophe Dufaza
#
# SPDX-License-Identifier: Apache-2.0
-"""Shell configuration API."""
+"""Devicetree shell configuration and data.
+User-specific configuration and data are stored into a
+platform-dependent directory.
+DTSh is configured by simple INI files named "dtsh.ini":
+
+- the bundled configuration file which sets the default configuration
+- an optional user's configuration file which customizes the defaults
+
+The command history file (if GNU readline support is enabled)
+is loaded from and stored into the same directory.
+
+Unit tests and examples: tests/test_dtsh_config.py
+"""
+
+
+from typing import Optional
+
+import configparser
+import codecs
+import enum
import os
+import re
+import shutil
import sys
-from rich.theme import Theme
-from dtsh.rl import readline
-from dtsh.dtsh import DtshError
+class ActionableType(enum.Enum):
+ """Control the rendering of actionable text elements (aka links).
+ When acted on (clicked) actionable UIs elements should open local
+ files in the system default text editor (e.g. YAML binding files),
+ or open external URLs in the system default browser (e.g. links to
+ the Zephyr project documentation or Devicetree Specification).
-class DtshConfig(object):
- """Shell configuration manager.
+ Valid values:
+
+ - "none": won't create any link
+ - "default": the text itself (e.g. "nordic,nrf-twi") is made actionable
+ - "sparse": an additional actionable view (e.g. "[↗]") is appended
+ to the original text
+
+ For sparse links, the additional view content is configured
+ by the DTSh option "wchar.actionable".
"""
- @staticmethod
- def usr_config_base_posix() -> str:
- """User configuration directory (Linux).
+ NONE = "none"
+ """No actions."""
- On Linux, the user configuration is $XDG_CONFIG_HOME/dtsh.
+ LINK = "link"
+ """Make linked text itself actionable."""
- According to the XDG Base Directory Specification,
- should default to ~/.config/dtsh if XDG_CONFIG_HOME is not set.
+ ALT = "alt"
+ """Append additional actionable view."""
- See:
- - https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html
- """
- cfg_base = os.environ.get('XDG_CONFIG_HOME')
- if not cfg_base:
- cfg_base = os.path.join(os.path.expanduser('~'), '.config')
- return os.path.join(cfg_base, 'dtsh')
- @staticmethod
- def usr_config_base_nt() -> str:
- r"""User configuration directory (Windows).
+class DTShConfig:
+ """Devicetree shell application data and configuration."""
+
+ class Error(BaseException):
+ """Error loading configuration file."""
+
+ @classmethod
+ def getinstance(cls) -> "DTShConfig":
+ """Access the preferences configuration instance."""
+ return _dtshconf
+
+ # RE for ASCII escape sequences that may appear in Python strings.
+ # See:
+ # - DTShConfig.getstr()
+ # - "unicode_escape doesn't work in general" (https://stackoverflow.com/a/24519338)
+ _RE_ESCAPE_SEQ: re.Pattern[str] = re.compile(
+ r"""
+ ( \\U........
+ | \\u....
+ | \\x..
+ | \\[0-7]{1,3}
+ | \\N\{[^}]+\}
+ | \\[\\'"abfnrtv]
+ )""",
+ re.UNICODE | re.VERBOSE,
+ )
- On Windows (NT+), the user configuration is %LOCALAPPDATA%\dtsh,
- and should default to ~\AppData\Local\Dtsh.
+ # Parsed configuration.
+ _cfg: configparser.ConfigParser
+
+ # Path to the per-user DTSh configuration and data directory
+ _app_dir: str
+
+ def __init__(self, path: Optional[str] = None) -> None:
+ """Initialize DTSh configuration.
+
+ If a configuration path is explicitly set,
+ only this configuration file is loaded.
+
+ Otherwise, proceed to default configuration initialization:
+
+ - 1st, load bundled default configuration file
+ - then, load user's configuration file to customize defaults
+
+ Args:
+ path: Path to configuration file,
+ or None for default configuration initialization.
"""
- cfg_base = os.environ.get('LOCALAPPDATA')
- if not cfg_base:
- cfg_base = os.path.join(os.path.expanduser('~'), 'AppData', 'Local')
- return os.path.join(cfg_base, 'Dtsh')
+ self._init_app_dir()
- @staticmethod
- def usr_config_base_darwin() -> str:
- """User configuration directory (macOS).
+ self._cfg = configparser.ConfigParser(
+ # Despite its name, extended interpolation seems more intuitive
+ # to us than basic interpolation.
+ interpolation=configparser.ExtendedInterpolation()
+ )
- On macOS, default to ~/Library/Dtsh.
+ if path:
+ # If explicitly specified, load only this one.
+ self.load_ini_file(path)
+ else:
+ # Load defaults from bundled configuration file.
+ path = os.path.join(os.path.dirname(__file__), "dtsh.ini")
+ self.load_ini_file(path)
+ # Load user's configuration file if any.
+ path = self.get_user_file("dtsh.ini")
+ if os.path.isfile(path):
+ self.load_ini_file(path)
+
+ @property
+ def app_dir(self) -> str:
+ r"""Path to the per-user DTSh configuration and data directory.
+
+ Location is platform-dependent:
+
+ - POSIX: "$XDG_CONFIG_HOME/dtsh", or "~/.config/dtsh" if XDG_CONFIG_HOME
+ is not set (Freedesktop XDG Base Directory Specification)
+ - Windows: "%LOCALAPPDATA%\DTSh", or "~\AppData\Local\DTSh"
+ if LOCALAPPDATA is unset (unlikely)
+ - macOS: "~/Library/DTSh"
+
+ Returns:
+ The path to the user's directory for DTSh application data
+ and configuration files. The directory is not granted to exist.
"""
- return os.path.join(os.path.expanduser('~'), 'Libray', 'Dtsh')
+ return self._app_dir
+
+ @property
+ def wchar_ellipsis(self) -> str:
+ """Ellipsis."""
+ return self.getstr("wchar.ellipsis")
+
+ @property
+ def wchar_arrow_ne(self) -> str:
+ """North-East Arrow."""
+ return self.getstr("wchar.arrow_ne")
+
+ @property
+ def wchar_arrow_nw(self) -> str:
+ """North-West Arrow."""
+ return self.getstr("wchar.arrow_nw")
+
+ @property
+ def wchar_arrow_right(self) -> str:
+ """Rightwards Arrow."""
+ return self.getstr("wchar.arrow_right")
+
+ @property
+ def wchar_arrow_right_hook(self) -> str:
+ """Rightwards Arrow."""
+ return self.getstr("wchar.arrow_right_hook")
+
+ @property
+ def wchar_dash(self) -> str:
+ """Tiret."""
+ return self.getstr("wchar.dash")
+
+ @property
+ def wchar_link(self) -> str:
+ """Tiret."""
+ return self.getstr("wchar.link")
+
+ @property
+ def prompt_default(self) -> str:
+ """Default ANSI prompt for DTSh sessions."""
+ return self.getstr("prompt.default")
+
+ @property
+ def prompt_alt(self) -> str:
+ """Alternate ANSI prompt for DTSh sessions."""
+ return self.getstr("prompt.alt")
+
+ @property
+ def prompt_sparse(self) -> bool:
+ """Whether to append an empty line after DTSh command outputs."""
+ return self.getbool("prompt.sparse")
+
+ @property
+ def pref_redir2_maxwidth(self) -> int:
+ """Maximum width in number characters for command output redirection."""
+ return self.getint("pref.redir2_maxwidth")
+
+ @property
+ def pref_always_longfmt(self) -> bool:
+ """Whether to assume the flag "use long listing format" is always set."""
+ return self.getbool("pref.always_longfmt")
+
+ @property
+ def pref_fs_hide_dotted(self) -> bool:
+ """Whether to hide files and directories whose name starts with "."."""
+ return self.getbool("pref.fs.hide_dotted")
+
+ @property
+ def pref_fs_no_spaces(self) -> bool:
+ """Whether to forbid spaces in redirection file paths."""
+ return self.getbool("pref.fs.no_spaces")
+
+ @property
+ def pref_fs_no_overwrite(self) -> bool:
+ """Whether to forbid redirection to overwrite existing files."""
+ return self.getbool("pref.fs.no_overwrite")
+
+ @property
+ def pref_sizes_si(self) -> bool:
+ """Whether to print sizes with SI units (bytes, kB, MB)."""
+ return self.getbool("pref.sizes_si")
+
+ @property
+ def pref_hex_upper(self) -> bool:
+ """Whether to print hexadicimal digits upper case."""
+ return self.getbool("pref.hex_upper")
+
+ @property
+ def pref_list_headers(self) -> bool:
+ """Whether to show the headers in list views."""
+ return self.getbool("pref.list.headers")
+
+ @property
+ def pref_list_placeholder(self) -> str:
+ """Placeholder for missing values in list views."""
+ return self.getstr("pref.list.place_holder")
+
+ @property
+ def pref_list_fmt(self) -> str:
+ """Default format string for node fields in list views."""
+ return self.getstr("pref.list.fmt")
+
+ @property
+ def pref_list_actionable_type(self) -> ActionableType:
+ """Actionable type for list views."""
+ return self.get_actionable_type("pref.list.actionable_type")
+
+ @property
+ def pref_list_multi(self) -> bool:
+ """Whether to allow multiple-line cells in list views."""
+ return self.getbool("pref.list.multi")
+
+ @property
+ def pref_tree_headers(self) -> bool:
+ """Whether to show the headers in tree views."""
+ return self.getbool("pref.tree.headers")
+
+ @property
+ def pref_tree_placeholder(self) -> str:
+ """Placeholder for missing values in tree views."""
+ return self.getstr("pref.tree.place_holder")
- @staticmethod
- def usr_config_base(enforce: bool = False) -> str:
- """Returns the user's configuration directory for dtsh sessions.
+ @property
+ def pref_tree_fmt(self) -> str:
+ """Default format string for node fields in tree views."""
+ return self.getstr("pref.tree.fmt")
- Arguments:
- enforce -- if True, will try to create the configuration directory
- when necessary
+ @property
+ def pref_tree_actionable_type(self) -> ActionableType:
+ """Actionable type for tree anchors."""
+ return self.get_actionable_type("pref.tree.actionable_type")
- Raises IOError when the requested directory can't be initialized.
+ @property
+ def pref_2Sided_actionable_type(self) -> ActionableType:
+ """Actionable type for the left list view of a 2-sided."""
+ return self.get_actionable_type("pref.2sided.actionable_type")
+
+ @property
+ def pref_tree_cb_anchor(self) -> str:
+ """Symbol to anchor child-bindings to their parent in tree-views."""
+ return self.getstr("pref.tree.cb_anchor")
+
+ @property
+ def pref_actionable_type(self) -> ActionableType:
+ """Default rendering for actionable texts."""
+ actionable_type = self.getstr("pref.actionable_type")
+ try:
+ return ActionableType(actionable_type)
+ except ValueError:
+ print(
+ f"Invalid actionable type: {actionable_type}", file=sys.stderr
+ )
+ return ActionableType.LINK
+
+ @property
+ def pref_actionable_text(self) -> str:
+ """Alternate actionable view."""
+ return self.getstr("pref.actionable_wchar")
+
+ @property
+ def pref_svg_theme(self) -> str:
+ """CSS theme for command output redirection to SVG."""
+ return self.getstr("pref.svg.theme")
+
+ @property
+ def pref_svg_font_family(self) -> str:
+ """Font family for command output redirection to SVG."""
+ return self.getstr("pref.svg.font_family")
+
+ @property
+ def pref_svg_font_ratio(self) -> float:
+ """Font aspect ratio for command output redirection to SVG."""
+ return self.getfloat("pref.svg.font_ratio")
+
+ @property
+ def pref_html_theme(self) -> str:
+ """CSS theme for command output redirection to HTML."""
+ return self.getstr("pref.html.theme")
+
+ @property
+ def pref_html_font_family(self) -> str:
+ """Font family for command output redirection to HTML."""
+ return self.getstr("pref.html.font_family")
+
+ def init_user_files(self) -> int:
+ """Initialize per-user configuration files."""
+ if os.path.isdir(self._app_dir):
+ src_dir = os.path.dirname(os.path.abspath(__file__))
+
+ try:
+ dst = self.get_user_file("dtsh.ini")
+ if os.path.exists(dst):
+ print(f"File exists, skipped: {dst}")
+ else:
+ shutil.copyfile(os.path.join(src_dir, "dtsh.ini"), dst)
+ print(f"User preferences: {dst}")
+
+ dst = self.get_user_file("theme.ini")
+ if os.path.exists(dst):
+ print(f"File exists, skipped: {dst}")
+ else:
+ shutil.copyfile(
+ os.path.join(src_dir, "rich", "theme.ini"), dst
+ )
+ print(f"User theme: {dst}")
+
+ return 0
+
+ except (OSError, OSError) as e:
+ print(f"Failed to create file: {dst}", file=sys.stderr)
+ print(f"Cause: {e}", file=sys.stderr)
+
+ # Per-user configuration files don't exist (-ENOENT).
+ return -2
+
+ def get_user_file(self, *paths: str) -> str:
+ """Get path to a use file within the DTSh application directory.
+
+ Args:
+ paths: Relative path to the resource.
"""
- if sys.platform == 'darwin':
- cfg_dir = DtshConfig.usr_config_base_darwin()
- elif os.name == 'nt':
- cfg_dir = DtshConfig.usr_config_base_nt()
- else:
- cfg_dir = DtshConfig.usr_config_base_posix()
- if enforce and not os.path.isdir(cfg_dir):
- os.mkdir(cfg_dir)
- return cfg_dir
+ return os.path.join(self._app_dir, *paths)
- @staticmethod
- def get_history_path() -> str:
- """Returns the history file's path.
+ def getbool(self, option: str, fallback: bool = False) -> bool:
+ """Access a configuration option's value as a boolean.
- Raises IOError when the configuration directory can't be initialized.
+ Boolean:
+ - True: '1', 'yes', 'true', and 'on'
+ - False: '0', 'no', 'false', and 'off'
+
+ Args:
+ option: The option's name.
+ fallback: If set, represents the fall-back value
+ for an undefined option or an invalid value.
+ Defaults to False.
+
+ Returns:
+ The option's value as a boolean.
"""
- return os.path.join(DtshConfig.usr_config_base(True), 'history')
+ try:
+ return self._cfg.getboolean("dtsh", option)
+ except (configparser.Error, ValueError) as e:
+ print(f"configuration error: {option}: {e}", file=sys.stderr)
+ return fallback
+
+ def getint(self, option: str, fallback: int = 0) -> int:
+ """Access a configuration option's value as a integer.
+
+ Integers:
+
+ - base-2, -8, -10 and -16 are supported
+ - if not base 10, the actual base is determined
+ by the prefix "0b/0B" (base-2), "0o/0O" (base-8),
+ or "0x/0X" (base-16)
- @staticmethod
- def get_theme_path() -> str:
- """Returns the rich theme's path.
+ Args:
+ option: The option's name.
+ fallback: If set, represents the fall-back value
+ for an undefined option or an invalid value.
+ Defaults to 0.
+
+ Returns:
+ The option's value as an integer.
"""
- theme_path = os.path.join(DtshConfig.usr_config_base(), 'theme')
- if not os.path.isfile(theme_path):
- # Fallback to default theme.
- theme_path = os.path.join(os.path.dirname(__file__), 'theme')
- return theme_path
-
- @staticmethod
- def readline_read_history() -> None:
- """Load history file.
+ try:
+ return int(self._cfg.get("dtsh", option), base=0)
+ except (configparser.Error, ValueError) as e:
+ print(f"configuration error: {option}: {e}", file=sys.stderr)
+ return fallback
+
+ def getfloat(self, option: str, fallback: float = 0) -> float:
+ """Access a configuration option's value as float.
+
+ Args:
+ option: The option's name.
+ fallback: If set, represents the fall-back value
+ for an undefined option or an invalid value.
+ Defaults to 0.
+
+ Returns:
+ The option's value as a float.
"""
try:
- history_path = DtshConfig.get_history_path()
- if os.path.isfile(history_path):
- readline.read_history_file(history_path)
- except IOError as e:
- print(f"Failed to load history: {str(e)}")
-
- @staticmethod
- def readline_write_history() -> None:
- """Save history file.
+ return float(self._cfg.get("dtsh", option))
+ except (configparser.Error, ValueError) as e:
+ print(f"configuration error: {option}: {e}", file=sys.stderr)
+ return fallback
+
+ def getstr(self, option: str, fallback: str = "") -> str:
+ r"""Access a configuration option's value as wide string.
+
+ Wide strings may contain:
+ - actual Unicode characters (e.g. "↗")
+ - UTF-8 character literals (e.g. "\u276d")
+ - other ASCII escape sequences (e.g. "\t")
+
+ Double-quotes are optional, excepted when the string value
+ ends with trailing spaces (e.g. "❯ ").
+
+ Args:
+ option: The option's name.
+ fallback: If set, represents the fall-back value
+ for an undefined option or an invalid value.
+ Defaults to an empty string.
+
+ Returns:
+ The option's value as a wide string.
"""
+ # To support what we named "wide strings", relying on Python UTF-8
+ # codecs won't work:
+ #
+ # "00î00".encode('utf-8').decode('unicode_escape')
+ # '00î00'
+ #
+ # Reason (see "unicode_escape doesn't work in general" bellow):
+ # The unicode_escape codec, despite its name, turns out to assume
+ # that all non-ASCII bytes are in the Latin-1 (ISO-8859-1) encoding.
+ #
+ # The best approach seems to be:
+ # - to not encode values, Python 3 strings are already Unicode
+ # - decode only escape sequences
+ #
+ # Adapted from @rspeer answer "unicode_escape doesn't work in general"
+ # at https://stackoverflow.com/a/24519338.
+ try:
+ # Quoted strings permit to define empty strings and strings
+ # that end with spaces: strip leading and trailing spaces.
+ val = self._cfg.get("dtsh", option).strip('"')
+ # Multi-line strings permit to define long strings: replace
+ # newlines with spaces.
+ val = val.replace("\n", " ")
+ return str(
+ DTShConfig._RE_ESCAPE_SEQ.sub(
+ lambda match: codecs.decode(
+ match.group(0), "unicode-escape"
+ ),
+ val,
+ )
+ )
+ except configparser.Error as e:
+ print(f"Configuration error: {option}: {e}", file=sys.stderr)
+ return fallback
+
+ def get_actionable_type(self, pref: str) -> ActionableType:
+ """Access a preference as actionable type."""
+ actionable_type = self.getstr(pref)
try:
- readline.write_history_file(DtshConfig.get_history_path())
- except IOError as e:
- print(f"Failed to save history: {str(e)}")
+ return ActionableType(actionable_type)
+ except ValueError:
+ print(
+ f"{pref}: invalid actionable type '{actionable_type}'",
+ file=sys.stderr,
+ )
+ # Fall-back, we don't want to fault.
+ return ActionableType.LINK
- @staticmethod
- def rich_read_theme() -> Theme:
- """Returns the rich theme.
+ def load_ini_file(self, path: str) -> None:
+ """Load options from configuration file (INI format).
- Raises DtshError when the theme file is invalid.
+ Overrides already loaded values with the same keys.
+
+ Args:
+ path: Path to a configuration file.
+
+ Raises:
+ DTShConfig.Error: Failed to load configuration file.
"""
try:
- return Theme.from_file(open(DtshConfig.get_theme_path()))
- except Exception as e:
- raise DtshError("Failed to load theme", e)
+ f = open( # pylint: disable=consider-using-with
+ path, "r", encoding="utf-8"
+ )
+ self._cfg.read_file(f)
+
+ except (OSError, configparser.Error) as e:
+ raise DTShConfig.Error(str(e)) from e
+
+ def _init_app_dir(self) -> None:
+ if sys.platform == "darwin":
+ self._app_dir = self._init_app_dir_darwin()
+ elif os.name == "nt":
+ self._app_dir = self._init_app_dir_nt()
+ else:
+ self._app_dir = self._init_app_dir_posix()
+
+ if not os.path.isdir(self._app_dir):
+ try:
+ os.makedirs(self._app_dir, mode=0o750)
+ except OSError as e:
+ print(
+ f"Failed to create directory: {self._app_dir}",
+ file=sys.stderr,
+ )
+ print(f"Cause: {e}", file=sys.stderr)
+
+ def _init_app_dir_darwin(self) -> str:
+ return os.path.abspath(
+ os.path.join(os.path.expanduser("~"), "Library", "DTSh")
+ )
+
+ def _init_app_dir_nt(self) -> str:
+ local_app_data = os.environ.get(
+ "LOCALAPPDATA",
+ os.path.join(os.path.expanduser("~"), "AppData", "Local"),
+ )
+ return os.path.abspath(os.path.join(local_app_data, "DTSh"))
+
+ def _init_app_dir_posix(self) -> str:
+ xdg_cfg_home = os.environ.get(
+ "XDG_CONFIG_HOME",
+ os.path.join(os.path.expanduser("~"), ".config"),
+ )
+ return os.path.abspath(os.path.join(xdg_cfg_home, "dtsh"))
+
+
+_dtshconf = DTShConfig()
diff --git a/src/dtsh/dts.py b/src/dtsh/dts.py
new file mode 100644
index 0000000..861978a
--- /dev/null
+++ b/src/dtsh/dts.py
@@ -0,0 +1,824 @@
+# Copyright (c) 2023 Christophe Dufaza
+#
+# Copyright (c) 2018 Open Source Foundries Limited.
+#
+# SPDX-License-Identifier: Apache-2.0
+
+"""Devicetree source definition.
+
+A devicetree is fully defined by:
+
+- a DTS file in Devicetree source format (DTSpec 6)
+- all of the YAML binding files the DTS recursively depends on
+- the Devicetree Specification
+
+This module may rely on cached CMake variables to locate
+the binding files the DTS file was actually generated with.
+The herein CMake cache reader implementation is adapted from
+the zcmake.py module in zephyr/scripts/west_commands.
+
+This module eventually introduces a YAML file system API
+that should cover the devicetree shell needs:
+
+- access the DT binding files with their base name
+- recursively access YAML-included bindings
+- the name2path semantic expected for edtlib.Binding objects initialization
+
+Unit tests and examples: tests/test_dtsh_dts.py
+"""
+
+
+from typing import (
+ cast,
+ Optional,
+ Union,
+ List,
+ Sequence,
+ Dict,
+ Iterator,
+ Mapping,
+)
+
+import os
+import re
+import sys
+
+import yaml
+
+# Custom PyYAML loader with support for the legacy '!include syntax.
+from devicetree.edtlib import _BindingLoader as YAMLBindingLoader
+
+
+class DTS:
+ """Devicetree source definition.
+
+ A devicetree source is defined by:
+
+ - a DTS file in Devicetree source format (DTSpec 6)
+ - all of the YAML binding files the DTS recursively depends on;
+ the list of directories to search for the YAML files is
+ termed "bindings search path"
+
+ When not explicitly set, a default bindings search path
+ can be retrieved, or worked out, based on:
+
+ - cached CMake variables: this is the preferred method,
+ assuming the cache file was produced by the same build as the DTS file
+ - environment variables: less reliable than the CMake cache,
+ since their *current* values are not bound to the build that
+ produced the DTS file
+ - assumptions on files layout, e.g. the CMake cache file is expected
+ to be named CMakeCache.txt, and located in the grand-parent directory
+ of where the DTS file itself is located (as shown bellow)
+
+ The CMake cache file is searched for according to the typical
+ Zephyr build layout:
+
+ build/
+ ├── CMakeCache.txt
+ └── zephyr/
+ └── zephyr.dts
+
+ """
+
+ # See path().
+ _dts_path: str
+
+ # See bindings_search_path().
+ _binding_dirs: List[str]
+
+ # See zephyr_base().
+ _zephyr_base: Optional[str]
+
+ # See vendors_file().
+ _vendors_file: Optional[str]
+
+ _cmake: Optional["CMakeCache"]
+ _yamlfs: "YAMLFilesystem"
+
+ def __init__(
+ self,
+ dts_path: str,
+ binding_dirs: Optional[Sequence[str]] = None,
+ vendors_file: Optional[str] = None,
+ ) -> None:
+ """Define a devicetree source.
+
+ Args:
+ dts_path: Path to the devicetree source file.
+ binding_dirs: The list of directories to search for the
+ YAML binding files the DTS depends on.
+ If not set, the default bindings search path is assumed.
+ vendors_file: Path to a file in vendor-prefixes.txt format.
+ """
+ self._dts_path = os.path.abspath(dts_path)
+ self._cmake = self._init_cmake_cache()
+ self._zephyr_base = self._init_zephyr_base()
+ self._vendors_file = self._init_vendors_file(vendors_file)
+ self._binding_dirs = self._init_binding_dirs(binding_dirs)
+ self._yamlfs = YAMLFilesystem(self._binding_dirs)
+
+ @property
+ def path(self) -> str:
+ """Path to the DTS file."""
+ return self._dts_path
+
+ @property
+ def vendors_file(self) -> Optional[str]:
+ "Path to the vendors file." ""
+ return self._vendors_file
+
+ @property
+ def bindings_search_path(self) -> Sequence[str]:
+ """Directories where the YAML binding files are located.
+
+ Either:
+
+ - set explicitly when defining the DT source
+ - or retrieved from the CMake cached variable CACHED_DTS_ROOT_BINDINGS
+ - or worked out (*best effort*) according to "Where bindings are located"
+
+ In the later case, depending on defined environment variables
+ or cached CMake variables, the bindings search path should contain
+ the dts/bindings sub-directories of:
+
+ - the zephyr repository
+ - the application source directory
+ - any custom board directory
+ - shield directories
+
+ This search path is not expunged from non existing directories:
+ it's intended to represent the *considered* directories.
+
+ Refer to zephyr/cmake/modules/dts.cmake for the DTS_ROOT
+ and DTS_ROOT_BINDINGS values set at build-time:
+
+ list(APPEND
+ DTS_ROOT
+ ${APPLICATION_SOURCE_DIR}
+ ${BOARD_DIR}
+ ${SHIELD_DIRS}
+ ${ZEPHYR_BASE}
+ )
+
+ foreach(dts_root ${DTS_ROOT})
+ set(bindings_path ${dts_root}/dts/bindings)
+ if(EXISTS ${bindings_path})
+ list(APPEND
+ DTS_ROOT_BINDINGS
+ ${bindings_path}
+ )
+ endif()
+
+ """
+ return self._binding_dirs
+
+ @property
+ def yamlfs(self) -> "YAMLFilesystem":
+ """YAML bindings search support."""
+ return self._yamlfs
+
+ @property
+ def app_binary_dir(self) -> str:
+ """Application binary directory (aka build directory).
+
+ Derived from the DTS file path.
+ """
+ return os.path.dirname(os.path.dirname(self._dts_path))
+
+ @property
+ def app_source_dir(self) -> Optional[str]:
+ """Application source directory (aka project directory).
+
+ Either retrieved from the CMake cache (APPLICATION_SOURCE_DIR),
+ or derived from the application binary directory.
+ """
+ app_src_dir = None
+ if self._cmake:
+ app_src_dir = self._cmake.getstr("APPLICATION_SOURCE_DIR")
+ if not app_src_dir:
+ app_src_dir = os.path.dirname(self.app_binary_dir)
+ return app_src_dir
+
+ @property
+ def board_dir(self) -> Optional[str]:
+ """Board directory.
+
+ Retrieved from the CMake cache (BOARD_DIR).
+ """
+ return self._cmake.getstr("BOARD_DIR") if self._cmake else None
+
+ @property
+ def board(self) -> Optional[str]:
+ """Board name.
+
+ Retrieved from the CMake cache (BOARD, CACHED_BOARD).
+ """
+ board = None
+ if self._cmake:
+ board = self._cmake.getstr("BOARD")
+ if not board:
+ board = self._cmake.getstr("CACHED_BOARD")
+ return board
+
+ @property
+ def board_file(self) -> Optional[str]:
+ """Board DTS file.
+
+ Shortcut to "${BOARD_DIR}/${BOARD}.dts".
+ """
+ if self.board_dir and self.board:
+ return os.path.join(self.board_dir, f"{self.board}.dts")
+ return None
+
+ @property
+ def shield_dirs(self) -> Sequence[str]:
+ """Shield directories.
+
+ Retrieved from the CMake cache (SHIELD_DIRS, CACHED_SHIELD_DIRS).
+ """
+ shield_dirs = []
+ if self._cmake:
+ shield_dirs = self._cmake.getstrs("SHIELD_DIRS")
+ if not shield_dirs:
+ shield_dirs = self._cmake.getstrs("CACHED_SHIELD_DIRS")
+ return shield_dirs
+
+ @property
+ def fw_name(self) -> Optional[str]:
+ """Application name.
+
+ Retrieved from the CMake cache (CMAKE_PROJECT_NAME).
+ """
+ if self._cmake:
+ return self._cmake.getstr("CMAKE_PROJECT_NAME")
+ return None
+
+ @property
+ def fw_version(self) -> Optional[str]:
+ """Application version.
+
+ Retrieved from the CMake cache (CMAKE_PROJECT_VERSION).
+ """
+ if self._cmake:
+ return self._cmake.getstr("CMAKE_PROJECT_VERSION")
+ return None
+
+ @property
+ def zephyr_base(self) -> Optional[str]:
+ """Path to Zephyr repository.
+
+ Either:
+
+ - retrieved from the CMake cache (ZEPHYR_BASE)
+ - or retrieved from the shell environment (ZEPHYR_BASE)
+ - or set according the expected file layout ZEPHYR_BASE/scripts/dts/dtsh
+ - or unvailable
+ """
+ return self._zephyr_base
+
+ @property
+ def zephyr_sdk_dir(self) -> Optional[str]:
+ """Path to Zephyr SDK.
+
+ Either retrieved from the CMake cache or the shell
+ environment (ZEPHYR_SDK_INSTALL_DIR).
+ """
+ zephyr_sdk_dir = None
+ if self._cmake:
+ zephyr_sdk_dir = self._cmake.getstr("ZEPHYR_SDK_INSTALL_DIR")
+ if not zephyr_sdk_dir:
+ zephyr_sdk_dir = os.environ.get("ZEPHYR_SDK_INSTALL_DIR")
+ return zephyr_sdk_dir
+
+ @property
+ def toolchain_variant(self) -> Optional[str]:
+ """Zephyr build toolchain variant.
+
+ Either retrieved from the CMake cache or the shell
+ environment (ZEPHYR_TOOLCHAIN_VARIANT).
+ """
+ toolchain_variant = None
+ if self._cmake:
+ toolchain_variant = self._cmake.getstr("ZEPHYR_TOOLCHAIN_VARIANT")
+ if not toolchain_variant:
+ toolchain_variant = os.environ.get("ZEPHYR_TOOLCHAIN_VARIANT")
+ return toolchain_variant
+
+ @property
+ def toolchain_dir(self) -> Optional[str]:
+ """Path to build toolchain.
+
+ Depends on toolchain variant:
+
+ - "zephyr": path to the Zephyr SDK
+ - other variants: build system variable {TOOLCHAIN}_TOOLCHAIN_PATH
+ (CMake cache or environment)
+ """
+ toolchain_dir = None
+ if self.toolchain_variant:
+ if self.toolchain_variant == "zephyr":
+ toolchain_dir = self.zephyr_sdk_dir
+ else:
+ var = f"{self.toolchain_variant.upper()}_TOOLCHAIN_PATH"
+ if self._cmake:
+ toolchain_dir = self._cmake.getstr(var)
+ if not toolchain_dir:
+ toolchain_dir = os.environ.get(var)
+ return toolchain_dir
+
+ def _init_cmake_cache(self) -> Optional["CMakeCache"]:
+ # Is the CMake cache available at ../CMakeCache.txt from the DTS file ?
+ path = os.path.join(self.app_binary_dir, "CMakeCache.txt")
+ if os.path.isfile(path):
+ return CMakeCache.open(path)
+ return None
+
+ def _init_zephyr_base(self) -> Optional[str]:
+ if self._cmake:
+ zephyr_base = self._cmake.getstr("ZEPHYR_BASE")
+ else:
+ zephyr_base = os.environ.get("ZEPHYR_BASE")
+ if not zephyr_base:
+ # dtsh/src/dtsh/__file__
+ dtsh_base = os.path.dirname(
+ os.path.dirname(os.path.dirname(__file__))
+ )
+ # ZEPHYR_BASE/scripts/dts/dtsh
+ zephyr_base = os.path.dirname(
+ os.path.dirname(os.path.dirname(dtsh_base))
+ )
+ return zephyr_base
+
+ def _init_vendors_file(self, vendors_file: Optional[str]) -> Optional[str]:
+ if (not vendors_file) and self._zephyr_base:
+ vendors_file = os.path.join(
+ self._zephyr_base, "dts", "bindings", "vendor-prefixes.txt"
+ )
+ return vendors_file
+
+ def _init_binding_dirs(
+ self, binding_dirs: Optional[Sequence[str]]
+ ) -> List[str]:
+ if binding_dirs:
+ binding_dirs = [os.path.abspath(path) for path in binding_dirs]
+ else:
+ if self._cmake:
+ binding_dirs = self._cmake.getstrs("CACHED_DTS_ROOT_BINDINGS")
+ if not binding_dirs:
+ # Fallback to "Where bindings are located".
+ # NOTE: Possible missing DTS roots ?
+ #
+ # - any directories manually included in the
+ # DTS_ROOT CMake variable
+ # - any module that defines a dts_root in its Build settings
+ #
+ # IIUC, this might be a non-issue, since either the CMake cache:
+ # - is available, and we should be able to get all bindings
+ # from the CACHED_DTS_ROOT_BINDINGS value
+ # - is unavailable, and we won't access any build settings
+ dts_roots: List[Optional[str]] = [
+ self.app_source_dir,
+ self.board_dir,
+ *self.shield_dirs,
+ self.zephyr_base,
+ ]
+ binding_dirs = [
+ os.path.join(dtsroot, "dts", "bindings")
+ for dtsroot in dts_roots
+ if dtsroot
+ ]
+ # cast() is required to avoid type hinting error since
+ # binding_dirs is first typed as an optional Sequence.
+ return cast(List[str], binding_dirs)
+
+
+class YAMLFilesystem:
+ """Find YAML files within a fixed search path (set of directories).
+
+ Rationale:
+
+ - retrieve YAML files with their base name
+ - provide the name2path semantic expected for edtlib.Binding
+ objects initialization
+ """
+
+ _name2path: Dict[str, str]
+
+ def __init__(self, yaml_dirs: Sequence[str]) -> None:
+ """Initialize the YAML file system.
+
+ Args:
+ yaml_dirs: The YAML search path as a set of absolute or relative
+ directory paths.
+ """
+ self._name2path = {}
+ for yaml_dir in [os.path.abspath(path) for path in yaml_dirs]:
+ for root, _, basenames in os.walk(yaml_dir):
+ for name in basenames:
+ if name.endswith((".yaml", ".yml")):
+ self._name2path[name] = os.path.join(root, name)
+
+ @property
+ def name2path(self) -> Mapping[str, str]:
+ """Mapping from YAML base names to absolute file paths.
+
+ This mapping contains all YAML files within this file system.
+ """
+ return self._name2path
+
+ def find_path(self, name: str) -> Optional[str]:
+ """Find a YAML file by name.
+
+ Args:
+ name: The base name of a YAML file.
+
+ Returns:
+ The absolute path to the requested YAML file,
+ or None if not found.
+ """
+ return self._name2path.get(name)
+
+ def find_file(self, name: str) -> Optional["YAMLFile"]:
+ """Find a YAML file by name.
+
+ Args:
+ name: The base name of a YAML file.
+
+ Returns:
+ A wrapper to the requested YAML file,
+ or None if not found.
+ """
+ path = self.find_path((name))
+ return YAMLFile(path) if path else None
+
+
+class CMakeCache:
+ """CMake cache reader.
+
+ Adapted from zcmake.CMakeCache.
+ """
+
+ _entries: Dict[str, "CMakeCacheEntry"]
+
+ @classmethod
+ def open(cls, path: str) -> Optional["CMakeCache"]:
+ """Open a CMake cache file for reading.
+
+ Args:
+ path: Path to the CMake cache file (CMakeCache.txt) to open.
+
+ Returns:
+ The CMakeCache content.
+ """
+ try:
+ return CMakeCache(path)
+ except OSError as e:
+ print(f"CMakeCache file error: {e}", file=sys.stderr)
+ except ValueError as e:
+ print(f"CMakeCache content error: {e}", file=sys.stderr)
+ return None
+
+ def __init__(self, path: str) -> None:
+ """Open a CMake cache file.
+
+ Args:
+ path: Path to the CMake cache file (CMakeCache.txt) to open.
+
+ Raises:
+ OSError: CMakeCache file error.
+ ValueError: CMakeCache content error.
+ """
+ with open(path, "r", encoding="utf-8") as cache:
+ entries = [
+ CMakeCacheEntry.from_line(line, line_no)
+ for line_no, line in enumerate(cache)
+ ]
+ self._entries = {entry.name: entry for entry in entries if entry}
+
+ def get(self, name: str) -> Optional["CMakeCacheEntry.ValueType"]:
+ """Access a cache entry by name.
+
+ Arg:
+ name: Cache entry name.
+
+ Returns:
+ The raw cache entry value, or None if not set.
+ """
+ if name in self._entries:
+ return self._entries[name].value
+ return None
+
+ def getbool(self, name: str) -> bool:
+ """Access a cache entry as boolean.
+
+ Arg:
+ name: Cache entry name.
+
+ Returns:
+ True if the entry is of type boolean and set ("ON", "YES", etc),
+ false otherwise.
+ """
+ val = self.get(name)
+ # May not be Pythonic, but is consistent with type hinting.
+ return val is True
+
+ def getstr(self, name: str) -> Optional[str]:
+ """Access a cache entry as string.
+
+ Arg:
+ name: Cache entry name.
+
+ Returns:
+ A string value if the entry exists and is actually of type string,
+ None otherwise.
+ """
+ val = self.get(name)
+ if val and isinstance(val, str):
+ return val
+ return None
+
+ def getstrs(self, name: str) -> List[str]:
+ """Access a cache entry as a list of strings.
+
+ Arg:
+ name: Cache entry name.
+
+ Returns:
+ A list of string values if the entry exists and is either of type
+ string or of type list of strings, an empty list otherwise.
+ """
+ val = self.get(name)
+ if val and isinstance(val, str):
+ return [val]
+ if isinstance(val, list):
+ # Assuming list of string.
+ return val
+ return []
+
+ def __contains__(self, name: str) -> bool:
+ """Map protocol."""
+ return name in self._entries
+
+ def __getitem__(self, name: str) -> "CMakeCacheEntry.ValueType":
+ """Map protocol."""
+ return self._entries[name].value
+
+ def __iter__(self) -> Iterator[str]:
+ """Iterate on the CMake cache entries."""
+ return iter(self._entries.keys())
+
+ def __len__(self) -> int:
+ """Number of entries in this CMake cache."""
+ return len(self._entries)
+
+
+class CMakeCacheEntry:
+ """CMake cache entry.
+
+ This class understands the type system in a CMakeCache.txt, and
+ converts the following cache types to Python types:
+
+ Cache Type Python type
+ ---------- -------------------------------------------
+ FILEPATH str
+ PATH str
+ STRING str OR list of str (if ';' is in the value)
+ BOOL bool
+ INTERNAL str OR list of str (if ';' is in the value)
+ STATIC str OR list of str (if ';' is in the value)
+ UNINITIALIZED str OR list of str (if ';' is in the value)
+ ---------- -------------------------------------------
+
+ Adapted from zcmake.CMakeCacheEntry.
+ """
+
+ # Regular expression for a cache entry.
+ #
+ # CMake variable names can include escape characters, allowing a
+ # wider set of names than is easy to match with a regular
+ # expression. To be permissive here, use a non-greedy match up to
+ # the first colon (':'). This breaks if the variable name has a
+ # colon inside, but it's good enough.
+ CACHE_ENTRY = re.compile(
+ r"""(?P.*?)
+ :(?PFILEPATH|PATH|STRING|BOOL|INTERNAL|STATIC|UNINITIALIZED)
+ =(?P.*)
+ """,
+ re.X,
+ )
+
+ ValueType = Union[str, List[str], bool]
+
+ _name: str
+ _value: "CMakeCacheEntry.ValueType"
+
+ @classmethod
+ def from_line(cls, line: str, line_no: int) -> Optional["CMakeCacheEntry"]:
+ """Create an entry from a cache line.
+
+ Args:
+ line: The cache line content.
+ line_no: The cache file line number (used for error reporting).
+
+ Returns:
+ A cache entry or `None` if the line was a comment, empty,
+ or malformed.
+
+ Raises:
+ ValueError: Failed conversion to bool.
+ """
+ # Comments can only occur at the beginning of a line.
+ # (The value of an entry could contain a comment character).
+ if line.startswith("//") or line.startswith("#"):
+ return None
+
+ # Whitespace-only lines do not contain cache entries.
+ if not line.strip():
+ return None
+
+ m = cls.CACHE_ENTRY.match(line)
+ if not m:
+ return None
+
+ name, type_, value = (m.group(g) for g in ("name", "type", "value"))
+ if type_ == "BOOL":
+ try:
+ value = cls._to_bool(value)
+ except ValueError as exc:
+ args = exc.args + (f"on line {line_no}: {line}",)
+ raise ValueError(args) from exc
+ elif type_ in {"STRING", "INTERNAL", "STATIC", "UNINITIALIZED"}:
+ # If the value is a CMake list (i.e. is a string which
+ # contains a ';'), convert to a Python list.
+ if ";" in value:
+ value = value.split(";")
+
+ return CMakeCacheEntry(name, value)
+
+ @classmethod
+ def _to_bool(cls, val: str) -> bool:
+ # Convert a CMake BOOL string into a Python bool.
+ #
+ # "True if the constant is 1, ON, YES, TRUE, Y, or a
+ # non-zero number. False if the constant is 0, OFF, NO,
+ # FALSE, N, IGNORE, NOTFOUND, the empty string, or ends in
+ # the suffix -NOTFOUND. Named boolean constants are
+ # case-insensitive. If the argument is not one of these
+ # constants, it is treated as a variable."
+ #
+ # https://cmake.org/cmake/help/v3.0/command/if.html
+ val = val.upper()
+ if val in ("ON", "YES", "TRUE", "Y"):
+ return True
+ if val in ("OFF", "NO", "FALSE", "N", "IGNORE", "NOTFOUND", ""):
+ return False
+ if val.endswith("-NOTFOUND"):
+ return False
+ try:
+ v = int(val)
+ return v != 0
+ except ValueError as e:
+ raise ValueError(f"not a bool: {val}") from e
+
+ def __init__(self, name: str, value: "CMakeCacheEntry.ValueType") -> None:
+ """Initialize a new cache entry.
+
+ Args:
+ name: Entry name.
+ value: Entry value.
+ """
+ self._name = name
+ self._value = value
+
+ @property
+ def name(self) -> str:
+ """Cache entry name."""
+ return self._name
+
+ @property
+ def value(self) -> "CMakeCacheEntry.ValueType":
+ """Cache entry raw value."""
+ return self._value
+
+ def __repr__(self) -> str:
+ return f"{self._name}: {self._value}"
+
+
+class YAMLFile:
+ """Cheap wrapper around a YAML file.
+
+ This API is:
+
+ - lazy-initialized: properties are initialized when accessed
+ - fail-safe: errors are logged once to stderr, and empty values are returned
+ """
+
+ # Absolute file path.
+ _path: str
+
+ # Lazy-initialized file content.
+ _content: Optional[str]
+
+ # Lazy-initialized YAML model.
+ _raw: Optional[Dict[str, object]]
+
+ # Lazy-initialized YAML "include: ".
+ _includes: Optional[List[str]]
+
+ def __init__(self, path: str) -> None:
+ """Lazy-initialize wrapper.
+
+ Only the YAML file path is set upon initialization.
+
+ Then accessing:
+
+ - the *includes* will require initializing the YAML model
+ - the YAML model will require loading the YAML file content
+
+ Args:
+ path: Absolute path to a YAML file.
+ """
+ self._path = path
+ # Lazy-initialized.
+ self._content = None
+ self._raw = None
+ self._includes = None
+
+ @property
+ def path(self) -> str:
+ """Absolute file path."""
+ return self._path
+
+ @property
+ def content(self) -> str:
+ """YAML file content."""
+ # Will Initialize an empty content if fails to load the YAML file.
+ self._init_content()
+ return self._content # type: ignore
+
+ @property
+ def raw(self) -> Dict[str, object]:
+ """YAML model."""
+ # Will Initialize an empty model if the YAML file content unavailable.
+ self._init_model()
+ return self._raw # type: ignore
+
+ @property
+ def includes(self) -> Sequence[str]:
+ """Names of included YAML files."""
+ # Will Initialize an empty list if the YAML model is unavailable.
+ self._init_includes()
+ return self._includes # type: ignore
+
+ def _init_content(self) -> None:
+ if self._content is not None:
+ return
+ # Only one attempt to initialize content.
+ self._content = ""
+
+ try:
+ with open(self._path, mode="r", encoding="utf-8") as f:
+ self._content = f.read().strip()
+ except OSError as e:
+ print(f"YAML: {e}", file=sys.stderr)
+
+ def _init_model(self) -> None:
+ if self._raw is not None:
+ return
+ # Only one attempt to initialize model.
+ self._raw = {}
+
+ # Depends on YAML content.
+ self._init_content()
+ if not self._content:
+ return
+
+ try:
+ self._raw = yaml.load(self._content, Loader=YAMLBindingLoader)
+ except yaml.YAMLError as e:
+ print(f"YAML: {self._path}: {e}", file=sys.stderr)
+
+ def _init_includes(self) -> None:
+ if self._includes is not None:
+ return
+ # Only one attempt to initialize includes.
+ self._includes = []
+
+ # Depends on YAML model.
+ self._init_model()
+ if not self._raw:
+ return
+
+ # See edtlib.Binding._merge_includes()
+ yaml_inc = self._raw.get("include")
+ if isinstance(yaml_inc, str):
+ self._includes.append(yaml_inc)
+ elif isinstance(yaml_inc, list):
+ for inc in yaml_inc:
+ if isinstance(inc, str):
+ self._includes.append(inc)
+ elif isinstance(inc, dict):
+ basename = inc.get("name")
+ if basename:
+ self._includes.append(basename)
diff --git a/src/dtsh/dtsh.ini b/src/dtsh/dtsh.ini
new file mode 100644
index 0000000..104e05f
--- /dev/null
+++ b/src/dtsh/dtsh.ini
@@ -0,0 +1,367 @@
+# Copyright (c) 2022 Christophe Dufaza
+#
+# SPDX-License-Identifier: Apache-2.0
+
+# Devicetree shell default configuration.
+#
+# Configuration files are written in standard Python configparser
+# format (aka Microsoft Windows INI files).
+
+[dtsh]
+# Devicetree shell configuration.
+#
+# String values:
+# - double-quote when ending with trailing spaces
+# - supports 'u' escape sequences followed by four hex digits
+# giving an Unicode code point (e.g. "\u2768")
+#
+# Boolean values:
+# - True: '1', 'yes', 'true', and 'on'
+# - False: '0', 'no', 'false', and 'off'
+#
+# Integer values:
+# - base-2, -8, -10 and -16 are supported
+# - if not base 10, the actual base is determined by the prefix
+# "0b/0B" (base-2), "0o/0O" (base-8), or "0x/0X" (base-16)
+#
+# Float values:
+# - decimal notation: e.g. 0.1
+# - scientific notation: e.g. 1e-1
+#
+# Option values support (extended) interpolation:
+# hello = hello
+# hello_world = ${hello} world
+#
+# Use a $$ to escape the dollar sign:
+# dollar = $$
+
+
+################################################################################
+# UNICODE Symbols.
+
+# Ellipsis.
+# Default: u2026 ("…")
+wchar.ellipsis = \u2026
+
+# North-East Arrow.
+# Default: u2197 ("↗")
+wchar.arrow_ne = \u2197
+
+# Notrh-West Arrow.
+# Default: u2196 ("↖")
+wchar.arrow_nw = \u2196
+
+# Rightwards arrow.
+# Default: u2192 ("→")
+wchar.arrow_right = \u2192
+
+# Rightwards arrow with hook.
+# Default: u21B3 ("↳")
+wchar.arrow_right_hook = \u21B3
+
+# Tiret.
+# Default: u2014 ("—").
+wchar.dash = \u2014
+
+# Link.
+# Default: u1f517 ("🔗")
+wchar.link = \u1F517
+
+
+################################################################################
+# Stateful ANSI prompt.
+#
+# Shell prompt:
+# - ANSI: may contain ANSI escape sequences (Select Graphic Rendition, SGR)
+# - support an alternative value, e.g. for changing the prompt color
+# when the last command has failed
+#
+# To use ANSI escape codes in `input()` without breaking
+# the GNU readline cursor position, please protect SGR parameters
+# with RL_PROMPT_{START,STOP}_IGNORE markers:
+#
+# := m
+# :=
+#
+# := '\001'
+# := '\002'
+#
+# := ESC[
+# := \x1b[
+# := \033[
+#
+# Additionally, both prompts should occupy the same physical space
+# on the terminal screen: i.e. they should involve the same number
+# of characters outside of these RL markers.
+#
+# For example, a bold (1) 8-bit color (38;5;N;1) stateful prompt:
+#
+# default: "\001\x1b[38;5;99;1m\002>\001\x1b[0m\002 "
+#
+# alt: "\001\x1b[38;5;88;1m\002>\001\x1b[0m\002 "
+#
+# See:
+# - ANSI/VT100 Terminal Control Escape Sequences
+# https://www2.ccs.neu.edu/research/gpc/VonaUtils/vona/terminal/vtansi.htm
+# - How to fix column calculation in Python readline if using color prompt
+# https://stackoverflow.com/questions/9468435
+# - ANSI escape code
+# https://en.wikipedia.org/wiki/ANSI_escape_code
+
+# Prompt character (or string, actually) from which
+# are derived the default ANSI prompts.
+#
+# Common UTF-8 prompt characters:
+#
+# - Single Right-Pointing Angle Quotation Mark: u203a (›)
+# - Medium Right-Pointing Angle Bracket Ornament: u276d (❭)
+# - Heavy Right-Pointing Angle Bracket Ornament: u2771 (❱)
+# - Heavy Right-Pointing Angle Quotation Mark Ornament: u276f (❯)
+# - Right-Pointing Curved Angle Bracket: u29fd (⧽)
+# - BLACK RIGHT-POINTING TRIANGLE: u25b6 (▶)
+# - Right shaded arrow \u27a9 (➩)
+#
+# Or simply:
+# - ">"
+# - "$"
+# - "dtsh:"
+#
+# Type: String
+#
+# Default: "\u276D"
+prompt.wchar = \u276D
+
+# Default ANSI prompt.
+#
+# Note: the trailing space is intentional but optional.
+#
+# Type: String
+prompt.default = "\001\x1b[38;5;99m\002${prompt.wchar}\001\x1b[0m\002 "
+
+# Alternative prompt, e.g. after a command has failed.
+#
+# Note: the trailing space is intentional but optional.
+#
+# Type: String
+prompt.alt = "\001\x1b[38;5;88m\002${prompt.wchar}\001\x1b[0m\002 "
+
+# Whether to append an empty line after commands output.
+# Type: Bool
+# Default: True
+prompt.sparse = yes
+
+
+# Whether to assume the "use a long listing format" flag (-l) flag is always set.
+# Type: Bool
+# Default: True
+pref.always_longfmt = no
+
+# Maximum width in number characters for commands output redirection.
+# Type: Integer
+# Default: 255
+pref.redir2_maxwidth = 255
+
+# Whether to print sizes with SI units (bytes, kB, MB).
+# Otherwise, sizes are printed in hexadecimal format.
+# Type: Bool
+# Default: True
+pref.sizes_si = yes
+
+# Whether to print hexadecimal digits upper case,
+# e.g. "0xFF" rather than "0xff".
+#
+# Type: Bool
+# Default: False
+pref.hex_upper = no
+
+
+# Whether to hide files and directories whose
+# name starts with "." (e.g. when completing file paths).
+#
+# Type: Bool
+# Default: True
+pref.fs.hide_dotted = yes
+
+# Whether to forbid spaces in redirection file paths.
+#
+# Type: Bool
+# Default: True
+pref.fs.no_spaces = yes
+
+# Whether to forbid command output redirection
+# to overwrite existing files.
+#
+# Type: Bool
+# Default: True
+pref.fs.no_overwrite = yes
+
+
+# List views: whether to show the headers.
+#
+# Type: Bool
+# Default: True
+pref.list.headers = yes
+
+# List views: placeholder for missing values.
+#
+# Type: String
+# Default: Unset (no place holder)
+pref.list.place_holder =
+
+# List views: default format string for node fields.
+#
+# Type: String
+# Default: NLC
+pref.list.fmt = NLC
+
+# List views: rendering for actionable texts (aka links).
+#
+# Type: String
+# - "none": do not create hyperlinks
+# - "link" (default): link text like browsers do
+# - "alt": append alternative actionable view
+pref.list.actionable_type = link
+
+# List views: whether to allow multiple-line cells.
+#
+# Type: Bool
+# Default: True
+pref.list.multi = no
+
+# Tree views: whether to show the headers.
+#
+# Type: Bool
+# Default: True
+pref.tree.headers = yes
+
+# Tree views: placeholder for missing values.
+#
+# Type: String
+# Default: Ellipsis
+pref.tree.place_holder = ${wchar.ellipsis}
+
+# Tree views: default format string for node fields.
+#
+# The first field specifies anchors,
+# the remaining fields the list view columns
+# of 2-sided views.
+#
+# Type: String
+# Default: Nd
+pref.tree.fmt = Nd
+
+# Tree views: rendering for actionable texts (aka links)
+# in anchors.
+#
+# Type: String
+# - "none": do not create hyperlinks
+# - "link" (default): link text like browsers do
+# - "alt": append alternative actionable view
+pref.tree.actionable_type = none
+
+# Tree views: rendering for actionable texts (aka links)
+# in the left list view of a 2-sided (long format).
+#
+# Type: String
+# - "none": do not create hyperlinks
+# - "link" (default): link text like browsers do
+# - "alt": append alternative actionable view
+pref.2sided.actionable_type = link
+
+
+# Symbol to anchor child-bindings to their parent in tree-views.
+# Set it to an empty value to disable.
+#
+# Type: String
+# Default: Rightwards arrow with hook.
+pref.tree.cb_anchor = ${wchar.arrow_right_hook}
+
+
+# Default rendering for actionable texts (aka links).
+#
+# Type: String
+# - "none": do not create hyperlinks
+# - "link" (default): link text like browsers do
+# - "alt": append alternative actionable view
+pref.actionable_type = link
+
+# Alternative actionable text.
+#
+# This is the appended text element that will
+# actually be actionable.
+# Depending on availability, one may try:
+# - Link symbol (U+1F517)
+# - External link symbol (not yet standardized,
+# e.g. U+F08E AwesomeFont)
+#
+# Type: String
+# Default: "[North-East Arrow]"
+pref.actionable_wchar = [${wchar.arrow_ne}]
+
+
+# Command output redirection to HTML: theme.
+#
+# Configure text and background colors for HTML documents.
+#
+# Possible values:
+# - "svg": default theme for SVG documents (dark bakground, light text)
+# - "html": default theme for HTML documents (dark bakground, light text)
+# - "dark": darker
+# - "light": lighter
+# - "night": darkest
+#
+# Type: String
+# Default: default
+pref.html.theme = html
+
+# Command output redirection to HTML: font family.
+# This the family name, e.g. "Source Code Pro".
+#
+# Note:
+# - multiple coma separated values allowed,
+# e.g. "Source Code Pro, Courier New"
+# - the generic "monospace" family is automatically appended last
+# - the "Courier New" default font family is installed nearly "everywhere",
+# but may appear a bit dull, and might not support the box drawing
+# characters range that make trees sharp
+#
+# Type: String
+# Default: Courier New
+pref.html.font_family = Courier New
+
+# Command output redirection to SVG: theme.
+#
+# Configure text and background colors for SVG documents.
+#
+# Possible values:
+# - "svg": default theme for SVG documents (dark bakground, light text)
+# - "html": default theme for HTML documents (dark bakground, light text)
+# - "dark": darker
+# - "light": lighter
+# - "night": darkest
+#
+# Type: String
+# Default: default
+pref.svg.theme = svg
+
+# Command output redirection to SVG: font family.
+# This the family name, e.g. "Source Code Pro".
+#
+# Note:
+# - multiple coma separated values allowed,
+# e.g. "Source Code Pro, Courier New"
+# - the generic "monospace" family is automatically appended last
+# - the "Courier New" default font family is installed nearly "everywhere",
+# but may appear a bit dull, and might not support the box drawing
+# characters range that make trees sharp
+#
+# Type: String
+# Default: Courier New
+pref.svg.font_family = Courier New
+
+# Command output redirection to SVG: font aspect ratio.
+# This is the width to height ratio, typically 3:5.
+#
+# Type: Float
+# Default: 0.6
+pref.svg.font_ratio = 0.6
diff --git a/src/dtsh/dtsh.py b/src/dtsh/dtsh.py
deleted file mode 100644
index b793756..0000000
--- a/src/dtsh/dtsh.py
+++ /dev/null
@@ -1,1627 +0,0 @@
-# Copyright (c) 2022 Chris Duf
-#
-# SPDX-License-Identifier: Apache-2.0
-
-"""Devicetree shell core API."""
-
-
-import getopt
-import os
-import re
-from pathlib import Path
-
-from abc import abstractmethod
-from typing import Any, ClassVar, Dict, List, Optional, Tuple, Union
-
-from devicetree.edtlib import EDT, EDTError, Node, Binding, Property
-
-from dtsh.systools import Git, CMakeCache, GCCArm
-
-
-class DtshVt(object):
- """Devicetree shells standard I/O API.
- """
-
- @abstractmethod
- def write(self, *args, **kwargs) -> None:
- """Write (aka print) to stdout.
-
- Arguments:
- args -- positional arguments for the underlying implementation
- kwargs -- keyword arguments for the underlying implementation
- """
-
- @abstractmethod
- def pager_enter(self) -> None:
- """Enter pager context.
-
- Output will be paged until a call to pager_exit().
- """
-
- @abstractmethod
- def pager_exit(self) -> None:
- """Exit pager context.
- """
-
- @abstractmethod
- def clear(self) -> None:
- """Clear stdout.
- """
-
- @abstractmethod
- def readline(self, prompt: str) -> str:
- """Read a line from stdin.
-
- Will block until ENTER or EOF.
-
- Arguments:
- prompt -- the prompt to use (see interactive sessions)
-
- Returns the next line from stdin, with leading and trailing spaces
- striped.
-
- Raises EOFError when the input stream meets an EOF character.
- """
-
- @abstractmethod
- def abort(self) -> None:
- """Abort current I/O, since we're either writing to stdout,
- or reading from stdin.
- """
-
-
-class DtshCommandOption(object):
- """Devicetree shell command option.
-
- Option definitions are compatible with GNU getopt.
-
- An option that does not expect any value is a boolean flag.
-
- An option that expects a named value is an argument.
-
- An option may admit a short name (e.g. 'v'),
- and/or a long name (e.g. 'verbose').
- """
-
- _desc: str
- _shortname: Union[str, None]
- _longname: Union[str, None]
- _arg: Union[str, None]
- _value: Union[str, bool, None]
-
- def __init__(self,
- desc: str,
- shortname: Optional[str],
- longname: Optional[str],
- arg: Optional[str] = None) -> None:
- """Define a command option.
-
- Arguments:
- desc -- short description, e.g. 'use a long listing format'
- shortname -- short option name (e.g. the 'v' in '-v),
- or None if the option does not admit a short name
- longname -- long option name (e.g. 'verbose' in '--verbose'),
- or None if the option does not admit a long name
- arg -- the argument name (e.g. in '-a i2c0', the argument name could
- be 'alias'), or None if the option is a flag
- """
- self._desc = desc
- self._shortname = shortname
- self._longname = longname
- self._arg = arg
- self._value = None
-
- @property
- def desc(self) -> str:
- """The option's description.
- """
- return self._desc
-
- @property
- def shortname(self) -> Union[str, None]:
- """The option's short name, e.g. 'v'.
-
- This name does not include the '-' prefix,
- neither the ':' postfix when an argument is expected.
-
- Retutns the option's short name, or None if the option does not admit
- a short name.
- """
- return self._shortname
-
- @property
- def longname(self) -> Union[str, None]:
- """The option's long name, e.g. 'verbose'.
-
- This name does not include the '--' prefix,
- neither the '=' postfix when an argument is expected.
-
- Returns the option's long name, or None if the option does not admit
- a long name.
- """
- return self._longname
-
- @property
- def argname(self) -> Union[str, None]:
- """The option's argument name, or None if the options is a flag.
- """
- return self._arg
-
- @property
- def usage(self) -> str:
- """The option's usage string, e.g. '-a -v --verbose'.
- """
- txt = ''
- if self._shortname:
- txt = f"-{self._shortname}"
- if self._longname:
- if txt:
- txt += f" --{self._longname}"
- else:
- txt = f"--{self._longname}"
- if self._arg:
- txt += f" <{self._arg}>"
- return txt
-
- @property
- def value(self) -> Union[str, bool, None]:
- """The option's value.
-
- Before a command's options are parsed, this value is None.
-
- After the command string is successfully parsed, an option value is:
- - True (set) or False (unset) for a flag
- - a string value for an argument
- """
- return self._value
-
- @value.setter
- def value(self, v: Union[bool, str]) -> None:
- """Set the option's value.
-
- The options values are typically set when parsing a command string.
-
- Arguments:
- v -- True (False) to set (unset) a flag,
- or a string value to set an argument
- """
- self._value = v
-
- def is_flag(self) -> bool:
- """Returns True if the option does not expect any value.
- """
- return self._arg is None
-
- def reset(self):
- """Reset this option's value, typically before parsing a command string.
- """
- self._value = None
-
-
-class DtshCommandFlagHelp(DtshCommandOption):
- """Common "help" flag ('-h', '--help).
-
- If True, print usage summary.
- """
-
- def __init__(self) -> None:
- super().__init__('print usage summary', 'h', 'help', None)
-
-
-class DtshCommandFlagLongFmt(DtshCommandOption):
- """Common "long format" flag ('-l')
-
- If True, use long (aka rich) listing format.
- """
-
- def __init__(self) -> None:
- super().__init__('use long (rich) listing format', 'l', None, None)
-
-
-class DtshCommandArgLongFmt(DtshCommandOption):
- """Common "long format" command argument ('-f')
-
- Specifies columns for a node table.
- """
-
- def __init__(self) -> None:
- super().__init__('visible columns format string', 'f', None, 'fmt')
-
-
-class DtshCommandFlagPager(DtshCommandOption):
- """Common "page output" flag ('--pager')
-
- If True, page command output.
- """
-
- def __init__(self) -> None:
- super().__init__('page command output', None, 'pager', None)
-
-
-class DtshCommand(object):
- """Devicetree shell command.
- """
-
- # Name, e.g. 'ls'.
- _name: str
- # Description, e.g. 'list nodes content'.
- _desc: str
- # Supported options.
- _options: List[DtshCommandOption]
- # Parsed parameters (command string components that are not parsed options).
- _params: List[str]
-
- def __init__(self,
- name: str,
- desc: str,
- with_pager: bool = False,
- options: List[DtshCommandOption] = []) -> None:
- """Defines a devicetree shell command.
-
- Arguments:
- name -- the command's name (e.g. 'ls')
- desc -- the command's description
- with_pager -- if True, enables pager option support
- options -- the command's options
- """
- self._name = name
- self._desc= desc
- self._params = []
- self._options = []
- self._options.extend(options)
- if with_pager:
- self._options.append(DtshCommandFlagPager())
- self._options.append(DtshCommandFlagHelp())
-
- @property
- def name(self) -> str:
- """Command's name, e.g. 'ls'.
- """
- return self._name
-
- @property
- def desc(self) -> str:
- """Command's description, e.g. 'list nodes content'.
- """
- return self._desc
-
- @property
- def usage(self) -> str:
- """The command's usage string, ' [options]'.
- """
- txt = self._name
- for opt in self._options:
- txt += f" [{opt.usage}]"
- return txt
-
- @property
- def options(self) -> List[DtshCommandOption]:
- """Available options.
- """
- return self._options
-
- @property
- def getopt_short(self) -> str:
- """Short options specification string compatible with GNU getopt.
-
- e.g. 'ha:' when the option supports a flag '-h',
- and an argument '-a:'.
- """
- shortopts = ''
- for opt in self._options:
- if opt.shortname:
- shortopts += opt.shortname
- if opt.argname:
- shortopts += ':'
- return shortopts
-
- @property
- def getopt_long(self) -> List[str]:
- """Long options specification list compatible with GNU getopt.
-
- e.g. ['help','alias='] when the option supports a flag '--help',
- and an argument '--alias='.
- """
- longopts = []
- for opt in self._options:
- if opt.longname:
- longopt = opt.longname
- if opt.argname:
- longopt += '='
- longopts.append(longopt)
- return longopts
-
- @property
- def with_pager(self) -> bool:
- return self.with_flag('--pager')
-
- @property
- def with_help(self) -> bool:
- return self.with_flag('-h')
-
- @property
- def with_longfmt(self) -> bool:
- return self.with_flag('-l')
-
- @property
- def arg_longfmt(self) -> Union[str, None]:
- return self.arg_value('-f')
-
- def option(self, name: str) -> Union[DtshCommandOption, None]:
- """Access a supported option.
-
- Arguments:
- name -- an option's name, either a short form (e.g. '-h'),
- or a long form (e.g. '--help')
-
- Returns None if the option is not supported by this command.
- """
- for opt in self._options:
- if name.startswith('--') and opt.longname:
- if name[2:] == opt.longname:
- return opt
- elif name.startswith('-') and opt.shortname:
- if name[1:] == opt.shortname:
- return opt
- return None
-
- def with_flag(self, name: str) -> bool:
- """Access a command's flag.
-
- Arguments:
- name -- the flag's name, either a short form (e.g. '-v'),
- or a long form (e.g. '--verbose')
-
- Returns True if name refers to a set flag, False otherwise.
- """
- opt = self.option(name)
- if opt:
- return opt.is_flag() and (opt.value == True)
- return False
-
- def arg_value(self, name) -> Union[str, None]:
- """Access an argument value.
-
- Arguments:
- name -- the option name
-
- Returns the argument value, None if the option was not provided on
- the command line, or if the option is a flag.
- """
- opt = self.option(name)
- # Note that parse_argv() would have failed if the argument
- # if actually defined as an argument (parse error otherwise).
- if opt and (not opt.is_flag()) and (opt.value is not None):
- return str(opt.value)
- return None
-
- def reset(self) -> None:
- """Reset command options and parameters.
- """
- for opt in self._options:
- opt.reset()
- self._params.clear()
-
- def parse_argv(self, argv: List[str]) -> None:
- """Parse command line arguments, setting options and parameters.
-
- Arguments:
- argv -- the command's arguments
-
- Raises DtshCommandUsageError when the arguments do not match the
- command's usage (getopt).
- """
- self.reset()
- try:
- parsed_opts, self._params = getopt.gnu_getopt(argv,
- self.getopt_short,
- self.getopt_long)
- except getopt.GetoptError as e:
- raise DtshCommandUsageError(self, str(e), e)
-
- for opt_name, opt_arg in parsed_opts:
- opt = self.option(opt_name)
- if opt:
- if opt.argname:
- opt.value = opt_arg
- else:
- opt.value = True
-
- if self.with_help:
- # Should print usage summary, see DevicetreeShellSession.run().
- raise DtshCommandUsageError(self)
-
- def autocomplete_option(self, prefix: str) -> List[DtshCommandOption]:
- """Auto-complete a command's options name.
-
- Arguments:
- prefix -- the option's name prefix, starting with '-' or '--'
-
- Returns a list of matching options.
- """
- completions: List[DtshCommandOption] = []
-
- if prefix == '-':
- # Match all options, sorting with short names first.
- shortopts = [o for o in self._options if o.shortname]
- completions.extend(shortopts)
- # Then options with long names only.
- otheropts = [
- o for o in self._options if o.longname and (o not in shortopts)
- ]
- completions.extend(otheropts)
-
- elif prefix.startswith('--'):
- # Auto-comp long option names only.
- p = prefix[2:]
- for opt in self._options:
- if not opt.longname:
- continue
- if not p:
- completions.append(opt)
- continue
- if opt.longname.startswith(p) and (len(opt.longname) > len(p)):
- completions.append(opt)
-
- return completions
-
- def autocomplete_argument(self,
- arg: DtshCommandOption, # pyright: ignore reportUnusedVariable
- prefix: str) -> List[str]: # pyright: ignore reportUnusedVariable
- """Auto-complete a command's option value (aka argument).
-
- Arguments:
- arg -- the option expecting a value
- prefix -- the option's name prefix, starting with '-' or '--'
-
- Returns a list of matching arguments.
- """
- return []
-
- def autocomplete_param(self, prefix: str) -> Tuple[int, List]: # pyright: ignore reportUnusedVariable
- """Auto-complete a command's parameter value.
-
- Completions are represented by the tagged list of possible
- parameter objects.
-
- The tag will help client code to interpret (type) these parameter values.
-
- Arguments:
- prefix -- the startswith pattern for parameter values
-
- Returns the tagged list of matching parameters as a tuple.
- """
- return DtshAutocomp.MODE_ANY, []
-
- @abstractmethod
- def execute(self, vt: DtshVt) -> None:
- """Execute the shell command.
-
- Arguments:
- vt -- where the command will write its output
-
- Raises DtshError when the command execution has failed.
- """
-
-
-class DtshUname(object):
- """System information inferred from environment variables,
- CMake cached variables and Zephyr's Git repository state.
-
- All paths are resolved (absolute, resolving any symlinks,
- “..” components are also eliminated).
- """
-
- # Resolved DTS file path.
- _dts_path: str
-
- # Resolved binding directories.
- _binding_dirs: List[str]
-
- # Resolved $ZEPHYR_BASE.
- _zephyr_base: Union[str, None]
-
- # Resolved $ZEPHYR_SDK_INSTALL_DIR.
- _zephyr_sdk_dir: Union[str, None]
-
- # Cached $ZEPHYR_SDK_INSTALL_DIR/sdk_version file content.
- _zephyr_sdk_version: Union[str, None]
-
- # Resolved $GNUARMEMB_TOOLCHAIN_PATH.
- _gnuarm_dir: Union[str, None]
-
- # $ZEPHYR_TOOLCHAIN_VARIANT ('gnuarmemb' or 'zephyr').
- _zephyr_toolchain: Union[str, None]
-
- # git -C $ZEPHYR_BASE log -n 1 --pretty=format:"%h"
- _zephyr_rev: Union[str, None]
-
- # git tag --points-at HEAD
- _zephyr_tags: List[str]
-
- # Resolved BOARD_DIR (CMake).
- _board_dir: Union[str, None]
-
- # CMake cached variables.
- _cmake_cache: CMakeCache
-
- def __init__(self, dts_path:str, binding_dirs: Optional[List[str]]) -> None:
- """Initialize system info.
-
- Arguments:
- dts_path -- Path to a devicetree source file.
- binding_dirs -- List of path to search for DT bindings.
- If unspecified, and ZEPHYR_BASE is set,
- defaults to Zephyr's DT bindings.
-
- Raises DtshError when a specified path is invalid.
- """
- try:
- self._dts_path = str(Path(dts_path).resolve(strict=True))
- except FileNotFoundError as e:
- raise DtshError(f"DTS file not found: {dts_path}", e)
-
- self._binding_dirs = []
- self._zephyr_tags = []
- self._zephyr_base = None
- self._zephyr_sdk_dir = None
- self._zephyr_sdk_version = None
- self._gnuarm_dir = None
- self._zephyr_toolchain = None
- self._zephyr_rev = None
- self._board_dir = None
-
- self._load_environment()
- self._load_cmake_cache()
-
- if self._zephyr_base:
- git = Git()
- self._zephyr_rev = git.get_head_commit(self._zephyr_base)
- self._zephyr_tags = git.get_head_tags(self._zephyr_base)
-
- if binding_dirs:
- for binding_dir in binding_dirs:
- path = Path(binding_dir).resolve()
- if os.path.isdir(path):
- self._binding_dirs.append(str(path))
- else:
- raise DtshError(f"Bindings directory not found: {binding_dir}")
- elif self._zephyr_base:
- self._init_zephyr_bindings_search_path()
-
- @property
- def dts_path(self) -> str:
- """Returns the resolved path to the session's DT source file.
- """
- return self._dts_path
-
- @property
- def dt_binding_dirs(self) -> List[str]:
- """Returns the DT bindings search path as a list of resolved path.
-
- When no bindings are specified by the dtsh command line,
- and the environment variable ZEPHYR_BASE is set,
- we'll try to default to the bindings Zephyr would use (has used)
- at build-time.
-
- "Where are bindings located ?" specifies that binding files are
- expected to be located in dts/bindings sub-directory of:
- - the zephyr repository
- - the application source directory
- - the board directory
- - any directories in DTS_ROOT
- - any module that defines a dts_root in its build
-
- Walking through the modules' build settings seems a lot of work
- (needs investigation, and confirmation that it's worth the effort),
- but we'll at least try to include:
- - $ZEPHYR_BASE/dts/bindings
- - APPLICATION_SOURCE_DIR/dts/bindings
- - BOARD_DIR/dts/bindings
- - DTS_ROOT/**/dts/bindings
-
- This implies we get the value of the CMake cached variables
- APPLICATION_SOURCE_DIR, BOARD_DIR and DTS_ROOT.
- To invoke CMake, we'll first need a value for APPLICATION_BINARY_DIR:
- we'll assume its the parent of the directory containing the DTS file,
- as in /build/zephyr/zephyr.dts.
-
- If that fails:
- - APPLICATION_SOURCE_DIR will default to $PWD
- - we will substitute BOARD_DIR/dts/bindings with $ZEPHYR_BASE/boards
- and $PWD/boards (we don't know if it's a Zephyr board or a custom board,
- we don't know wich //dts/bindings subdirectory to select)
-
- Only directories that actually exist are included.
-
- See:
- - $ZEPHYR_BASE/cmake/modules/dts.cmake
- - https://docs.zephyrproject.org/latest/build/dts/bindings.html#where-bindings-are-located
- """
- return self._binding_dirs
-
- @property
- def zephyr_base(self) -> Union[str, None]:
- """Returns the resolved path to the Zephyr kernel repository set by
- the environment variable ZEPHYR_BASE, or None if unset.
- """
- return self._zephyr_base
-
- @property
- def zephyr_toolchain(self) -> Union[str, None]:
- """Returns the toolchain variant ('zephyr' or 'gnuarmemb') set by the
- environment variable ZEPHYR_TOOLCHAIN_VARIANT, or None if unset.
- """
- return self._zephyr_toolchain
-
- @property
- def zephyr_sdk_dir(self) -> Union[str, None]:
- """Returns resolved path the Zephyr SDK directory set by the environment
- variable ZEPHYR_SDK_INSTALL_DIR, or None if unset.
- """
- return self._zephyr_sdk_dir
-
- @property
- def gnuarm_dir(self) -> Union[str, None]:
- """Value of the environment variable GNUARMEMB_TOOLCHAIN_PATH, or None.
- """
- """Returns the GCC Arm base directory set by the environment variable
- GNUARMEMB_TOOLCHAIN_PATH, or None if unset.
- """
- return self._gnuarm_dir
-
- @property
- def zephyr_kernel_rev(self) -> Union[str, None]:
- """Returns the Zephyr kernel revision as given by
- git -C $ZEPHYR_BASE log -n 1 --pretty=format:"%h",
- or None when unavailable.
- """
- return self._zephyr_rev
-
- @property
- def zephyr_kernel_tags(self) -> List[str]:
- """Returns the Zephyr kernel tags for the current
- repository state, as given by git tag --points-at HEAD,
- or None when unavailable.
- """
- return self._zephyr_tags
-
- @property
- def zephyr_kernel_version(self) -> Union[str, None]:
- """Returns the Zephyr kernel version tag for the current
- repository state, e.g. 'zephyr-v3.1.0',
- or None if the state does not match a tagged Zephyr kernel release.
- """
- version = None
- if self.zephyr_kernel_tags:
- # Include stable and RC releases.
- regex = re.compile(r'^zephyr-(v\d.\d.\d[rc\-\d]*)$')
- for tag in self.zephyr_kernel_tags:
- m = regex.match(tag)
- if m:
- version = tag
- break
- return version
-
- @property
- def zephyr_sdk_version(self) -> Union[str, None]:
- """Returns the Zephyr SDK version set in the file
- $ZEPHYR_SDK_INSTALL_DIR/sdk_version, or None if unavailable.
- """
- if self._zephyr_sdk_version is None:
- if self._zephyr_sdk_dir:
- path = os.path.join(self._zephyr_sdk_dir, 'sdk_version')
- try:
- with open(path, 'r') as f:
- self._zephyr_sdk_version = f.read().strip()
- except IOError:
- # Silently fail.
- pass
- return self._zephyr_sdk_version
-
- @property
- def gnuarm_version(self) -> Union[str, None]:
- """Returns GCC Arm toolchain version, or None if unavailable.
- """
- if self._gnuarm_dir:
- return GCCArm(self._gnuarm_dir).version
- return None
-
- @property
- def board_dir(self) -> Union[str, None]:
- """Returns the resolved path to the board directory set by
- the CMake cached variable BOARD_DIR, or None if unavailable.
- """
- return self._board_dir
-
- @property
- def board(self) -> Union[str, None]:
- """Returns the best guess fo the board (try BOARD environment variable
- and CMake cache) or None if unavailable.
- """
- # 1st, try environmant variable.
- found_board = os.getenv('BOARD')
- if not found_board:
- # Then try CMake cache.
- found_board = self._cmake_cache.get('BOARD')
- if not found_board:
- # More likely than above.
- found_board = self._cmake_cache.get('CACHED_BOARD')
- if (not found_board) and self.board_dir:
- # Fallback: extract BOARD from BOARD_DIR
- found_board = os.path.basename(self.board_dir)
- return found_board
-
- @property
- def board_dts_file(self) -> Union[str, None]:
- """Returns the best guess for the the DTS file path (relies on
- CMake cache), or None if unavailable.
- """
- if self.board_dir and self.board:
- path = os.path.join(self.board_dir, f'{self.board}.dts')
- if os.path.isfile(path):
- return path
- return None
-
- @property
- def board_binding_file(self) -> Union[str, None]:
- """Returns the best guess for the board binding file path (relies on
- CMake cache), or None if unavailable.
- """
- if self.board_dir and self.board:
- path = os.path.join(self.board_dir, f'{self.board}.yaml')
- if os.path.isfile(path):
- return path
- return None
-
- def _load_environment(self) -> None:
- env = os.getenv('ZEPHYR_BASE')
- if env:
- path = Path(env).resolve()
- self._zephyr_base = str(path)
- env = os.getenv('ZEPHYR_SDK_INSTALL_DIR')
- if env:
- path = Path(env).resolve()
- self._zephyr_sdk_dir = str(path)
- env = os.getenv('GNUARMEMB_TOOLCHAIN_PATH')
- if env:
- path = Path(env).resolve()
- self._gnuarm_dir = str(path)
- self._zephyr_toolchain = os.getenv('ZEPHYR_TOOLCHAIN_VARIANT')
-
- def _load_cmake_cache(self) -> None:
- # self._dts_path is already resolved.
- dts_dir = os.path.dirname(self._dts_path)
- if os.path.isdir(dts_dir):
- build_dir = str(Path(dts_dir).parent.absolute())
- self._cmake_cache = CMakeCache(build_dir)
-
- def _init_zephyr_bindings_search_path(self) -> None:
- if not self._zephyr_base:
- return
- # self._zephyr_base is already resolved.
- path = Path(os.path.join(self._zephyr_base, 'dts', 'bindings'))
- self._binding_dirs.append(str(path))
-
- app_src_dir = self._cmake_cache.get('APPLICATION_SOURCE_DIR')
- if not app_src_dir:
- # APPLICATION_SOURCE_DIR will default to $PWD.
- app_src_dir = os.getcwd()
- path = Path(os.path.join(app_src_dir, 'dts', 'bindings')).resolve()
- if os.path.isdir(path):
- self._binding_dirs.append(str(path))
-
- board_dir = self._cmake_cache.get('BOARD_DIR')
- if board_dir:
- board_path = Path(board_dir).resolve()
- self._board_dir = str(board_path)
- binding_path = Path(os.path.join(board_dir, 'dts', 'bindings')).resolve()
- if os.path.isdir(binding_path):
- self._binding_dirs.append(str(binding_path))
- else:
- # When BOARD_DIR is unset, we add both $ZEPHYR_BASE/boards
- # and $PWD/boards (we don't know if it's a Zephyr board
- # or a custom board).
- #
- # ISSUE: may we have multiple YAML binding files with the same name,
- # but for different boards (in different directories) ?
- path = Path(os.path.join(self._zephyr_base, 'boards')).resolve()
- if os.path.isdir(path):
- self._binding_dirs.append(str(path))
- path = Path(os.path.join(os.getcwd(), 'boards')).resolve()
- if os.path.isdir(path):
- self._binding_dirs.append(str(path))
-
- dts_root = self._cmake_cache.get('DTS_ROOT')
- if dts_root:
- # Append all DTS_ROOT/**/dts/bindings we find.
- for root, _, _ in os.walk(dts_root):
- path = Path(os.path.join(root, 'dts', 'bindings')).resolve()
- if os.path.isdir(path):
- self._binding_dirs.append(str(path))
-
-
-class Dtsh(object):
- """Shell-like interface to a devicetree.
-
- The global metaphor is:
- - a filesystem-like view of the devicetree model
- - a command string interface to POSIX-like shell commands (aka built-ins)
- """
-
- API_VERSION = '0.1.0b2'
- """API version for the dtsh module.
-
- Should match 'version' in setup.py.
- """
-
- # Devicetree model (edtlib).
- _edt: EDT
-
- # Current working node.
- _cwd: Node
-
- # Built-in commands.
- _builtins: Dict[str, DtshCommand]
-
- # Cached bindings map.
- _bindings: Dict[str, Binding]
-
- # Cached available DT binding paths (including YAML files that do
- # not describe a compatible).
- # Memory trade-off: this map may contain about 2000 entries.
- _binding2path: Dict[str, str]
-
- # Sysinfo.
- _uname: DtshUname
-
- def __init__(self, edt: EDT, uname: DtshUname) -> None:
- """Initialize a shell-like interface to a devicetree.
-
- The current working node is initialized to the devicetree's root.
-
- The built-in list is empty.
-
- Arguments:
- edt -- devicetree model (sources and bindings), provided by edtlib
- uname -- system information inferred from environment variables,
- CMake cached variables, Zephyr's Git repository state.
- """
- self._edt = edt
- self._uname = uname
- self._cwd = self._edt.get_node('/')
- self._builtins = {}
- self._bindings = {}
- self._binding2path = {}
- self._init_binding_paths()
- self._init_bindings()
-
- @property
- def uname(self) -> DtshUname:
- """System information inferred from environment variables,
- CMake cached variables and Zephyr's Git repository state.
-
- This is the system information used to initialize the
- devicetree and its bindings.
- """
- return self._uname
-
- @property
- def cwd(self) -> Node:
- """Current working node.
- """
- return self._cwd
-
- @property
- def pwd(self) -> str:
- """Current working node's path.
- """
- return self._cwd.path
-
- @property
- def builtins(self) -> List[DtshCommand]:
- """Available shell built-ins as a list.
- """
- return [cmd for _, cmd in self._builtins.items()]
-
- @property
- def dt_bindings(self) -> Dict[str, Binding]:
- """Map each compatible to its binding.
-
- This collection should include all compatibles that are both:
- - matched (by a node's "compatible" property)
- - described (by a corresponding YAML file)
-
- However, the current implementation of the devicetree model initialization
- may filter out bindings that never appear first (i.e. as most specific)
- in the "compatible" list of a node.
- For example, the binding for "nordic,nrf-swi" is likely to
- always be masked by a more specific compatible, e.g. "nordic,nrf-egu".
- """
- return self._bindings
-
- def dt_binding(self, compat: str) -> Union[Binding, None]:
- """Access bindings by their compatible.
-
- See Dtsh.dt_bindings() for limitations.
-
- Arguments:
- compat -- a compatible (DTSpec 2.3.1)
-
- Returns the binding describing this compatible,
- or None when this compatible is either unmatched or not described.
- """
- return self._bindings.get(compat)
-
- def dt_binding_path(self, fname: str) -> Union[str, None]:
- """Search binding directories for a given DT specification file name.
-
- Contrary to the Dtsh.dt_binding() API, this search is not limited
- to bindings that describe a compatible.
-
- Arguments
- fname -- the YAML file name, e.g. "nordic,nrf-swi.yaml"
-
- Returns the full path of the YAML file, or None when not found.
- """
- return self._binding2path.get(fname)
-
- @property
- def dt_aliases(self) -> Dict[str, Node]:
- aliases: Dict[str, Node] = {}
- for alias, dt_node in self._edt._dt.alias2node.items():
- edt_node = self._edt.get_node(dt_node.path)
- aliases[alias] = edt_node
- return aliases
-
- @property
- def dt_chosen(self) -> Dict[str, Node]:
- return self._edt.chosen_nodes
-
- def builtin(self, name: str) -> Union[DtshCommand, None]:
- """Access a built-in by command name.
-
- Arguments:
- name -- a command name
-
- Returns None if this built-in name is not supported by this command.
- """
- return self._builtins.get(name)
-
- def realpath(self, path: str) -> str:
- """Resolve a node's path.
-
- The devicetree's root path resolves to '/'.
-
- An absolute path resolves to itself.
-
- When the path starts with '.', wildcard substitution occurs:
- - a leading '.' represents the current working node
- - a leading '..' represents the current working node's parent;
- the devicetree's root is its own parent
-
- Otherwise, path is concatenated to the current working node's path.
-
- Path resolution will always:
- - strip any trailing '/' (excepted for the devicetree's root)
- - preserve any trailing wildcard ('*')
-
- See also: man realpath(1), but here none of the path components
- is required to exist (i.e. to actually represent a devicetree node).
-
- Arguments:
- path -- the node's path to resolve
-
- Returns the resolved path.
-
- Raises ValueError when path is unspecified.
- """
- if not path:
- raise ValueError('path must be specified')
-
- if path.startswith('/'):
- # The devicetree's root path resolves to '/'.
- # A path which starts with '/' resolves to itself but any trailing '/.
- if path.endswith('/') and len(path) > 1:
- path = path[:-1]
- else:
- if path.startswith('.'):
- # Wildcard substitution.
- if path.startswith('..'):
- dirpath = Dtsh.dirname(self.pwd)
- path_trailing = path[2:]
- else:
- dirpath = self.pwd
- path_trailing = path[1:]
- # Handle '../dir' and './dir' (typical case).
- if path_trailing.startswith('/'):
- path_trailing = path_trailing[1:]
-
- if path_trailing:
- path = Dtsh.path_concat(dirpath, path_trailing)
- else:
- path = dirpath
- else:
- # Otherwise, path is concatenated to the current
- # working node's path.
- path = Dtsh.path_concat(self.pwd, path)
-
- return path
-
- def path2node(self, path:str) -> Node:
- """Access devicetree nodes by path.
-
- Arguments:
- path -- an absolute devicetree node's path
-
- Returns a devicetree node (EDT).
-
- Raises:
- - ValueError when path is unspecified
- - DtshError when path does not represent an actual devicetree node
- """
- if not path:
- raise ValueError('path must be specified')
-
- try:
- return self._edt.get_node(path)
- except EDTError as e:
- raise DtshError(f'no such node: {path}', e)
-
- def isnode(self, path: str) -> bool:
- """Answsers whether a path represents an actual devicetree node.
-
- Arguments:
- path -- an absolute devicetree node's path
-
- Raises:
- - ValueError when path is unspecified
- """
- try:
- self._edt.get_node(path)
- return True
- except EDTError:
- pass
- return False
-
- def cd(self, path: str) -> None:
- """Change the current working node.
-
- The path is first resolved (see `Dtsh.realpath()`).
-
- Arguments:
- path -- a node's path, either absolute or relative
-
- Raises:
- - ValueError when path is unspecified
- - DtshError when the destination node does not exist
- """
- path = self.realpath(path)
- self._cwd = self.path2node(path)
-
- def ls(self, path: str) -> List[Node]:
- """List a devicetree node's children.
-
- The path is first resolved (see `Dtsh.realpath()`) to:
-
- []
-
- := [/][]'*'
-
- Filtering is thereby applied if the resolved path ends
- with a trailing '*', such that:
-
- ls('/[]*')
-
- will list the children of the node with path '',
- whose name starts with prefix ''.
-
- When 'prefix' is not set,
-
- ls('/*')
-
- is equivalent to:
-
- ls('')
-
- Note that
-
- ls('/parent*')
-
- is interpreted as
-
- ls('/*')
-
- with '' equals to 'parent', even if '/parent' is the path
- to an actual devicetree node.
-
- Arguments:
- path -- a node's path, either absolute or relative
-
- Returns the listed nodes.
-
- Raises:
- - ValueError when path is unspecified
- - DtshError on devicetree node path's resolution failure
- """
- path = self.realpath(path)
-
- if path.endswith('*'):
- dirpath = Dtsh.dirname(path)
- prefix = path[:-1]
- return [n for n in self.ls(dirpath) if n.path.startswith(prefix)]
- else:
- dirnode = self.path2node(path)
- return list(dirnode.children.values())
-
- def exec_command_string(self, cmd_str: str, vt: DtshVt) -> None:
- """Execute a command string.
-
- Note that the command string content after any '--' token
- will be interpreted as command parameters.
-
- See:
- - https://docs.python.org/3.9/library/getopt.html
-
- Arguments:
- cmd_str -- the command string in GNU getopt format
- vt -- where the command will write its output,
- or None for quiet execution
-
- Raises:
- - DtshCommandNotFoundError when the requested command is not supported
- - DtshCommandUsageError when the command string does not match
- the associated GNU getopt usage
- - DtshCommandFailedError when the command execution has failed
- """
- if not cmd_str:
- return
-
- cmdline_vstr = cmd_str.strip().split()
- cmd_name = cmdline_vstr[0]
-
- cmd = self._builtins.get(cmd_name)
- if not cmd:
- raise DtshCommandNotFoundError(cmd_name)
-
- # Set command options and parameters (raises DtshCommandUsageError).
- cmd_argv = cmdline_vstr[1:]
- cmd.parse_argv(cmd_argv)
-
- try:
- cmd.execute(vt)
- except DtshError as e:
- raise DtshCommandFailedError(cmd, e.msg, e)
-
- @staticmethod
- def is_node_enabled(node: Node):
- """Returns True if the node is enabled according to its status.
- """
- return node.status in ['ok', 'okay']
-
- @staticmethod
- def nodename(path: str) -> str:
- """Strip directory and suffix ('/') components from a node's path.
-
- See also: man basename(1)
-
- Arguments:
- path -- a node's path, either absolute or relative
-
- Returns path with any leading directory components removed.
-
- Raises ValueError when path is unspecified.
- """
- if not path:
- raise ValueError('path must be specified')
-
- if path == '/':
- return '/'
-
- x = path.rfind('/')
- if x < 0:
- return path
-
- if path.endswith('/'):
- path = path[:-1]
- x = path.rfind('/')
-
- return path[x+1:]
-
- @staticmethod
- def dirname(path: str) -> str:
- """Strip last component from a node's name.
-
- See also: man dirname(1)
-
- Arguments:
- path -- a node's path, either absolute or relative
-
- Returns path with its last non-slash component and trailing slashes removed,
- or '.' when path does not contain any '/'.
-
- Raises ValueError when path is unspecified.
- """
- if not path:
- raise ValueError('path must be specified')
-
- x = path.rfind('/')
- if x == 0:
- # dirname('/[]') = '/'
- return '/'
- if x > 0:
- # dirname('/[]') = ''
- if path.endswith('/'):
- x = path.rfind('/', 0, len(path) - 1)
- return path[0:x]
-
- # Path does not contain any '/'.
- return '.'
-
- @staticmethod
- def path_concat(path_prefix: str, path: str) -> str:
- """Devicetree node path concatenation.
-
- This helper will:
- - assert path is relative (does not start with '/')
- - append '/' to path_prefix when appropriate
- - drop any trailing '/' from path
-
- Arguments:
- path_prefix -- the leading path prefix
- path -- the relative path to concatenate
-
- Returns the resulting path.
-
- Raises ValueError when path_prefix or path is unspecified,
- or when path starts with '/'.
- """
- if not path_prefix:
- raise ValueError('path prefix must specified')
- if not path:
- raise ValueError('path must specified')
- if path.startswith('/'):
- raise ValueError('path must be relative')
-
- if not path_prefix.endswith('/'):
- path_prefix += '/'
- if path.endswith('/'):
- path = path[:-1]
-
- return path_prefix + path
-
- def _init_bindings(self) -> None:
- # EDT.compat2nodes includes all compatibles matched by a devicetree node.
- # See also EDT._init_luts().
- for compat, nodes in self._edt.compat2nodes.items():
- # A compatible may not map to any binding in the devicetree
- # underlying model:
- # - a compatible that represents a board, for which the binding
- # is looked up with the board identifier, and describes the board
- # itself (e.g. architecture, supported toolchains and Zephyr subsystems)
- # and not a devicetree content; for example, the board identified
- # by "nrf52840dk_nrf52840" is described by its binding file nrf52840dk_nrf52840.yaml,
- # while its DTS file nrf52840dk_nrf52840.dts will set the
- # compatible property of the devicetree root node to "nordic,nrf52840-dk-nrf52840"
- # - a compatible somewhat part of the DT core specifications
- # (e.g. "simple-bus", DTSpec 4.5)
- # - a compatible that does not define any property beside those
- # inherited from the base bindings (e.g. "arm,armv7m-systick")
- # - typically a compatible that isn't described by any YAML file
- #
- # See also edtlib.Binding.compatible:
- # For example, it's None when the Binding is inferred
- # from node properties. It can also be None for Binding objects
- # created using 'child-binding:' with no compatible.
- binding = None
- for node in nodes:
- # There are handfull of issues here:
- # - we access the private member edtlib.Node._binding,
- # and assume Node.matching_compat will equal to
- # Node._binding.compatible wherever a node has a binding
- # - filtering by Node.matching_compat may filter out
- # compatibles that are actually matched by devicetree nodes;
- # e.g. the compatible "nordic,nrf-swi" that's matched by
- # nodes with the more specific compatible "nordic,nrf-egu"
- # will remain undefined despite the proper binding file
- # (nordic,nrf-swi.yaml) being available
- # - not filtering on Node.matching_compat would /define/
- # inconsistent bindings, e.g. the compatible "nordic,nrf-swi"
- # would bind with nordic,nrf-egu.yaml
- #
- # See also edtlib.EDT._init_compat2binding()
- if node._binding and (node.matching_compat == compat):
- binding = node._binding
- break
- if not binding:
- # We may have missed a binding for a compatible that never
- # appears as the most specific (see above): if a corresponding
- # YAML file seems to actually exist, try to instantiate an
- # out-of-devicetree Binding.
- path = self.dt_binding_path(f'{compat}.yaml')
- if path:
- # WARNING: this may fail with an exception,
- # for now let it crash to better know how and when.
- binding = Binding(path, self._binding2path)
- if binding:
- self._bindings[compat] = binding
-
- def _init_binding_paths(self) -> None:
- # Mostly duplicates code from edtlib._binding_paths()
- # and edtlib.EDT._init_compat2binding().
- yaml_paths: List[str] = []
- for bindings_dir in self._edt.bindings_dirs:
- for root, _, filenames in os.walk(bindings_dir):
- for filename in filenames:
- if filename.endswith(".yaml") or filename.endswith(".yml"):
- yaml_paths.append(os.path.join(root, filename))
- for path in yaml_paths:
- self._binding2path[os.path.basename(path)] = path
-
-
-class DtshAutocomp(object):
- """Devicetree shell command line completer.
-
- Usually associated to the shell session's input buffer
- shared with GNU readline.
-
- The auto-completion state machine is made of:
- - the completion state, which is the sequence of possible input strings
- (hints) matching a given prefix
- - a model, which is the list of the actual possible objects matching the
- given prefix
- - a mode, that tags the model semantic (may help client code to avoid
- calling isinstance())
- """
-
- MODE_ANY: ClassVar[int] = 0
- MODE_DTSH_CMD: ClassVar[int] = 1
- MODE_DTSH_OPT: ClassVar[int] = 2
- MODE_DTSH_PAGE: ClassVar[int] = 3
- MODE_DT_NODE: ClassVar[int] = 4
- MODE_DT_PROP: ClassVar[int] = 5
- MODE_DT_BINDING: ClassVar[int] = 6
-
- @property
- @abstractmethod
- def count(self) -> int:
- """Current completions count.
- """
-
- @property
- @abstractmethod
- def hints(self) -> List[str]:
- """Current completion state.
-
- This is the list of completion strings that match the last prefix
- provided to autocomplete().
- """
-
- @property
- @abstractmethod
- def model(self) -> List[Any]:
- """Current completion model.
-
- This is the model objects correponding to the current completion hints.
-
- Permits rich implementation of the rl_completion_display_matches_hook()
- callback.
- """
-
- @property
- @abstractmethod
- def mode(self) -> int:
- """Current completion mode.
-
- Tag describing the current completion model.
- """
-
- @abstractmethod
- def reset(self) -> None:
- """Reset current completion state and model.
- """
-
- @abstractmethod
- def autocomplete(self,
- cmdline: str,
- prefix: str,
- cursor: int = -1) -> List[str]:
- """Auto-complete command line.
-
- Arguments:
- cmdline -- the command line's current content
- prefix -- the prefix word to complete
- cursor -- required to get the full command line's state,
- but unsupported
-
- Returns the completion state (hint strings) matching the prefix.
- """
-
- @staticmethod
- def autocomplete_with_nodes(prefix: str, shell: Dtsh) -> List[Node]:
- """Helper function to auto-complete with a list of nodes.
-
- Arguments:
- prefix -- the node path prefix
- shell -- the shell instance the nodes belong to
-
- Returns a list of matching nodes.
- """
- completions: List[Node] = []
-
- if prefix:
- path_prefix = shell.realpath(prefix)
- if prefix.endswith('/'):
- path = path_prefix
- else:
- path = Dtsh.dirname(path_prefix)
- else:
- path_prefix = shell.pwd
- path = shell.pwd
-
- try:
- roots = [n for n in shell.ls(path) if n.path.startswith(path_prefix)]
- for child in roots:
- if len(child.path) > len(path_prefix):
- completions.append(child)
- except DtshError:
- # No completions for invalid path.
- pass
-
- return completions
-
- @staticmethod
- def autocomplete_with_properties(node_prefix: str,
- prop_prefix: str,
- shell: Dtsh) -> List[Property]:
-
- completions: List[Property] = []
- path_prefix = shell.realpath(node_prefix)
- if shell.isnode(path_prefix):
- node = shell.path2node(path_prefix)
- for _, p in node.props.items():
- if p.name.startswith(prop_prefix) and len(p.name) > len(prop_prefix):
- completions.append(p)
- return completions
-
-
-class DtshError(Exception):
- """Base exception for devicetree shell errors.
- """
-
- _msg: Union[str, None]
- _cause: Union[Exception, None]
-
- def __init__(self,
- msg: Optional[str],
- cause: Optional[Exception] = None) -> None:
- """Create an error.
-
- Arguments:
- msg -- the error message
- cause -- the exception that caused this error, if any
- """
- super().__init__(msg)
- self._msg = msg
- self._cause = cause
-
- @property
- def msg(self) -> str:
- """The error message.
- """
- return self._msg or ''
-
- @property
- def cause(self) -> Union[Exception, None]:
- """The error cause as an exception, or None.
- """
- return self._cause
-
-
-class DtshCommandUsageError(DtshError):
- """A devicetree shell command execution has failed.
- """
-
- def __init__(self,
- command: DtshCommand,
- msg: Optional[str] = None,
- cause: Optional[Exception] = None) -> None:
- """Create a new error.
-
- Arguments:
- command -- the failed command
- msg -- a message describing the usage error
- cause -- the cause exception, if any
- """
- super().__init__(msg, cause)
- self._command = command
-
- @property
- def command(self):
- """The failed command.
- """
- return self._command
-
-
-class DtshCommandFailedError(DtshError):
- """A devicetree shell command execution has failed.
- """
-
- def __init__(self,
- command: DtshCommand,
- msg: str,
- cause: Optional[Exception] = None) -> None:
- """Create a new error.
-
- Arguments:
- command -- the failed command
- msg -- the failure message
- cause -- the failure cause, if any
- """
- super().__init__(msg, cause)
- self._command = command
-
- @property
- def command(self):
- """The failed command.
- """
- return self._command
-
-
-class DtshCommandNotFoundError(DtshError):
- """The requested command is not supported by this devicetree shell.
- """
-
- def __init__(self, name: str) -> None:
- """Create a new error.
-
- Arguments:
- name -- the command name
- """
- super().__init__(f'command not found: {name}')
- self._name = name
-
- @property
- def name(self):
- """The not supported built-in name.
- """
- return self._name
-
-
-class DtshSession(object):
- """Interactive devicetree shell session.
- """
-
- @property
- @abstractmethod
- def shell(self) -> Dtsh:
- """The session's shell.
- """
-
- @property
- @abstractmethod
- def vt(self) -> DtshVt:
- """The session's VT.
- """
-
- @property
- @abstractmethod
- def autocomp(self) -> DtshAutocomp:
- """The session's command line completer.
- """
-
- @property
- @abstractmethod
- def last_err(self) -> Union[DtshError, None]:
- """Last error triggered by a command execution.
- """
-
- @abstractmethod
- def run(self):
- """Enter interactive mode main loop.
- """
-
- @abstractmethod
- def close(self) -> None:
- """Close session, leaving interactive mode.
- """
diff --git a/src/dtsh/io.py b/src/dtsh/io.py
new file mode 100644
index 0000000..85c6c28
--- /dev/null
+++ b/src/dtsh/io.py
@@ -0,0 +1,259 @@
+# Copyright (c) 2023 Christophe Dufaza
+#
+# SPDX-License-Identifier: Apache-2.0
+
+"""I/O streams for devicetree shells.
+
+- "/dev/null" I/O semantic: DTShOutput, DTShInput
+- base terminal API: DTShVT
+- parsed command redirection: DTShRedirection
+- command redirection to file: DTShOutputFile
+
+Unit tests and examples: tests/test_dtsh_io.py
+"""
+
+from typing import Any, IO, Tuple
+
+import os
+import sys
+
+from dtsh.config import DTShConfig
+
+_dtshconf: DTShConfig = DTShConfig.getinstance()
+
+
+class DTShOutput:
+ """Base for shell output streams.
+
+ This base implementation behaves like "/dev/null".
+ """
+
+ def write(self, *args: Any, **kwargs: Any) -> None:
+ """Write to output stream.
+
+ Args:
+ *args: Positional arguments.
+ Semantic depends on the actual concrete stream.
+ **kwargs: Keyword arguments.
+ Semantic depends on the actual concrete stream.
+ """
+
+ def flush(self) -> None:
+ """Flush output stream.
+
+ Semantic depends on the actual concrete stream.
+ """
+
+ def pager_enter(self) -> None:
+ """Page output until a call to pager_exit()."""
+
+ def pager_exit(self) -> None:
+ """Stop paging output."""
+
+
+class DTShInput:
+ """Base for shell input streams.
+
+ This base implementation behaves like "/dev/null".
+ """
+
+ def readline(self, prompt: str = "") -> str:
+ """Print the prompt and read a command line.
+
+ Args:
+ prompt: The command line prompt to use.
+ Defaults to an empty string.
+ """
+ raise EOFError()
+
+
+class DTShVT(DTShInput, DTShOutput):
+ """Base terminal for devicetree shells.
+
+ This base implementation writes to stdout, reads from stdin
+ and ignores paging.
+ """
+
+ def readline(self, prompt: str = "> ") -> str:
+ r"""Print the prompt and read a command line.
+
+ To use ANSI escape codes in the prompt without breaking
+ the GNU readline cursor position, please protect SGR parameters
+ with RL_PROMPT_{START,STOP}_IGNORE markers:
+
+ := m
+ :=
+
+ := \001
+ := \002
+
+ Overrides DTShInput.readline().
+
+ Args:
+ prompt: The command line prompt to use.
+ """
+ return input(prompt)
+
+ def write(self, *args: Any, **kwargs: Any) -> None:
+ """Write to stdout.
+
+ Overrides DTShOutput.write().
+
+ Args:
+ *args: Positional arguments, standard Python print() semantic.
+ **kwargs: Keyword arguments, standard Python print() semantic.
+ """
+ print(*args, **kwargs)
+
+ def flush(self) -> None:
+ """Flush stdout.
+
+ Overrides DTShOutput.flush().
+ """
+ sys.stdout.flush()
+
+ def clear(self) -> None:
+ """Clear VT.
+
+ Ignored by base VT.
+
+ NOTE: duplicate implementation from rich.Console.clear() ?
+ """
+
+
+class DTShOutputFile(DTShOutput):
+ """Output file for command output redirection."""
+
+ _out: IO[str]
+
+ def __init__(self, path: str, append: bool) -> None:
+ """Initialize output file.
+
+ Args:
+ path: The output file path.
+ append: Whether to redirect the command's output in "append" mode.
+
+ Raises:
+ DTShRedirect.Error: Invalid path or permission errors.
+ """
+ try:
+ # We can't use a context manager here, we just want to open
+ # the file for later subsequent writes.
+ self._out = open( # pylint: disable=consider-using-with
+ path,
+ "a" if append else "w",
+ encoding="utf-8",
+ )
+ if append:
+ # Insert blank line between command outputs.
+ self._out.write(os.linesep)
+ except OSError as e:
+ raise DTShRedirect.Error(e.strerror) from e
+
+ def write(self, *args: Any, **kwargs: Any) -> None:
+ """Write to output file.
+
+ Overrides DTShOutput.write().
+
+ Args:
+ *args: Positional arguments, standard Python print(file=) semantic.
+ **kwargs: Keyword arguments, standard Python print(file=) semantic.
+ """
+ print(*args, **kwargs, file=self._out)
+
+ def flush(self) -> None:
+ """Close output file.
+
+ Overrides DTShOutput.flush().
+ """
+ self._out.close()
+
+
+class DTShRedirect(DTShOutput):
+ """Setup command output redirection."""
+
+ class Error(BaseException):
+ """Failed to setup redirection stream."""
+
+ @classmethod
+ def parse_redir2(cls, redir2: str) -> Tuple[str, bool]:
+ """Parse redirection stream into path and mode.
+
+ Does not validate the path.
+
+ Args:
+ redir2: The redirection expression, starting with ">".
+
+ Returns:
+ Path and mode.
+
+ Raises:
+ DTShRedirect.Error: Invalid redirection string.
+ """
+ if not redir2.startswith(">"):
+ raise DTShRedirect.Error(f"invalid redirection: '{redir2}'")
+
+ if redir2.startswith(">>"):
+ append = True
+ path = redir2[2:].strip()
+ else:
+ append = False
+ path = redir2[1:].strip()
+
+ if not path:
+ raise DTShRedirect.Error(
+ "don't know where to redirect the output to ?"
+ )
+
+ return (path, append)
+
+ _path: str
+ _append: bool
+ _out: IO[str]
+
+ def __init__(self, redir2: str) -> None:
+ """New redirection.
+
+ Args:
+ redir2: The redirection expression, starting with ">" or ">>",
+ followed by a file path.
+
+ Raises:
+ DTShRedirect.Error: Forbidden spaces or overwrite.
+ """
+ path, append = self.parse_redir2(redir2)
+
+ if _dtshconf.pref_fs_no_spaces and " " in path:
+ raise DTShRedirect.Error(
+ f"spaces not allowed in redirection: '{path}'"
+ )
+
+ if path.startswith("~"):
+ # abspath() won't expand a leading "~".
+ path = path.replace("~", os.path.expanduser("~"), 1)
+ path = os.path.abspath(path)
+
+ if os.path.isfile(path):
+ if _dtshconf.pref_fs_no_overwrite:
+ raise DTShRedirect.Error(f"file exists: '{path}'")
+ else:
+ # We won't actually append if the file does not exist,
+ # checking this here will help actual DTShOutput implementations
+ # to do the right thing.
+ append = False
+
+ self._append = append
+ self._path = path
+
+ @property
+ def append(self) -> bool:
+ """Whether to redirect output in append-mode.
+
+ Can be true only when the output file actually already exists.
+ """
+ return self._append
+
+ @property
+ def path(self) -> str:
+ """Redirection real path."""
+ return self._path
diff --git a/src/dtsh/man.py b/src/dtsh/man.py
deleted file mode 100644
index ec9d4b0..0000000
--- a/src/dtsh/man.py
+++ /dev/null
@@ -1,683 +0,0 @@
-# Copyright (c) 2022 Chris Duf
-#
-# SPDX-License-Identifier: Apache-2.0
-
-"""Manual pages for devicetree shells."""
-
-import re
-
-from abc import abstractmethod
-from typing import List, Union
-
-from devicetree.edtlib import Binding
-
-from rich.console import RenderableType
-from rich.markdown import Markdown
-from rich.padding import Padding
-from rich.table import Table
-from rich.text import Text
-
-from dtsh.dtsh import Dtsh, DtshCommand, DtshVt
-from dtsh.tui import DtshTui
-
-
-class DtshManPage(object):
- """Abstract manual page.
- """
-
- SECTION_DTSH = 'dtsh'
- SECTION_COMPATS = 'Compatibles'
-
- _section: str
- _page: str
- _view: Table
-
- def __init__(self, section: str, page: str) -> None:
- """Create a manual page.
-
- Arguments:
- section -- the manual section
- page -- the manual page
- """
- self._section = section
- self._page = page
- self._view = DtshTui.mk_grid(1)
- self._view.expand = True
-
- @property
- def section(self) -> str:
- """The manual page's section.
- """
- return self._section
-
- @property
- def page(self) -> str:
- """The manual page.
- """
- return self._page
-
- def show(self, vt: DtshVt, no_pager: bool = False) -> None:
- """Show this man page.
-
- Arguments:
- vt -- the VT to show the man page on
- no_pager -- print the man page without pager
- """
- self._add_header()
- self.add_content()
- self._add_footer()
-
- if not no_pager:
- vt.pager_enter()
- vt.write(self._view)
- if not no_pager:
- vt.pager_exit()
-
- def _add_header(self) -> None:
- """
- """
- bar = DtshTui.mk_grid_statusbar()
- bar.add_row(
- DtshTui.mk_txt_bold(self.section.upper()),
- None,
- DtshTui.mk_txt_bold(self.page.upper())
- )
- self._view.add_row(bar)
- bar.add_row(None)
-
- def _add_footer(self) -> None:
- """
- """
- bar = DtshTui.mk_grid_statusbar()
- bar.add_row(
- DtshTui.mk_txt_bold(Dtsh.API_VERSION),
- DtshTui.mk_txt('Shell-like interface with devicetrees'),
- DtshTui.mk_txt_bold('DTSH')
- )
- self._view.add_row(bar)
-
- def _add_named_content(self, name:str, content: RenderableType) -> None:
- self._view.add_row(DtshTui.mk_txt_bold(name.upper()))
- self._view.add_row(Padding(content, (0,8)))
- self._view.add_row(None)
-
- @abstractmethod
- def add_content(self) -> None:
- """Callback invoked by show() to setup view content.
- """
-
-
-class DtshManPageBuiltin(DtshManPage):
- """
- """
-
- # Documented dtsh command.
- _builtin: DtshCommand
-
- # Regexp for page sections.
- _re: re.Pattern = re.compile('^[A-Z]+$')
-
- def __init__(self, builtin: DtshCommand) -> None:
- super().__init__(DtshManPage.SECTION_DTSH, builtin.name)
- self._builtin = builtin
-
- def add_content(self) -> None:
- self._add_content_name()
- self._add_content_synopsis()
- self._add_markdown()
-
- def _add_content_name(self) -> None:
- txt = DtshTui.mk_txt(self._builtin.name)
- txt.append_text(Text(f' {DtshTui.WCHAR_HYPHEN} ', DtshTui.style_default()))
- txt = DtshTui.mk_txt(self._builtin.desc)
- self._add_named_content('name', txt)
-
- def _add_content_synopsis(self) -> None:
- grid = DtshTui.mk_grid(1)
- grid.add_row(DtshTui.mk_txt(self._builtin.usage))
- grid.add_row(None)
- for opt in self._builtin.options:
- grid.add_row(DtshTui.mk_txt_bold(opt.usage))
- grid.add_row(DtshTui.mk_txt(f' {opt.desc}'))
- self._add_named_content('synopsis', grid)
-
- def _add_markdown(self) -> None:
- content = self._builtin.__doc__
- if content:
- content = content.strip()
- content_vstr = content.splitlines()
- # Skip until 1st section
- for i, line in enumerate(content_vstr):
- if self._is_section_header(line):
- content_vstr = content_vstr[i:]
- break
- # Parse all sections.
- sec_name: Union[str, None] = None
- sec_vstr: Union[List[str], None] = None
- for line in content_vstr:
- line = line.rstrip()
- if self._is_section_header(line):
- # Add current section's content to view if any.
- if sec_name and sec_vstr:
- self._add_section(sec_name, sec_vstr)
- # Init new section's content.
- sec_vstr = []
- sec_name = line
- else:
- # Append line to current section.
- if sec_vstr is not None:
- sec_vstr.append(line)
-
- if sec_name and sec_vstr:
- self._add_section(sec_name, sec_vstr)
-
- def _is_section_header(self, line: str) -> bool:
- return self._re.match(line) is not None
-
- def _add_section(self, name: str, vstr: List[str]) -> None:
- md_src = '\n'.join(vstr)
- md = Markdown(md_src)
- self._add_named_content(name, md)
-
-
-class DtshManPageBinding(DtshManPage):
- """
- """
-
- _binding: Binding
-
- def __init__(self, binding: Binding) -> None:
- super().__init__(DtshManPage.SECTION_COMPATS, binding.compatible)
- self._binding = binding
-
- def add_content(self) -> None:
- self._add_content_compat()
- self._add_content_desc()
- self._add_content_cell_specs()
- self._add_content_bus()
- self._add_content_properties()
- self._add_content_binding()
-
- def _add_content_compat(self) -> None:
- grid = DtshTui.mk_form()
- grid.add_row(DtshTui.mk_txt('Compatible: '),
- DtshTui.mk_txt_binding(self._binding))
- grid.add_row(DtshTui.mk_txt('Summary: '),
- DtshTui.mk_txt_desc_short(self._binding.description))
- self._add_named_content('binding', grid)
-
- def _add_content_desc(self) -> None:
- self._add_named_content('description',
- DtshTui.mk_txt_desc(self._binding.description))
-
- def _add_content_bus(self) -> None:
- if not (self._binding.buses or self._binding.on_bus):
- return
-
- if self._binding.buses:
- str_label = "Nodes with this compatible's binding support buses"
- str_bus = " ".join(self._binding.buses)
- else:
- str_label = "Nodes with this compatible's binding appear on bus"
- str_bus = self._binding.on_bus
-
- txt = DtshTui.mk_txt(f'{str_label}: ')
- txt.append_text(
- DtshTui.mk_txt(str_bus, DtshTui.style(DtshTui.STYLE_DT_BUS))
- )
- self._add_named_content('bus', txt)
-
- def _add_content_cell_specs(self) -> None:
- # Maps specifier space names (e.g. 'gpio') to list of
- # cell names (e.g. ['pin', 'flags']).
- spec_map = self._binding.specifier2cells
- # Number of specifier spaces.
- N = len(spec_map)
- if N == 0:
- return
- grid = DtshTui.mk_grid(1)
- i_spec = 0
- for spec_space, spec_names in spec_map.items():
- grid.add_row(f'{spec_space}-cells:')
- for name in spec_names:
- grid.add_row(f'- {name}')
- if i_spec < (N - 1):
- grid.add_row(None)
- i_spec += 1
- self._add_named_content('cell specifiers', grid)
-
- def _add_content_properties(self) -> None:
- # Maps property names to specifications (PropertySpec).
- spec_map = self._binding.prop2specs
- # Number of property specs.
- N = len(spec_map)
- if N == 0:
- return
- grid = DtshTui.mk_grid(1)
- i_spec = 0
- for _, spec in spec_map.items():
- grid.add_row(DtshTui.mk_form_prop_spec(spec))
- if i_spec < (N - 1):
- grid.add_row(None)
- i_spec += 1
- self._add_named_content('properties', grid)
-
- def _add_content_binding(self) -> None:
- self._add_named_content('binding',
- DtshTui.mk_yaml_binding(self._binding))
-
-
-class DtshManPageDtsh(DtshManPage):
- """
- """
-
- # Regexp for page sections.
- _re: re.Pattern = re.compile('^[A-Z]+$')
-
- def __init__(self) -> None:
- super().__init__(DtshManPage.SECTION_DTSH, 'dtsh')
-
- def add_content(self) -> None:
- self._add_content_as_md()
-
- def _add_content_as_md(self):
- md_src = _DTSH_MAN_PAGE.strip()
- md = Markdown(md_src)
- self._view.add_row(Padding(md, (0,8)))
- self._view.add_row(None)
-
- def _add_content_as_sections(self):
- # Parse all sections.
- sec_name: Union[str, None] = None
- sec_vstr: Union[List[str], None] = None
- content_vstr = _DTSH_MAN_PAGE.strip().splitlines()
- for line in content_vstr:
- line = line.rstrip()
- if self._is_section_header(line):
- # Add current section's content to view if any.
- if sec_name and sec_vstr:
- self._add_section(sec_name, sec_vstr)
- # Init new section's content.
- sec_vstr = []
- sec_name = line
- else:
- # Append line to current section.
- if sec_vstr is not None:
- sec_vstr.append(line)
-
- if sec_name and sec_vstr:
- self._add_section(sec_name, sec_vstr)
-
-
- def _is_section_header(self, line: str) -> bool:
- return self._re.match(line) is not None
-
- def _add_section(self, name: str, vstr: List[str]) -> None:
- md_src = '\n'.join(vstr)
- md = Markdown(md_src)
- self._add_named_content(name, md)
-
-
-_DTSH_MAN_PAGE="""
-# dtsh
-
-[Home](https://github.com/dottspina/dtsh) [PyPI](https://pypi.org/project/devicetree/) [Known issues](https://github.com/dottspina/dtsh/issues)
-
-**dtsh** is an interactive *shell-like* interface with a devicetree and its bindings:
-
-- browse the devicetree through a familiar hierarchical file-system metaphor
-- retrieve nodes and bindings with accustomed command names and command line syntax
-- generate simple documentation artifacts by redirecting commands output to files (text, HTML, SVG)
-- common command line interface paradigms (auto-completion, history) and keybindings
-
-## SYNOPSIS
-
-To start a shell session: `dtsh [] [*]`
-
-where:
-
-- ``: path to the device tree source file in [DTS Format](https://devicetree-specification.readthedocs.io/en/latest/chapter6-source-language.html) (`.dts`);
- if unspecified, defaults to `$PWD/build/zephyr/zephyr.dts`
-- ``: directory to search for [YAML](https://yaml.org/) binding files;
- if unspecified, and the environment variable `ZEPHYR_BASE` is set,
- defaults to [Zephyr’s bindings](https://docs.zephyrproject.org/latest/build/dts/bindings.html#where-bindings-are-located)
-
-ℹ See [Incomplete Zephyr bindings search path #1](https://github.com/dottspina/dtsh/issues/1)
-for details and limitations.
-
-To open an arbitrary DTS file with custom bindings:
-
- $ dtsh /path/to/foobar.dts /path/to/custom/bindings /path/to/other/custom/bindings
-
-To open the same DTS file with Zephyr’s bindings:
-
- $ export ZEPHYR_BASE=/path/to/zephyr
- $ dtsh /path/to/foobar.dts
-
-## THE SHELL
-
-`dtsh` defines a set of *built-in* commands that interface with a devicetree
-and its bindings through a hierarchical file-system metaphor.
-
-### File system metaphor
-
-Within a `dtsh` session, a devicetree shows itself as a familiar hierarchical file-system,
-where [path names](https://devicetree-specification.readthedocs.io/en/stable/devicetree-basics.html#path-names)
-*look like* paths to files or directories, depending on the acting shell command.
-
-A current *working node* is defined, similar to any shell’s current working directory,
-allowing `dtsh` to also support relative paths.
-
-A leading `.` represents the current working node, and `..` its parent.
-The devicetree root node is its own parent.
-
-To designate properties, `dtsh` uses `$` as a separator between DT path names and [property names](https://devicetree-specification.readthedocs.io/en/stable/devicetree-basics.html#property-names)
-(should be safe since `$` is an invalid character for both node and property names).
-
-Some commands support filtering or *globbing* with trailing wild-cards `*`.
-
-### Command strings
-
-The `dtsh` command string is based on the
-[GNU getopt](https://www.gnu.org/software/libc/manual/html_node/Using-Getopt.html) syntax.
-
-#### Synopsis
-
-All built-ins share the same synopsis:
-
- CMD [OPTIONS] [PARAMS]
-
-where:
-
-- `CMD`: the built-in name, e.g. `ls`
-- `OPTIONS`: the options the command is invoked with (see bellow), e.g. `-l`
-- `PARAMS`: the parameters the command is invoked for, e.g. a path name
-
-`OPTIONS` and `PARAMS` are not positional: `ls -l /soc` is equivalent to `ls /soc -l`.
-
-#### Options
-
-An option may support:
-
-- a short name, starting with a single `-` (e.g. `-h`)
-- a long name, starting with `--` (e.g. `--help`)
-
-Short option names can combine: `-lR` is equivalent to `-l -R`.
-
-An Option may also require an argument, e.g. `find /soc --interrupt 12`.
-
-Options semantic should be consistent across commands, e.g. `-l` always means *long format*.
-
-We also try to re-use *well-known* option names, e.g. `-r` for *reverse sort* or `-R` for *recursive*.
-
-
-ℹ Trigger `TAB` completion after a single `-` to *pull* a summary
-of a command's options, e.g:
-
-```
-❯ find -[TAB][TAB]
--c print nodes count
--q quiet, only print nodes count
--l use rich listing format
--f visible columns format string
--h --help print usage summary
---name find by name
---compat find by compatible
---bus find by bus device
---interrupt find by interrupt
---enabled-only search only enabled nodes
---pager page command output
-❯ find -
-```
-
-### Built-ins
-
- | Built-in | |
- |------------+-------------------------------------------|
- | alias | print defined aliases |
- | chosen | print chosen configuration |
- | pwd | print current working node's path |
- | cd | change current working node |
- | ls | list devicetree nodes |
- | tree | list devicetree nodes in tree-like format |
- | cat | concatenate and print devicetree content |
- | find | find devicetree nodes |
- | uname | print system information |
- | man | open a manual page |
-
-Use `man ` to print a command's manual page,
-e.g. `man ls`.
-
-### Manual pages
-
-As expected, the `man` command will open the manual page for the shell itself (`man dtsh`),
-or one of its built-ins (e.g. `man ls`).
-
-Additionally, `man` can also open a manual page for a
-[compatible](https://devicetree-specification.readthedocs.io/en/latest/chapter2-devicetree-basics.html#compatible),
-which is essentially a view of its (YAML) bindings: e.g. `man --compat nordic,nrf-radio`
-
-`man` should eventually also serve as an entry point to external useful or normative documents,
-e.g. the Devicetree Specifications or the Zephyr project’s documentation.
-
-### System information
-
-**dtsh** may also expose *system* information, including:
-
-- the Zephyr kernel version, e.g. `zephyr-3.1.0`, with a link to the corresponding
- release notes when available
-- board information, based on the content of its YAML binding file,
- with a link to the corresponding documentation when the board
- is [supported by Zephyr](https://docs.zephyrproject.org/latest/boards/index.html)
-- the configured *toolchain*, either Zephyr SDK or GNU Arm Embedded
-
-For example:
-
- BOARD
- Board directory: $ZEPHYR_BASE/boards/arm/nrf52840dk_nrf52840
- Name: nRF52840-DK-NRF52840 (Supported Boards)
- Board: nrf52840dk_nrf52840 (DTS)
-
- nrf52840dk_nrf52840.yaml
-
- identifier: nrf52840dk_nrf52840
- name: nRF52840-DK-NRF52840
- type: mcu
- arch: arm
- ram: 256
- flash: 1024
- toolchain:
- - zephyr
- - gnuarmemb
- - xtools
- supported:
- - adc
- - arduino_gpio
- - arduino_i2c
- - arduino_spi
- - ble
- - counter
- - gpio
- - i2c
- - i2s
- - ieee802154
- - pwm
- - spi
- - usb_cdc
- - usb_device
- - watchdog
- - netif:openthread
-
-Retrieving this information may involve environment variables (e.g. `ZEPHYR_BASE`
-or `ZEPHYR_TOOLCHAIN_VARIANT`), CMake cached variables, `git` or GCC.
-
-Refer to `man uname` for details.
-
-### Find nodes
-
-The `find` command permits to search the devicetree by:
-
-- node names
-- compatible strings
-- bus devices
-- interrupt names or numbers
-
-For example, the command line bellow would list all enabled bus devices
-that generate IRQs :
-
-
- ❯ find --enabled-only --bus * --interrupt *
-
-`find` is quite versatile and supports a handful of options.
-Refer to its extensive manual page (`man find`).
-
-## USER INTERFACE
-
-The `dtsh` command line interface paradigms and keybindings should sound familiar.
-
-### The prompt
-
-The default shell prompt is ❯.
-The line immediately above the prompt shows the current working node’s path.
-
- /
- ❯ pwd
- /
-
- /
- ❯ cd /soc/i2c@40003000/bme680@76
-
- /soc/i2c@40003000/bme680@76
- ❯ pwd
- /soc/i2c@40003000/bme680@76
-
-Pressing `C-d` (aka `CTRL-D`) at the prompt will exit the `dtsh` session.
-
-### Commands history
-
-Commands history is provided through GNU readline integration.
-
-At the shell prompt, press:
-
-- up arrow (↑) to navigate the commands history backward
-- down arrow (↓) to navigate the commands history forward
-- `C-r` (aka `CTRL-R`) to search the commands history
-
-The history file (typically `$HOME/.config/dtsh/history`) is saved on exit, and loaded on startup.
-
-### Auto-completion
-
-Command line auto-completion is provided through GNU readline integration.
-
-Auto-completion is triggered by first pressing the `TAB` key twice,
-then once for subsequent completions of the same command line, and may apply to:
-
-- command names (aka built-ins)
-- command options
-- command parameters
-
-### The pager
-
-Built-ins that may produce large outputs support the `--pager` option: the command’s
-output is then *paged* using the system pager, typically `less`:
-
-- use up (↑) and down (↓) arrows to navigate line by line
-- use page up (⇑) and down (⇓) to navigate *window* by *window*
-- press `g` go to first line
-- press `G` go to last line
-- press `/` to enter search mode
-- press `h` for help
-- press `q` to quit the pager and return to the `dtsh` prompt
-
-On the contrary, the `man` command uses the pager by default
-and defines a `--no-pager` option to disable it.
-
-### External links
-
-`dtsh` commands output may contain links to external documents such as:
-
-- the local YAML binding files, that should open in the system’s
- default text editor
-- the Devicetree specifications or the Zephyr project’s documentation,
- that should open in the system’s default web browser
-
-How these links will appear in the console, and whether they are *actionable* or not,
-eventually depend on the terminal and the desktop environment.
-
-This is an example of such links: [Device Tree What It Is](https://elinux.org/Device_Tree_What_It_Is)
-
-ℹ In particular, the environment may assume DTS files are DTS audio streams
-(e.g. the VLC media player could have registered itself for handling the `.dts` file extension).
-In this case, the external link won't open, possibly without any error message.
-A work-around is to configure the desktop environment to open DTS files with
-a text editor (e.g. with the *Open with* paradigm).
-
-### Output redirection
-
-Command output redirection uses the well-known syntax:
-
- CMD [OPTIONS] [PARAMS] > PATH
-
-where `PATH` is the absolute or relative path to the file the command output will be redirected to.
-
-Depending on the extension, the command output may be saved as an HTML page (`.html`), an SVG image (`.svg`),
-or a text file (default).
-
-For example:
-
- /
- ❯ ls -l soc > soc.html
-
-### Keybindings
-
-Familiar keybindings are set through GNU readline integration.
-
-- `C-l` clear terminal screen
-- `C-a` move cursor to beginning of command line
-- `C-e` move cursor to end of command line
-- `C-k` *kill* text from cursor to end of command line
-- `M-d` *kill* word at cursor
-- `C-y` *yank* (paste) the content of the *kill buffer*
-- `C-←` move cursor one word backward
-- `C-→` move cursor one word forward
-- `↑` navigate the commands history backward
-- `↓` navigate the commands history forward
-- `C-r` search the commands history
-- `TAB` trigger auto-completion
-
-### Theme
-
-Colors and such are subjective, and most importantly the rendering will
-eventually depend on the terminal’s font and palette,
-possibly resulting in severe accessibility issues, e.g. grey text on white background
-or a weird shell prompt.
-
-In such situations, or to accommodate personal preferences, users can try to override
-`dtsh` colors (and prompt) by creating a *theme* file (typically `$HOME/.config/dtsh/theme`).
-
-Use the [default theme](https://github.com/dottspina/dtsh/blob/main/src/dtsh/theme) as template:
-
- cp src/dtsh/theme ~/.config/dtsh/theme
-
-## References
-
-**Devicetree Specifications**
-
-- [Online Devicetree Specifications](https://devicetree-specification.readthedocs.io/en/latest/) (latest)
-- [Online Devicetree Specifications](https://devicetree-specification.readthedocs.io/en/stable/) (stable)
-
-**Zephyr**
-
-- [Introduction to devicetree](https://docs.zephyrproject.org/latest/build/dts/intro.html)
-- [Devicetree bindings](https://docs.zephyrproject.org/latest/build/dts/bindings.html)
-- [Bindings index](https://docs.zephyrproject.org/latest/build/dts/api/bindings.html)
-- [Zephyr-specific chosen nodes](https://docs.zephyrproject.org/latest/build/dts/api/api.html#zephyr-specific-chosen-nodes)
-- [Devicetree versus Kconfig](https://docs.zephyrproject.org/latest/build/dts/dt-vs-kconfig.html)
-
-**Linux**
-
-- [Open Firmware and Devicetree](https://docs.kernel.org/devicetree/index.html)
-- [Device Tree Usage](https://elinux.org/Device_Tree_Usage)
-- [Device Tree Reference](https://elinux.org/Device_Tree_Reference)
-- [Device Tree What It Is](https://elinux.org/Device_Tree_What_It_Is)
-"""
diff --git a/src/dtsh/model.py b/src/dtsh/model.py
new file mode 100644
index 0000000..85fe1d2
--- /dev/null
+++ b/src/dtsh/model.py
@@ -0,0 +1,1889 @@
+# Copyright (c) 2023 Christophe Dufaza
+#
+# SPDX-License-Identifier: Apache-2.0
+
+"""Devicetree model.
+
+Rationale:
+
+- factorize the dtsh interface with edtlib (aka python-devicetree)
+- support the hierarchical file system metaphor at the model layer
+- unified API for sorting and matching nodes
+- provide model objects (nodes, bindings, etc) with identity, equality
+ and a default order-by semantic
+- write most dtsh unit tests at the model layer
+
+Implementation notes:
+
+- identity: permits to build sets (uniqueness) of model objects
+ and filter out duplicates, should imply equality
+- equality: testing equality for model objects of different types
+ is allowed and answers false, because an orange is not an apple
+- default order: "less than" typically means "appears first";
+ comparing model objects of different types is an API violation because
+ we can't sort oranges and apples without an additional heuristic,
+ such as preferring oranges
+
+Unit tests and examples: tests/test_dtsh_model.py
+"""
+
+
+from typing import (
+ Any,
+ Optional,
+ Iterator,
+ List,
+ Set,
+ Mapping,
+ Dict,
+ Tuple,
+ Sequence,
+)
+
+import os
+import posixpath
+import sys
+
+from devicetree import edtlib
+
+from dtsh.dts import DTS, YAMLFile
+
+
+class DTPath:
+ """Devicetree paths for the hierarchical file-system metaphor.
+
+ This API does not involve actual Devicetree nodes, only path strings.
+
+ An absolute path is a path name,
+ as defined in DTSpec 2.2.1 and 2.2.3..
+
+ A path that does not start from the devicetree root ("/")
+ is a relative path.
+
+ API functions may:
+
+ - work equally with absolute and relative path parameters
+ - expect an additional "current working branch" parameter
+ to interpret a relative path parameter
+ - require an absolute path parameter, and fault if invoked
+ with a relative path
+
+ POSIX-like path references are supported:
+
+ - "." represents the current working branch
+ - ".." represents the parent of the current working branch;
+
+ By convention, the Devicetree root is its own parent.
+ """
+
+ @staticmethod
+ def split(path: str) -> List[str]:
+ """Convert a DT path into a list of path segments.
+
+ Each path segment is a node name:
+
+ split('a') == ['a']
+ split('a/b/c') == ['a', 'b', 'c']
+
+ By convention, the DT root "/" always represent the first
+ node name of a path name:
+
+ split('/') == ['/']
+ split('/x') == ['/', 'x']
+ split('/x/y/z') == ['/', 'x', 'y', 'z']
+
+ Any Trailing empty node name component is stripped:
+
+ split('/a/') == ['/', "a"]
+ split('') == []
+
+ Args:
+ path: A DT path.
+
+ Returns:
+ The list of the node names in path.
+ The number of names in this list minus one
+ represents the (relative) depth of the node at path.
+ """
+ if not path:
+ return []
+ splits = path.split("/")
+ if not splits[0]:
+ # path := /[]
+ # Substitute 1st empty split with "/".
+ splits[0] = "/"
+ if not splits[-1]:
+ # path := /
+ # Remove trailing empty split.
+ splits = splits[:-1]
+ return splits
+
+ @staticmethod
+ def join(path: str, *paths: str) -> str:
+ """Concatenate DT paths.
+
+ Join the paths *intelligently*, as defined by posixpath.join().
+
+ For example:
+
+ join('/', 'a', 'b') == "/a/b"
+ join('/a', 'b') == '/a/b'
+ join('a/', 'b') == 'a/b'
+
+ Paths are not normalized:
+
+ join('a', '.') == 'a/.'
+ join('a', 'b/') == 'a/b/'
+ join('a', '') == 'a/'
+
+ Joining an absolute path will reset the join chain:
+
+ join('/a', 'b', '/x', 'y') == '/x/y'
+
+ Args:
+ path: A DT path.
+ *paths: The path segments to concatenate to path.
+
+ Returns:
+ The joined path segments.
+ """
+ return posixpath.join(path, *paths)
+
+ @staticmethod
+ def normpath(path: str) -> str:
+ """Normalize a DT path.
+
+ Normalize a path by collapsing redundant or trailing separators
+ and common path references ("." and ".."),
+ as defined in posixpath.normpath().
+
+ For example:
+
+ normpath('/.') == '/'
+ normpath('a/b/') == 'a/b'
+ normpath('a//b') == 'a/b'
+ normpath('a/foo/./../b') == 'a/b'
+
+ The devicetree root is its own parent:
+
+ normpath('/../a') == '/a'
+
+ The normalized form of an empty path is a reference
+ to the current working branch.
+
+ normpath('') == '.'
+
+ Note: a relative path that starts with a reference to a parent,
+ e.g. '../a', cannot be normalized alone. See also DTPath.abspath().
+
+ Args:
+ path: A DT path.
+
+ Returns:
+ The normalized path.
+ """
+ return posixpath.normpath(path)
+
+ @staticmethod
+ def abspath(path: str, pwd: str = "/") -> str:
+ """Absolutize and normalize a DT path.
+
+ This is equivalent to normpath(join(pwd, path)).
+
+ For example:
+
+ abspath('') == '/'
+ abspath('/') == '/'
+ abspath('a/b') == '/a/b'
+ abspath('/a/b', '/x') == '/a/b'
+ abspath('a', '/foo') == '/foo/a'
+
+ Args:
+ path: A DT path.
+ pwd: The DT path name of the current working branch.
+
+ Returns:
+ The normalized absolute writing of path.
+ """
+ DTPath.check_path_name(pwd)
+ return DTPath.normpath(DTPath.join(pwd, path))
+
+ @staticmethod
+ def relpath(pathname: str, pwd: str = "/") -> str:
+ """Get the relative DT path from one node to another.
+
+ For example:
+
+ relpath('/') == '.'
+ relpath('/a') == 'a'
+ relpath('/a', '/a') == '.'
+ relpath('/a/b/c', '/a') == 'b/c'
+
+ If going backward is necessary, ".." references are used:
+
+ relpath('/foo/a/b/c', '/bar') == '../foo/a/b/c'
+
+ Args:
+ pathname: The DT path name of the final node.
+ pwd: The DT path name of the initial node.
+
+ Returns:
+ The relative DT path from pwd to path.
+ """
+ DTPath.check_path_name(pathname)
+ DTPath.check_path_name(pwd)
+ return posixpath.relpath(pathname, pwd)
+
+ @staticmethod
+ def dirname(path: str) -> str:
+ """Get the head of the node names in a DT path.
+
+ Most often, dirname() semantic will match the parent node's path:
+
+ dirname(node.path) == node.parent.path
+
+ For example:
+
+ dirname('/a') == '/'
+ dirname('/a/b/c') == '/a/b'
+ dirname('a/b') == 'a'
+
+ The root node is its own parent:
+
+ dirname('/') == '/'
+
+ When the path does not contain any '/', dirname() always
+ returns a reference to the *current working branch*:
+
+ dirname('') == '.'
+ dirname('a') == '.'
+ dirname('.') == '.'
+ dirname('..') == '.'
+
+ A trailing '/' is interpreted as an empty trailing node name:
+
+ dirname('/a/') == '/a'
+
+ Args:
+ path: A DT path.
+
+ Returns:
+ The absolute or relative head of the split path,
+ or "." when path does not contain any "/".
+ """
+ # Note: the sematic here is NOT DtPath.split().
+ head, _ = posixpath.split(path)
+ return head or "."
+
+ @staticmethod
+ def basename(path: str) -> str:
+ """Get the tail of the node names in a DT path.
+
+ Most often, basename() semantic will match the node name:
+
+ basename(node.path) == node.name
+
+ For example:
+
+ basename('/a') == 'a'
+ basename('a') == 'a'
+ basename('a/b') == 'b'
+
+ A trailing '/' is interpreted as an empty trailing node name:
+
+ basename('/') == ''
+ basename('a/') == ''
+
+ And by convention:
+
+ basename('') == ''
+ basename('.') == '.'
+ basename('..') == '..'
+
+ Args:
+ path: A DT path.
+
+ Returns:
+ The tail of the split path,
+ or an empty string when path ends with "/".
+ """
+ # Note: the sematic here is NOT DtPath.split().
+ _, tail = posixpath.split(path)
+ return tail
+
+ @staticmethod
+ def check_path_name(path: str) -> None:
+ """Check path names.
+
+ Will fault if:
+ - path is not absolute (does not start with "/")
+ - path contains path references ("." or "..")
+ """
+ if not path.startswith("/"):
+ # Path names are absolute DT paths.
+ raise ValueError(path)
+ if "." in path:
+ # Path names do not contain path references ("." or "..").
+ raise ValueError(path)
+
+
+class DTVendor:
+ """Device or device class vendor.
+
+ Identity, equality and default order relationship are based on
+ the vendor prefix.
+
+ See:
+ - DTSpec 2.3.1. compatible
+ - zephyr/dts/bindings/vendor-prefixes.txt
+ """
+
+ _prefix: str
+ _name: str
+
+ def __init__(self, prefix: str, name: str) -> None:
+ """Initialize vendor.
+
+ Args
+ prefix: Vendor prefix, e.g. "nordic".
+ name: Vendor name, e.g. "Nordic Semiconductor"
+ """
+ self._prefix = prefix
+ self._name = name
+
+ @property
+ def prefix(self) -> str:
+ """The vendor prefix.
+
+ This prefix appears as the manufacturer component
+ in compatible strings.
+ """
+ return self._prefix
+
+ @property
+ def name(self) -> str:
+ """The vendor name."""
+ return self._name
+
+ def __eq__(self, other: object) -> bool:
+ if isinstance(other, DTVendor):
+ return self.prefix == other.prefix
+ return False
+
+ def __lt__(self, other: object) -> bool:
+ if isinstance(other, DTVendor):
+ return self.prefix < other.prefix
+ raise TypeError(other)
+
+ def __hash__(self) -> int:
+ return hash(self.prefix)
+
+ def __repr__(self) -> str:
+ return self._prefix
+
+
+class DTBinding:
+ """Devicetree binding (DTSpec 4. Device Bindings).
+
+ Bindings include:
+
+ - bindings that specify a device or device class identified by
+ a compatible string
+ - child-bindings of those, which may or not be identified by
+ their own compatible string
+ - the base bindings recursively included by the above
+
+ Bindings identity, equality and default order are based on:
+
+ - the base name of the YAML file: bindings that originate from
+ different YAML files are different and ordered by base name
+ - a child-binding depth that permits to distinguish and compare
+ bindings that originate from the same YAML file
+ """
+
+ # Peer edtlib binding (permits to avoid "if self.binding" statements
+ # when accessing most properties).
+ _edtbinding: edtlib.Binding
+
+ # Child-binding depth.
+ _cb_depth: int
+
+ # YAML binding file.
+ _yaml: YAMLFile
+
+ # Nested child-binding this binding defines, if any.
+ _child_binding: Optional["DTBinding"]
+
+ # The parent model (used to resolve child binding).
+ def __init__(
+ self,
+ edtbinding: edtlib.Binding,
+ cb_depth: int,
+ child_binding: Optional["DTBinding"],
+ ) -> None:
+ """Initialize binding.
+
+ Args:
+ edtbinding: Peer edtlib binding object.
+ cb_depth: The child-binding depth.
+ child_binding: Nested child-binding this binding defines, if any.
+ """
+ if not edtbinding.path:
+ # DTModel hypothesis.
+ raise ValueError(edtbinding)
+
+ self._edtbinding = edtbinding
+ self._cb_depth = cb_depth
+ self._child_binding = child_binding
+ # Lazy-initialized: won't read/parse YAML content until needed.
+ self._yaml = YAMLFile(edtbinding.path)
+
+ @property
+ def path(self) -> str:
+ """Absolute path to the YAML file defining the binding."""
+ return self._yaml.path
+
+ @property
+ def compatible(self) -> Optional[str]:
+ """Compatible string for the devices specified by this binding.
+
+ None for child-bindings without compatible string,
+ and for bindings that do not specify a device or device class.
+ """
+ return self._edtbinding.compatible
+
+ @property
+ def buses(self) -> Sequence[str]:
+ """Bus protocols that the nodes specified by this binding should support.
+
+ Empty list if this binding does not specify a bus node.
+ """
+ return self._edtbinding.buses
+
+ @property
+ def on_bus(self) -> Optional[str]:
+ """The bus that the nodes specified by this binding should appear on.
+
+ None if this binding does not expect a bus of appearance.
+ """
+ return self._edtbinding.on_bus
+
+ @property
+ def description(self) -> Optional[str]:
+ """The description of this binding, if any."""
+ return self._edtbinding.description
+
+ @property
+ def includes(self) -> Sequence[str]:
+ """The bindings included by this binding file."""
+ return self._yaml.includes
+
+ @property
+ def cb_depth(self) -> int:
+ """Child-binding depth.
+
+ Zero if this is not a child-binding.
+ """
+ return self._cb_depth
+
+ @property
+ def child_binding(self) -> Optional["DTBinding"]:
+ """The nested child-binding this binding defines, if any."""
+ return self._child_binding
+
+ def get_headline(self) -> Optional[str]:
+ """The headline of this binding description, if any."""
+ desc = self._edtbinding.description
+ if desc:
+ return desc.lstrip().split("\n", 1)[0]
+ return None
+
+ def __eq__(self, other: object) -> bool:
+ if isinstance(other, DTBinding):
+ this_fname = os.path.basename(self.path)
+ other_fname = os.path.basename(other.path)
+ return (this_fname == other_fname) and (
+ self.cb_depth == other.cb_depth
+ )
+ return False
+
+ def __lt__(self, other: object) -> bool:
+ if isinstance(other, DTBinding):
+ this_fname = os.path.basename(self.path)
+ other_fname = os.path.basename(other.path)
+ if this_fname == other_fname:
+ # Bindings that originate from the same YAML file
+ # are ordered from parent to child-bindings.
+ return self.cb_depth < other.cb_depth
+ # Bindings that originate from different YAML files
+ # are ordered by file name.
+ return this_fname < other_fname
+ raise TypeError(other)
+
+ def __hash__(self) -> int:
+ return hash((os.path.basename(self.path), self.cb_depth))
+
+ def __repr__(self) -> str:
+ return f"yaml:{os.path.basename(self.path)}, cb_depth:{self.cb_depth}"
+
+
+class DTNodeInterrupt:
+ """Interrupts a node may generate.
+
+ See DTSpec 2.4.2. Properties for Interrupt Controllers.
+ """
+
+ _edtirq: edtlib.ControllerAndData
+ _node: "DTNode"
+
+ @classmethod
+ def sort_by_number(
+ cls, irqs: Sequence["DTNodeInterrupt"], reverse: bool
+ ) -> List["DTNodeInterrupt"]:
+ """Sort interrupts by IRQ number."""
+ return sorted(irqs, key=lambda irq: irq.number, reverse=reverse)
+
+ @classmethod
+ def sort_by_priority(
+ cls, irqs: Sequence["DTNodeInterrupt"], reverse: bool
+ ) -> List["DTNodeInterrupt"]:
+ """Sort interrupts by IRQ priority."""
+ return sorted(
+ irqs,
+ key=lambda irq: irq.priority
+ if irq.priority is not None
+ else sys.maxsize,
+ reverse=reverse,
+ )
+
+ def __init__(
+ self, edtirq: edtlib.ControllerAndData, node: "DTNode"
+ ) -> None:
+ """Initialize interrupt.
+
+ Args:
+ edtirq: Peer edtlib IRQ object.
+ node: The device node that may generate this IRQ.
+ """
+ self._edtirq = edtirq
+ self._node = node
+
+ @property
+ def number(self) -> int:
+ """The IRQ number."""
+ return self._edtirq.data.get("irq", sys.maxsize)
+
+ @property
+ def priority(self) -> Optional[int]:
+ """The IRQ priority.
+
+ Although interrupts have a priority on most platforms,
+ it's not actually true for all boards, e.g. the EPS32 SoC.
+ """
+ # NOTE[PR-edtlib]: ControllerAndData docstring may be misleading
+ # about the "priority" and/or "level" data.
+ return self._edtirq.data.get("priority")
+
+ @property
+ def name(self) -> Optional[str]:
+ """The IRQ name."""
+ return self._edtirq.name
+
+ @property
+ def emitter(self) -> "DTNode":
+ """The device node that may generate this IRQ."""
+ return self._node
+
+ @property
+ def controller(self) -> "DTNode":
+ """The controller this interrupt gets sent to."""
+ return self._node.dt[self._edtirq.controller.path]
+
+ def __eq__(self, other: object) -> bool:
+ """Interrupts equal when IRQ numbers, priorities equal."""
+ if isinstance(other, DTNodeInterrupt):
+ return (self.number == other.number) and (
+ self.priority == other.priority
+ )
+ return False
+
+ def __lt__(self, other: object) -> bool:
+ """By default, interrupts are sorted by IRQ numbers, then priorities."""
+ if isinstance(other, DTNodeInterrupt):
+ if self.number == other.number:
+ if (self.priority is not None) and (other.priority is not None):
+ return self.priority < other.priority
+ return self.number < other.number
+ raise TypeError(other)
+
+ def __hash__(self) -> int:
+ """Identity inlcludes IRQ number and priority, and emitter."""
+ return hash((self.number, self.priority, self.emitter))
+
+ def __repr__(self) -> str:
+ return (
+ f"IRQ_{self.number}, prio:{self.priority}, src:{self.emitter.path}"
+ )
+
+
+class DTNodeRegister:
+ """Address of a node resource.
+
+ A register describes the address of a resource
+ within the address space defined by its parent bus.
+
+ According to DTSpec 2.3.6 reg:
+
+ - the reg property is composed of an arbitrary number of pairs
+ of address and length
+ - if the parent node specifies a value of 0 for #size-cells,
+ the length field in the value of reg shall be omitted
+
+ This API will then assume:
+
+ - all node registers should have an address (will fallback to MAXINT
+ rather than fault when unset)
+ - an unspecified size represents a zero-size register (an address)
+
+ A default order is defined, based on register addresses,
+ which is meaningful only when sorting registers within a same parent bus.
+ """
+
+ _edtreg: edtlib.Register
+ _addr: int
+
+ @classmethod
+ def sort_by_addr(
+ cls, regs: Sequence["DTNodeRegister"], reverse: bool
+ ) -> List["DTNodeRegister"]:
+ """Sort registers by address."""
+ return sorted(regs, key=lambda reg: reg.address, reverse=reverse)
+
+ @classmethod
+ def sort_by_size(
+ cls, regs: Sequence["DTNodeRegister"], reverse: bool
+ ) -> List["DTNodeRegister"]:
+ """Sort registers by size."""
+ return sorted(regs, key=lambda reg: reg.size, reverse=reverse)
+
+ def __init__(self, edtreg: edtlib.Register) -> None:
+ self._edtreg = edtreg
+ if edtreg.addr is not None:
+ self._addr = edtreg.addr
+ else:
+ # We assume node registers have an address.
+ # Fallback to funny value (not 0).
+ self._addr = sys.maxsize
+
+ @property
+ def address(self) -> int:
+ """The address within the address space defined by the parent bus."""
+ return self._addr
+
+ @property
+ def size(self) -> int:
+ """The register size.
+
+ Mostly meaningful for memory mapped IO.
+ """
+ return self._edtreg.size or 0
+
+ @property
+ def tail(self) -> int:
+ """The last address accessible through this register.
+
+ Mostly meaningful for memory mapped IO.
+ """
+ if self.size > 0:
+ return self.address + self.size - 1
+ return self.address
+
+ @property
+ def name(self) -> Optional[str]:
+ """The register name, if any."""
+ return self._edtreg.name
+
+ def __lt__(self, other: object) -> bool:
+ if isinstance(other, DTNodeRegister):
+ return self.address < other.address
+ raise TypeError(other)
+
+ def __repr__(self) -> str:
+ return f"addr:{hex(self.address)}, size:{hex(self.size)}"
+
+
+class DTWalkable:
+ """Virtual devicetree we can walk through.
+
+ The simplest walk-able is a Devicetree branch,
+ implemented by DTNode objects.
+
+ DTWalkableComb permits to define a virtual devicetree as a root node
+ and a set of selected leaves.
+ """
+
+ def walk(
+ self,
+ /,
+ order_by: Optional["DTNodeSorter"] = None,
+ reverse: bool = False,
+ enabled_only: bool = False,
+ fixed_depth: int = 0,
+ ) -> Iterator["DTNode"]:
+ """Walk through the virtual devicetree.
+
+ Args:
+ order_by: Children ordering while walking branches.
+ None will preserve the DTS order.
+ reverse: Whether to reverse children order.
+ If set and no order_by is given, means reversed DTS order.
+ enabled_only: Whether to stop at disabled branches.
+ fixed_depth: The depth limit, defaults to 0,
+ walking through to leaf nodes, according to enabled_only.
+
+ Returns:
+ An iterator yielding the nodes in order of traversal.
+ """
+ del order_by
+ del reverse
+ del enabled_only
+ del fixed_depth
+ yield from ()
+
+
+class DTNode(DTWalkable):
+ """Devicetree nodes.
+
+ The nodes API has a double aspect:
+
+ - node-oriented: properties, identity, equality,
+ and default order-by relationship
+ - branch-oriented: walk-able, search-able
+ """
+
+ # Peer edtlib node.
+ _edtnode: edtlib.Node
+
+ # The devicetree model this node belongs to.
+ _dt: "DTModel"
+
+ # Parent node.
+ _parent: "DTNode"
+
+ # Child nodes (DTS-order).
+ _children: List["DTNode"]
+
+ # The binding that specifies the node content, if any.
+ _binding: Optional[DTBinding]
+
+ def __init__(
+ self,
+ edtnode: edtlib.Node,
+ model: "DTModel",
+ parent: Optional["DTNode"],
+ ) -> None:
+ """Insert a node in a devicetree model.
+
+ Nodes are first inserted childless at the location pointed to by
+ the parent parameter, then children are appended while the model
+ initialization process walks the devicetree in DTS order.
+
+ Args:
+ edtnode: The peer edtlib.Node node.
+ model: The devicetree model this node is inserted into.
+ parent: The parent node (point of insertion),
+ or None when creating the model's root.
+ """
+ self._edtnode = edtnode
+ self._dt = model
+ # The devicetree root is its own parent.
+ self._parent = parent or self
+ # Nodes are first inserted childless.
+ self._children = []
+ # Device bindings are initialized on start-up.
+ self._binding = self._dt.get_device_binding(self)
+
+ @property
+ def dt(self) -> "DTModel":
+ """The devicetree model this node belongs to."""
+ return self._dt
+
+ @property
+ def path(self) -> str:
+ """The path name (DTSpec 2.2.3)."""
+ return self._edtnode.path
+
+ @property
+ def name(self) -> str:
+ """The node name (DTSpec 2.2.1)."""
+ return self._edtnode.name
+
+ @property
+ def unit_name(self) -> str:
+ """The unit-name component of the node name.
+
+ The term unit-name is not widely used for the node-name component
+ of node names: for an example, see in DTSpec 3.4. /memory node.
+ """
+ return self._edtnode.name.split("@")[0]
+
+ @property
+ def unit_addr(self) -> Optional[int]:
+ """The (value of the) unit-address component of the node name.
+
+ May be None if this node has no unit address.
+ """
+ # NOTE: edtlib.Node.unit_addr may answer a string in the (near) future.
+ return self._edtnode.unit_addr
+
+ @property
+ def status(self) -> str:
+ """The node's status string (DTSpec 2.3.4).
+
+ While the devicetree specifications allows this property
+ to have values "okay", "disabled", "reserved", "fail", and "fail-sss",
+ only the values "okay" and "disabled" are currently relevant to Zephyr.
+ """
+ return self._edtnode.status
+
+ @property
+ def enabled(self) -> bool:
+ """Whether this node is enabled, according to its status property.
+
+ For backwards compatibility, the value "ok" is treated the same
+ as "okay", but this usage is deprecated.
+ """
+ # edtlib.Node.status() has already substituted "ok" with "okay",
+ # no need to test both values again.
+ return self._edtnode.status == "okay"
+
+ @property
+ def aliases(self) -> Sequence[str]:
+ """The names this node is aliased to.
+
+ Retrieved from the "/aliases" node content (DTSpec 3.3).
+ """
+ return self._edtnode.aliases
+
+ @property
+ def chosen(self) -> List[str]:
+ """The parameters this node is a chosen for.
+
+ Retrieved from the "/chosen" node content (DTSpec 3.3).
+ """
+ return [
+ chosen
+ for chosen, node in self._dt.chosen_nodes.items()
+ if node is self
+ ]
+
+ @property
+ def labels(self) -> Sequence[str]:
+ """The labels attached to the node in the DTS (DTSpec 6.2)."""
+ return self._edtnode.labels
+
+ @property
+ def label(self) -> Optional[str]:
+ """A human readable description of the node device (DTSpec 4.1.2.3)."""
+ return self._edtnode.label
+
+ @property
+ def compatibles(self) -> Sequence[str]:
+ """Compatible strings, from most specific to most general (DTSpec 3.3.1)."""
+ return self._edtnode.compats
+
+ @property
+ def compatible(self) -> Optional[str]:
+ """The compatible string value that actually specifies the node content.
+
+ A few nodes have no compatible value.
+ """
+ return self._edtnode.matching_compat
+
+ @property
+ def vendor(self) -> Optional[DTVendor]:
+ """The device vendor.
+
+ This is the manufacturer for the compatible string
+ that specifies the node content.
+ """
+ if self.compatible:
+ return self._dt.get_vendor(self.compatible)
+
+ if self.binding:
+ # Search parent nodes for vendor up to child-binding depth.
+ node = self.parent
+ cb_depth = self.binding.cb_depth
+ while cb_depth != 0:
+ if node.vendor:
+ return node.vendor
+
+ cb_depth -= 1
+ node = node.parent
+
+ return None
+
+ @property
+ def binding_path(self) -> Optional[str]:
+ """Path to the binding file that specifies this node content."""
+ return self._edtnode.binding_path
+
+ @property
+ def binding(self) -> Optional[DTBinding]:
+ """The binding that specifies the node content, if any."""
+ return self._binding
+
+ @property
+ def on_bus(self) -> Optional[str]:
+ """The bus this node appears on, if any."""
+ # NOTE[edtlib]:
+ # What's the actual semantic of edtlib.Node.on_buses() ?
+ # Doesn't the node appear on a single bus in a given devicetree ?
+ return self.binding.on_bus if self.binding else None
+
+ @property
+ def on_bus_device(self) -> Optional["DTNode"]:
+ """The bus controller if this node is connected to a bus."""
+ if self._edtnode.bus_node:
+ return self._dt[self._edtnode.bus_node.path]
+ return None
+
+ @property
+ def buses(self) -> Sequence[str]:
+ """List of supported protocols if this node is a bus device.
+
+ Empty list if this node is not a bus node.
+ """
+ return self._edtnode.buses
+
+ @property
+ def interrupts(self) -> List[DTNodeInterrupt]:
+ """The interrupts generated by the node."""
+ return [
+ DTNodeInterrupt(edtirq, self) for edtirq in self._edtnode.interrupts
+ ]
+
+ @property
+ def registers(self) -> List[DTNodeRegister]:
+ """Addresses of the device resources (DTSpec 2.3.6)."""
+ return [DTNodeRegister(edtreg) for edtreg in self._edtnode.regs]
+
+ @property
+ def description(self) -> Optional[str]:
+ """The node description retrieved from its binding, if any."""
+ return self._edtnode.description
+
+ @property
+ def children(self) -> Sequence["DTNode"]:
+ """The node children, in DTS-order."""
+ return self._children
+
+ @property
+ def parent(self) -> "DTNode":
+ """The parent node.
+
+ By convention, the root node is its own parent.
+ """
+ return self._parent
+
+ @property
+ def dep_ordinal(self) -> int:
+ """Dependency ordinal.
+
+ Non-negative integer value such that the value for a node is less
+ than the value for all nodes that depend on it.
+
+ See edtlib.Node.dep_ordinal.
+ """
+ return self._edtnode.dep_ordinal
+
+ @property
+ def required_by(self) -> List["DTNode"]:
+ """The nodes that directly depend on this device."""
+ return [
+ self._dt[edt_node.path] for edt_node in self._edtnode.required_by
+ ]
+
+ @property
+ def depends_on(self) -> List["DTNode"]:
+ """The nodes this device directly depends on."""
+ return [
+ self._dt[edt_node.path] for edt_node in self._edtnode.depends_on
+ ]
+
+ def get_child(self, name: str) -> "DTNode":
+ """Retrieve a child node by name.
+
+ The requested child MUST exist.
+
+ Args:
+ name: The child node name to search for.
+
+ Returns:
+ The requested child.
+ """
+ for node in self._children:
+ if node.name == name:
+ return node
+ raise KeyError(name)
+
+ def walk(
+ self,
+ /,
+ order_by: Optional["DTNodeSorter"] = None,
+ reverse: bool = False,
+ enabled_only: bool = False,
+ fixed_depth: int = 0,
+ ) -> Iterator["DTNode"]:
+ """Walk the devicetree branch under this node.
+
+ Args:
+ order_by: Children ordering while walking through branches.
+ None will preserve the DTS order.
+ reverse: Whether to reverse sort order.
+ If set and no order_by is given, means reverse DTS-order.
+ enabled_only: Whether to stop at disabled branches.
+ fixed_depth: The depth limit.
+ Defaults to 0, which means walking through to leaf nodes,
+ according to enabled_only.
+
+ Returns:
+ An iterator yielding the nodes in order of traversal.
+ """
+ return self._walk(
+ self,
+ order_by=order_by,
+ reverse=reverse,
+ enabled_only=enabled_only,
+ fixed_depth=fixed_depth,
+ )
+
+ def rwalk(self) -> Iterator["DTNode"]:
+ """Walk the devicetree backward from this node through to the root node.
+
+ Returns:
+ An iterator yielding the nodes in order of traversal.
+ """
+ return self._rwalk(self)
+
+ def find(
+ self,
+ criterion: "DTNodeCriterion",
+ /,
+ order_by: Optional["DTNodeSorter"] = None,
+ reverse: bool = False,
+ enabled_only: bool = False,
+ ) -> List["DTNode"]:
+ """Search the devicetree branch under this node.
+
+ Args:
+ criterion: The search criterion children must match.
+ order_by: Sort matched nodes, None will preserve the DTS order.
+ reverse: Whether to reverse sort order.
+ If set and no order_by is given, means reverse DTS-order.
+ enabled_only: Whether to stop at disabled branches.
+
+ Returns:
+ The list of matched nodes.
+ """
+ nodes: List[DTNode] = [
+ node
+ for node in self.walk(
+ enabled_only=enabled_only,
+ # We don't want children ordering here,
+ # we'll sort results once finished.
+ order_by=None,
+ )
+ if criterion.match(node)
+ ]
+
+ # Sort matched nodes.
+ if order_by:
+ nodes = order_by.sort(nodes, reverse=reverse)
+ elif reverse:
+ # Reverse DTS-order.
+ nodes.reverse()
+ return nodes
+
+ def _walk(
+ self,
+ node: "DTNode",
+ /,
+ order_by: Optional["DTNodeSorter"],
+ reverse: bool,
+ enabled_only: bool,
+ fixed_depth: int,
+ at_depth: int = 0,
+ ) -> Iterator["DTNode"]:
+ if enabled_only and not node.enabled:
+ # Abort early on disabled branches when enabled_only is set.
+ return
+
+ # Yield branch and increment depth.
+ yield node
+ if fixed_depth > 0:
+ if at_depth == fixed_depth:
+ return
+ at_depth += 1
+
+ # Filter and sort children.
+ children = node.children
+ if enabled_only:
+ children = [child for child in children if child.enabled]
+ if order_by:
+ children = order_by.sort(children, reverse=reverse)
+ elif reverse:
+ children = list(reversed(children))
+
+ for child in children:
+ yield from self._walk(
+ child,
+ order_by=order_by,
+ reverse=reverse,
+ enabled_only=enabled_only,
+ fixed_depth=fixed_depth,
+ at_depth=at_depth,
+ )
+
+ def _rwalk(self, node: "DTNode") -> Iterator["DTNode"]:
+ # Walk the subtree backward to the root node,
+ # which is by convention its own parent.
+ yield node
+ if node.parent != node:
+ yield from self._rwalk(node.parent)
+
+ def __eq__(self, other: object) -> bool:
+ if isinstance(other, DTNode):
+ return other.path == self.path
+ return False
+
+ def __lt__(self, other: object) -> bool:
+ if isinstance(other, DTNode):
+ return self.path < other.path
+ raise TypeError(other)
+
+ def __hash__(self) -> int:
+ return hash(self.path)
+
+ def __repr__(self) -> str:
+ return self.path
+
+
+class DTModel:
+ """Devicetree model.
+
+ This is the main entry point dtsh will rely on to access
+ a successfully loaded devicetree.
+
+ It's a facade to an edtlib.EDT devicetree model,
+ with extensions and helpers for implementing the Devicetree shell.
+ """
+
+ # The EDT model this is a facade of.
+ _edt: edtlib.EDT
+
+ # Devicetree root.
+ _root: DTNode
+
+ # Devicetree source definition.
+ _dts: DTS
+
+ # Map path names to nodes,
+ # redundant with edtlib.EDT but "cheap".
+ _nodes: Dict[str, DTNode]
+
+ # Bindings identified by a compatible string:
+ # (compatible string, bus of appearance) -> binding
+ _compatible_bindings: Dict[Tuple[str, Optional[str]], DTBinding]
+
+ # Bindings without compatible string.
+ # (YAML file name, cb_depth) -> binding
+ _compatless_bindings: Dict[Tuple[str, int], DTBinding]
+
+ # YAML binding files included by actual device bindings.
+ # YAML file name -> binding
+ _base_bindings: Dict[str, DTBinding]
+
+ # Device and device class vendors that appear in this model.
+ # prefix (aka manufacturer) -> vendor
+ _vendors: Dict[str, DTVendor]
+
+ # See aliased_nodes().
+ _aliased_nodes: Dict[str, DTNode]
+
+ # See chosen_nodes().
+ _chosen_nodes: Dict[str, DTNode]
+
+ # See labeled_nodes().
+ _labeled_nodes: Dict[str, DTNode]
+
+ @staticmethod
+ def get_cb_depth(node: DTNode) -> int:
+ """Compute the child-binding depth for a node.
+
+ This is a non negative integer:
+
+ - initialized to zero
+ - incremented while walking the devicetree backward
+ until we reach a node whose binding does not have child-binding
+ """
+ cb_depth = 0
+ edtparent = node._edtnode.parent # pylint: disable=protected-access
+ while edtparent:
+ parent_binding = (
+ edtparent._binding # pylint: disable=protected-access
+ )
+ if parent_binding and parent_binding.child_binding:
+ cb_depth += 1
+ edtparent = edtparent.parent
+ else:
+ break
+ return cb_depth
+
+ @classmethod
+ def create(
+ cls,
+ dts_path: str,
+ binding_dirs: Optional[Sequence[str]] = None,
+ vendors_file: Optional[str] = None,
+ ) -> "DTModel":
+ """Devicetree model factory.
+
+ Args:
+ dts_path: The DTS file path.
+ binding_dirs: The DT bindings search path as a list of directories.
+ These directories are not required to exist.
+ If unset, a default search path is retrieved or worked out.
+ vendors_file: Path to a file in vendor-prefixes.txt format.
+
+ Returns:
+ An initialized devicetree model.
+
+ Raises:
+ OSError: Failed to open the DTS file.
+ EDTError: Failed to initialize the devicetree model.
+ """
+ return cls(DTS(dts_path, binding_dirs, vendors_file))
+
+ def __init__(self, dts: DTS) -> None:
+ """Initialize a Devicetree model.
+
+ Args:
+ dts: The devicetree source definition.
+
+ Raises:
+ OSError: Failed to open the DTS file.
+ EDTError: Failed to parse the DTS file, missing bindings.
+ """
+ self._dts = dts
+
+ # Vendors file support initialization.
+ self._vendors = {}
+ if self._dts.vendors_file:
+ # Load vendor definitions, and get the prefixes mapping
+ # to provide EDT() with.
+ vendor_prefixes = self._load_vendors_file(self._dts.vendors_file)
+ else:
+ vendor_prefixes = None
+
+ self._edt = edtlib.EDT(
+ self._dts.path,
+ # NOTE[PR-edtlib]: could EDT() accept a Sequence
+ # as const-qualified argument ?
+ list(self._dts.bindings_search_path),
+ vendor_prefixes=vendor_prefixes,
+ )
+
+ # Lazy-initialized base bindings.
+ self._base_bindings = {}
+ # Mappings initialized on 1st access.
+ self._labeled_nodes = {}
+ self._aliased_nodes = {}
+ self._chosen_nodes = {}
+
+ # The root node is its own parent.
+ self._root = DTNode(self._edt.get_node("/"), self, parent=None)
+
+ # Lazy-initialization.
+ self._compatible_bindings = {}
+ self._compatless_bindings = {}
+
+ # Walk the EDT model, recursively initializing peer nodes.
+ self._nodes = {}
+ self._init_dt(self._root)
+
+ @property
+ def dts(self) -> DTS:
+ """The devicetree source definition."""
+ return self._dts
+
+ @property
+ def root(self) -> DTNode:
+ """The devicetree root node."""
+ return self._root
+
+ @property
+ def size(self) -> int:
+ """The number of nodes in this model (including the root node)."""
+ return len(self._edt.nodes)
+
+ @property
+ def aliased_nodes(self) -> Mapping[str, DTNode]:
+ """Aliases retrieved from the "/aliases" node."""
+ if not self._aliased_nodes:
+ self._init_aliased_nodes()
+ return self._aliased_nodes
+
+ @property
+ def chosen_nodes(self) -> Mapping[str, DTNode]:
+ """Chosen configurations retrieved from the "/chosen" node."""
+ if not self._chosen_nodes:
+ self._init_chosen_nodes()
+ return self._chosen_nodes
+
+ @property
+ def labeled_nodes(self) -> Mapping[str, DTNode]:
+ """Nodes that can be referenced with a devicetree label."""
+ if not self._labeled_nodes:
+ self._init_labeled_nodes()
+ return self._labeled_nodes
+
+ @property
+ def bus_protocols(self) -> List[str]:
+ """All bus protocols supported by this model."""
+ buses: Set[str] = set()
+ for node in self._nodes.values():
+ buses.update(node.buses)
+ return list(buses)
+
+ @property
+ def compatible_strings(self) -> List[str]:
+ """All compatible strings that appear on some node in this model."""
+ return list(self._edt.compat2nodes.keys())
+
+ @property
+ def vendors(self) -> List[DTVendor]:
+ """Vendors for this model compatible strings.
+
+ NOTE: Will return an empty list if this model contains
+ compatible strings with undefined manufacturers.
+ """
+ if not self._vendors:
+ return []
+
+ try:
+ return [
+ self._vendors[prefix]
+ for prefix in {
+ compat.split(",", 1)[0]
+ for compat in self.compatible_strings
+ if "," in compat
+ }
+ ]
+ except KeyError as e:
+ print(f"WARN: invalid manufacturer: {e}", file=sys.stderr)
+ return []
+
+ def get_device_binding(self, node: DTNode) -> Optional[DTBinding]:
+ """Retrieve a device binding.
+
+ Args:
+ node: The device to get the bindings for.
+
+ Returns:
+ The device or device class binding,
+ or None if the node does not represent an actual device,
+ e.g. "/cpus".
+ """
+ edtnode, edtbinding = self._edtnode_edtbinding(node)
+ if not edtbinding:
+ return None
+
+ compat = edtbinding.compatible
+ if compat:
+ bus = edtbinding.on_bus
+ return self.get_compatible_binding(compat, bus)
+
+ cb_depth = self._edtnode_cb_depth(edtnode)
+ return self._get_compatless_binding(edtbinding, cb_depth)
+
+ def get_compatible_binding(
+ self, compat: str, bus: Optional[str] = None
+ ) -> Optional[DTBinding]:
+ """Access bindings identified by a compatible string.
+
+ If the lookup fails for the requested bus of appearance,
+ a binding is searched for the compatible string only.
+ This permits client code to uniformly enumerate the node bindings,
+ not all of which will relate to the bus the node may appear on:
+
+ for compat in node.compatibles:
+ binding = model.get_compatible_binding(node.compatible, node.on_bus)
+ if binding:
+ # Do something if the binding
+
+ Args:
+ compat: A compatible string to search the defining binding of.
+ bus: The bus that nodes with this binding should appear on, if any.
+
+ Returns:
+ The binding for compatible devices, or None if not found.
+ """
+ binding: Optional[DTBinding] = None
+
+ binding = self._compatible_bindings.get((compat, bus))
+ if not binding and bus:
+ binding = self._compatible_bindings.get((compat, None))
+
+ if not binding:
+ edtbinding = self._edt_compat2binding(compat, bus)
+ if edtbinding:
+ cb_depth = self._edtbinding_cb_depth(edtbinding)
+ binding = self._init_binding(edtbinding, cb_depth)
+
+ return binding
+
+ def get_base_binding(self, basename: str) -> DTBinding:
+ """Retrieve a base binding by its file name.
+
+ The requested binding file MUST exist.
+
+ This API is a factory and a cache.
+
+ Args:
+ basename: The binding file name.
+
+ Returns:
+ The binding the file specifies.
+ """
+ if basename not in self._base_bindings:
+ path = self.dts.yamlfs.find_path(basename)
+ if path:
+ self._base_bindings[basename] = self._load_binding_file(path)
+ else:
+ # Should not happen: all included YAML files have been
+ # loaded by EDT at this point, failing now to retrieve
+ # them again is worth a fault.
+ pass
+ return self._base_bindings[basename]
+
+ def get_vendor(self, compat: str) -> Optional[DTVendor]:
+ """Retrieve the vendor for a compatible string.
+
+ Args:
+ compat: The compatible string to search the vendor for.
+
+ Returns:
+ The device or device class vendor,
+ or None if the compatible string has no vendor prefix.
+ """
+ if self._dts.vendors_file and ("," in compat):
+ manufacturer, _ = compat.split(",", 1)
+ try:
+ return self._vendors[manufacturer]
+ except KeyError:
+ # All vendors that appear as manufacturer in compatible
+ # strings should exit: don't fault but log this nonetheless.
+ print(
+ f"WARN: unknown manufacturer: {manufacturer}",
+ file=sys.stderr,
+ )
+ return None
+
+ def get_compatible_devices(self, compat: str) -> List[DTNode]:
+ """Retrieve devices whose binding matches a given compatible string.
+
+ Args:
+ compat: The compatible string to match.
+
+ Returns:
+ The matched nodes.
+ """
+ return [
+ self[edt_node.path]
+ for edt_node in self._edt.compat2nodes[compat]
+ if edt_node.matching_compat == compat
+ ]
+
+ def walk(
+ self,
+ /,
+ order_by: Optional["DTNodeSorter"] = None,
+ reverse: bool = False,
+ enabled_only: bool = False,
+ fixed_depth: int = 0,
+ ) -> Iterator[DTNode]:
+ """Walk the devicetree from the root node through to all leaves.
+
+ Shortcut for root.walk().
+
+ Args:
+ order_by: Children ordering while walking branches.
+ None will preserve the DTS-order.
+ reverse: Whether to reverse walk order.
+ If set and no order_by is given, means reverse DTS-order.
+ enabled_only: Whether to stop at disabled branches.
+ fixed_depth: The depth limit.
+ Defaults to 0, which means walking through to leaf nodes,
+ according to enabled_only.
+
+ Returns:
+ A generator yielding the devicetree nodes in order of traversal.
+ """
+ return self._root.walk(
+ order_by=order_by,
+ reverse=reverse,
+ enabled_only=enabled_only,
+ fixed_depth=fixed_depth,
+ )
+
+ def find(
+ self,
+ criterion: "DTNodeCriterion",
+ /,
+ order_by: Optional["DTNodeSorter"] = None,
+ reverse: bool = False,
+ enabled_only: bool = False,
+ ) -> List[DTNode]:
+ """Search the devicetree.
+
+ Shortcut for root.find().
+
+ Args:
+ criterion: The search criterion nodes must match.
+ order_by: Sort matched nodes, None will preserve the DTS-order.
+ reverse: Whether to reverse found nodes order.
+ If set and no order_by is given, means reverse DTS-order.
+ enabled_only: Whether to stop at disabled branches.
+
+ Returns:
+ The list of matched nodes.
+ """
+ return self._root.find(
+ criterion,
+ order_by=order_by,
+ reverse=reverse,
+ enabled_only=enabled_only,
+ )
+
+ def __contains__(self, pathname: str) -> bool:
+ return pathname in self._nodes
+
+ def __getitem__(self, pathname: str) -> DTNode:
+ return self._nodes[pathname]
+
+ def __len__(self) -> int:
+ return self.size
+
+ def __iter__(self) -> Iterator[DTNode]:
+ return self.walk()
+
+ def _init_dt(self, branch: DTNode) -> None:
+ self._nodes[branch.path] = branch
+ # Append children in DTS order.
+ for (
+ edtchild
+ ) in (
+ branch._edtnode.children.values() # pylint: disable=protected-access
+ ):
+ child = DTNode(edtchild, self, branch)
+ self._nodes[child.path] = child
+ branch._children.append(child) # pylint: disable=protected-access
+ self._init_dt(child)
+
+ def _init_aliased_nodes(self) -> None:
+ self._aliased_nodes.update(
+ {
+ alias: self[dtnode.path]
+ # NOTE[PR-edtlib]: Could we have something like EDT.aliased_nodes ?
+ for alias, dtnode in self._edt._dt.alias2node.items() # pylint: disable=protected-access
+ }
+ )
+
+ def _init_chosen_nodes(self) -> None:
+ self._chosen_nodes.update(
+ {
+ chosen: self[edtnode.path]
+ for chosen, edtnode in self._edt.chosen_nodes.items()
+ }
+ )
+
+ def _init_labeled_nodes(self) -> None:
+ for node, labels in [
+ (node_, node_.labels) for node_ in self._nodes.values()
+ ]:
+ self._labeled_nodes.update({label: node for label in labels})
+
+ def _get_compatless_binding(
+ self, edtbinding: edtlib.Binding, cb_depth: int
+ ) -> DTBinding:
+ if not edtbinding.path:
+ raise ValueError(edtbinding)
+
+ basename = os.path.basename(edtbinding.path)
+ if (basename, cb_depth) not in self._compatless_bindings:
+ binding = self._init_binding(edtbinding, cb_depth)
+ self._compatless_bindings[(basename, cb_depth)] = binding
+
+ return self._compatless_bindings[(basename, cb_depth)]
+
+ def _init_binding(
+ self, edtbinding: edtlib.Binding, cb_depth: int
+ ) -> DTBinding:
+ if not edtbinding.path:
+ raise ValueError(f"Binding file expected: {edtlib.Binding}")
+
+ edtbinding_child = edtbinding.child_binding
+ if edtbinding_child:
+ child_binding = self._init_binding(edtbinding_child, cb_depth + 1)
+ else:
+ child_binding = None
+
+ binding = DTBinding(edtbinding, cb_depth, child_binding)
+
+ if edtbinding.compatible:
+ compat = edtbinding.compatible
+ bus = edtbinding.on_bus
+ self._compatible_bindings[(compat, bus)] = binding
+ else:
+ basename = os.path.basename(edtbinding.path)
+ self._compatless_bindings[(basename, cb_depth)] = binding
+
+ return binding
+
+ def _edt_compat2binding(
+ self, compat: str, bus: Optional[str]
+ ) -> Optional[edtlib.Binding]:
+ edtbinding = (
+ self._edt._compat2binding.get( # pylint: disable=protected-access
+ (compat, bus)
+ )
+ )
+ if not edtbinding and bus:
+ edtbinding = self._edt._compat2binding.get( # pylint: disable=protected-access
+ (compat, None)
+ )
+ return edtbinding
+
+ def _edtbinding_cb_depth(self, edtbinding: edtlib.Binding) -> int:
+ if not edtbinding.compatible:
+ raise ValueError(edtbinding)
+
+ # We're computing the child-binding depth of any node
+ # whose compatible value and bus of appearance
+ # match this binding.
+ compat = edtbinding.compatible
+ bus = edtbinding.on_bus
+
+ for edtnode in self._edt.compat2nodes[compat]:
+ binding = edtnode._binding # pylint: disable=protected-access
+ if binding and (binding.on_bus == bus):
+ return self._edtnode_cb_depth(edtnode)
+
+ # The binding does not appear in any compatible string in the model.
+ raise ValueError(edtbinding)
+
+ def _edtnode_cb_depth(self, edtnode: edtlib.Node) -> int:
+ cb_depth = 0
+
+ # Walk the devicetree backward until we're not specified
+ # by a child-binding.
+ parent = edtnode.parent
+ while parent:
+ binding = parent._binding # pylint: disable=protected-access
+ if binding and binding.child_binding:
+ cb_depth += 1
+ parent = parent.parent
+ else:
+ parent = None
+
+ return cb_depth
+
+ def _edtnode_edtbinding(
+ self, node: DTNode
+ ) -> Tuple[edtlib.Node, Optional[edtlib.Binding]]:
+ return (
+ node._edtnode, # pylint: disable=protected-access
+ node._edtnode._binding, # pylint: disable=protected-access
+ )
+
+ def _load_binding_file(self, path: str) -> DTBinding:
+ edtbinding = edtlib.Binding(
+ path,
+ # NOTE[PR-edtlib]: patch Binding ctor to accept a Mapping
+ # as const-qualified argument.
+ fname2path=dict(self._dts.yamlfs.name2path),
+ require_compatible=False,
+ require_description=False,
+ )
+ return DTBinding(edtbinding, 0, None)
+
+ def _load_vendors_file(self, vendors_file: str) -> Dict[str, str]:
+ vendor_prefixes = edtlib.load_vendor_prefixes_txt(vendors_file)
+ self._vendors.update(
+ {
+ prefix: DTVendor(prefix, name)
+ for prefix, name in vendor_prefixes.items()
+ }
+ )
+ return vendor_prefixes
+
+
+class DTNodeSorter:
+ """Basis for order relationships applicable to nodes.
+
+ This defines stateless adapters for the standard Python sorted() function
+ that support input lists containing nodes for which the key function
+ would return None values.
+
+ This is a 3-stage sort:
+
+ - split nodes into sortable and unsortable
+ - sort those that can be sorted using
+ - append the unsortable to the sorted (by default, nodes for which
+ the key function would have no value appear last)
+
+ This base implementation sort nodes into "natural order".
+ """
+
+ def split_sortable_unsortable(
+ self, nodes: Sequence[DTNode]
+ ) -> Tuple[List[DTNode], List[DTNode]]:
+ """Split nodes into sortable and unsortable.
+
+ Returns:
+ The tuple of (sortable, unsortable) nodes.
+ This base implementation returns all nodes.
+ """
+ return (list(nodes), [])
+
+ def weight(self, node: DTNode) -> Any:
+ """The key function.
+
+ Args:
+ node: The node to get the weight of.
+
+ Returns:
+ The weight this sorter gives to the node.
+ This base implementation returns the node itself ("natural order").
+ """
+ # TODO[python]: Where is SupportsLessThanT defined ?
+ return node
+
+ def sort(
+ self, nodes: Sequence[DTNode], reverse: bool = False
+ ) -> List[DTNode]:
+ """Sort nodes.
+
+ Args:
+ nodes: The nodes to sort.
+ reverse: Whether to reverse sort order.
+
+ Returns:
+ A list with the nodes sorted.
+ """
+ sortable, unsortable = self.split_sortable_unsortable(nodes)
+
+ # We can't rely on Python sort(reverse=True) when multiple
+ # items would pretend for the "same" position in the sorted
+ # list (e.g. sorting nodes with the same unit-name by unit-name):
+ # the Python sort() implementation is not granted to actually
+ # reverse the items in the way we'd expect,
+ # e.g. for the "-r" option that should always reverse
+ # the command output order.
+ sortable.sort(key=self.weight)
+ if reverse:
+ sortable.reverse()
+ # Reverse order also applies to unsortable (sic).
+ unsortable.reverse()
+
+ # Unsortable first.
+ unsortable.extend(sortable)
+ return unsortable
+
+ # Unsortable last.
+ sortable.extend(unsortable)
+ return sortable
+
+
+class DTNodeCriterion:
+ """Base criterion for searching or filtering DT nodes.
+
+ This base criterion does not match with any node.
+ """
+
+ def match(self, node: DTNode) -> bool:
+ """Match a node with this criterion.
+
+ Args:
+ node: The DT node to match.
+
+ Returns:
+ True if the node matches with this criterion.
+ """
+ del node # Unused argument.
+ return False
+
+
+class DTNodeCriteria(DTNodeCriterion):
+ """Chain of criterion for searching or filtering DT nodes.
+
+ The criterion chain is evaluated either as:
+
+ - a logical conjunction, and fails at the first unmatched criterion (default)
+ - a logical disjunction, and succeeds at the first matched criterion
+
+ By default, an empty chain will match any node.
+
+ A logical negation may eventually be applied to the chain.
+ """
+
+ _criteria: List[DTNodeCriterion]
+ _ored_chain: bool
+ _negative_chain: bool
+
+ def __init__(
+ self,
+ criterion_chain: Optional[List[DTNodeCriterion]] = None,
+ ored_chain: bool = False,
+ negative_chain: bool = False,
+ ) -> None:
+ """Initialize a new chain of criterion.
+
+ Args:
+ *criteria: Initial criterion chain.
+ ored_chain: Whether this chain is a logical disjonction of
+ criterion (default is logical conjunction).
+ negative_chain: Whether to apply a logical negation to the chain.
+ """
+ self._criteria = criterion_chain if criterion_chain else []
+ self._ored_chain = ored_chain
+ self._negative_chain = negative_chain
+
+ def append_criterion(self, criterion: DTNodeCriterion) -> None:
+ """Append a criterion.
+
+ Args:
+ criterion: The criterion to append to this chain.
+ """
+ self._criteria.append(criterion)
+
+ def match(self, node: DTNode) -> bool:
+ """Does a node match this chain of criterion ?
+
+ Args:
+ node: The node to match.
+
+ Returns:
+ True if the node matches this criteria.
+ """
+ if self._ored_chain:
+ chain_match = any(
+ criterion.match(node) for criterion in self._criteria
+ )
+ else:
+ chain_match = all(
+ criterion.match(node) for criterion in self._criteria
+ )
+
+ return (not chain_match) if self._negative_chain else chain_match
diff --git a/src/dtsh/modelutils.py b/src/dtsh/modelutils.py
new file mode 100644
index 0000000..9ccdf3b
--- /dev/null
+++ b/src/dtsh/modelutils.py
@@ -0,0 +1,724 @@
+# Copyright (c) 2023 Christophe Dufaza
+#
+# SPDX-License-Identifier: Apache-2.0
+
+"""Devicetree model helpers.
+
+- Match node with text based criteria (implement DTNodeTextCriterion)
+- Match node with integer based criteria (implement DTNodeIntCriterion)
+- Sort nodes (implement DTNodeSorter)
+- Arbitrary virtual devicetree (DTWalkableComb)
+
+Unit tests and examples: tests/test_dtsh_modelutils.py
+"""
+
+
+from typing import (
+ Any,
+ Callable,
+ Tuple,
+ Set,
+ List,
+ Optional,
+ Sequence,
+ Iterator,
+ Mapping,
+)
+
+import operator
+import re
+import sys
+
+from dtsh.model import DTWalkable, DTNode, DTNodeSorter, DTNodeCriterion
+
+
+class DTNodeSortByAttr(DTNodeSorter):
+ """Base for sorters that weight node attributes."""
+
+ # The name of the dtsh.model.Node attribute this sorter is based on.
+ _attname: str
+
+ # Whether we should compare minimums or maximums when the attribute
+ # value is a list.
+ _reverse: bool = False
+
+ def __init__(self, attname: str) -> None:
+ """Initialize sorter.
+
+ Args:
+ attname: The Python attribute name.
+ """
+ self._attname = attname
+
+ def split_sortable_unsortable(
+ self, nodes: Sequence[DTNode]
+ ) -> Tuple[List[DTNode], List[DTNode]]:
+ """Overrides DTNodeSorter.split_sortable_unsortable().
+
+ Returns:
+ The tuple of (sortable, unsortable) where unsortable include
+ nodes for which :
+ - the attribute has no value
+ - the attribute value is an empty lists: [] is less than
+ any non empty list, which would not match
+ the expected semantic (empty lists would appear first)
+ """
+ sortable = []
+ unsortable = []
+ for node in nodes:
+ attr = getattr(node, self._attname)
+ if (attr is not None) and (attr != []):
+ sortable.append(node)
+ else:
+ unsortable.append(node)
+ return (sortable, unsortable)
+
+ def gravity(self, node: DTNode) -> Any:
+ """Input of the weight function.
+
+ This is typically the value of the attribute this sorter is based on,
+ but subclasses may override this method to provide a more precise
+ semantic: e.g. a sorter based on the node registers may either
+ weight the register addresses or the register sizes.
+
+ Args:
+ node: The node to weight.
+
+ Returns:
+ The input for the weight function.
+ """
+ return getattr(node, self._attname)
+
+ def weight(self, node: DTNode) -> Any:
+ """Overrides DTNodeSorter.weight().
+
+ The node weight is based on its gravity.
+
+ If the gravity value is a list:
+
+ - in ascending order: we expect to compare minimum,
+ so the weight is min(gravity)
+ - in descending order: we expect to compare maximum,
+ so the weight is max(gravity)
+
+ Returns:
+ The node weight.
+ """
+ w = self.gravity(node)
+ if isinstance(w, list):
+ w = max(w) if self._reverse else min(w)
+ return w
+
+ def sort(
+ self, nodes: Sequence[DTNode], reverse: bool = False
+ ) -> List[DTNode]:
+ """Overrides DTNodeSorter.sort()."""
+ # Set the reverse flag that the weight function will rely on.
+ self._reverse = reverse
+ # Then sort nodes with the base DTNodeSorter implementation.
+ return super().sort(nodes, reverse)
+
+
+class DTNodeSortByPathName(DTNodeSortByAttr):
+ """Sort nodes by path name."""
+
+ def __init__(self) -> None:
+ super().__init__("path")
+
+
+class DTNodeSortByNodeName(DTNodeSortByAttr):
+ """Sort nodes by node name."""
+
+ def __init__(self) -> None:
+ super().__init__("name")
+
+
+class DTNodeSortByUnitName(DTNodeSortByAttr):
+ """Sort nodes by unit-name."""
+
+ def __init__(self) -> None:
+ super().__init__("unit_name")
+
+
+class DTNodeSortByUnitAddr(DTNodeSortByAttr):
+ """Sort nodes by unit-address."""
+
+ def __init__(self) -> None:
+ super().__init__("unit_addr")
+
+
+class DTNodeSortByCompatible(DTNodeSortByAttr):
+ """Sort nodes by compatible strings."""
+
+ def __init__(self) -> None:
+ super().__init__("compatibles")
+
+
+class DTNodeSortByBinding(DTNodeSortByAttr):
+ """Sort nodes by binding (compatible value)."""
+
+ def __init__(self) -> None:
+ super().__init__("compatible")
+
+
+class DTNodeSortByVendor(DTNodeSortByAttr):
+ """Sort nodes by vendor name."""
+
+ def __init__(self) -> None:
+ super().__init__("vendor")
+
+ def gravity(self, node: DTNode) -> Any:
+ """Overrides DTNodeSortByAttrValue.weight().
+
+ Returns:
+ The vendor name.
+ """
+ # At this point, we know the device has a vendor.
+ return [node.vendor.name] # type: ignore
+
+
+class DTNodeSortByDeviceLabel(DTNodeSortByAttr):
+ """Sort nodes by device label."""
+
+ def __init__(self) -> None:
+ super().__init__("label")
+
+
+class DTNodeSortByNodeLabel(DTNodeSortByAttr):
+ """Sort nodes by DTS label."""
+
+ def __init__(self) -> None:
+ super().__init__("labels")
+
+
+class DTNodeSortByAlias(DTNodeSortByAttr):
+ """Sort nodes by alias."""
+
+ def __init__(self) -> None:
+ super().__init__("aliases")
+
+
+class DTNodeSortByBus(DTNodeSortByAttr):
+ """Sort nodes by supported bus protocols."""
+
+ def __init__(self) -> None:
+ super().__init__("buses")
+
+
+class DTNodeSortByOnBus(DTNodeSortByAttr):
+ """Sort nodes by bus of appearance."""
+
+ def __init__(self) -> None:
+ super().__init__("on_bus")
+
+
+class DTNodeSortByDepOrdinal(DTNodeSortByAttr):
+ """Sort nodes by dependency ordinal."""
+
+ def __init__(self) -> None:
+ super().__init__("dep_ordinal")
+
+
+class DTNodeSortByIrqNumber(DTNodeSortByAttr):
+ """Sort nodes by interrupt number."""
+
+ def __init__(self) -> None:
+ super().__init__("interrupts")
+
+ def gravity(self, node: DTNode) -> Any:
+ """Overrides DTNodeSortByAttrValue.weight().
+
+ Returns:
+ The interrupt numbers.
+ """
+ return [irq.number for irq in node.interrupts]
+
+
+class DTNodeSortByIrqPriority(DTNodeSortByAttr):
+ """Sort nodes by interrupt priority."""
+
+ def __init__(self) -> None:
+ super().__init__("interrupts")
+
+ def gravity(self, node: DTNode) -> Any:
+ """Overrides DTNodeSortByAttrValue.weight().
+
+ Returns:
+ The interrupt priorities.
+ """
+ return [
+ irq.priority if irq.priority is not None else sys.maxsize
+ for irq in node.interrupts
+ ]
+
+
+class DTNodeSortByRegAddr(DTNodeSortByAttr):
+ """Sort nodes by register address."""
+
+ def __init__(self) -> None:
+ super().__init__("registers")
+
+ def gravity(self, node: DTNode) -> Any:
+ """Overrides DTNodeSortByAttrValue.weight().
+
+ Returns:
+ The register addresses.
+ """
+ return [reg.address for reg in node.registers]
+
+
+class DTNodeSortByRegSize(DTNodeSortByAttr):
+ """Sort nodes by register size."""
+
+ def __init__(self) -> None:
+ super().__init__("registers")
+
+ def gravity(self, node: DTNode) -> Any:
+ """Overrides DTNodeSortByAttrValue.weight().
+
+ Returns:
+ The register addresses.
+ """
+ return [reg.size for reg in node.registers]
+
+
+class DTNodeSortByBindingDepth(DTNodeSortByAttr):
+ """Sort nodes by child-binding depth."""
+
+ def __init__(self) -> None:
+ super().__init__("binding")
+
+ def gravity(self, node: DTNode) -> Any:
+ """Overrides DTNodeSortByAttrValue.weight().
+
+ Returns:
+ The interrupt priorities.
+ """
+ return node.binding.cb_depth if node.binding else None
+
+
+class DTNodeTextCriterion(DTNodeCriterion):
+ """Basis for text-based (pattern) criteria.
+
+ A search pattern is matched to a node aspect that has
+ a textual representation.
+
+ This criterion may either match a Regular Expression
+ or search for plain text.
+
+ When the pattern is a strict RE, the criterion behaves as a RE-match,
+ and any character in the pattern may be interpreted as special character:
+
+ - in particular, "*" will represent a repetition qualifier,
+ not a wild-card for any character: e.g. a pattern starting with "*"
+ would be an invalid RE because there's nothing to repeat
+ - parenthesis will group sub-expressions, as in "(image|storage).*"
+ - brackets will mark the beginning ("[") and end ("]") of a character set
+
+ When the pattern is not a strict RE, but contains at least one "*":
+
+ - "*" is actually interpreted as a wild-card and not a repetition qualifier:
+ here "*" is a valid expression that actually means "anything"
+ - the criterion behaves as a RE-match: "*pattern" means ends with "pattern",
+ "pattern*" starts with "pattern", and "*pattern*" contains "pattern"
+
+ If the pattern is not a strict RE, and does not contain any "*":
+
+ - specials characters won't be interpreted (plain text search)
+ - the criterion will behave as a RE-search
+ """
+
+ # The PATTERN argument from the command line.
+ _pattern: str
+
+ # The RE that implements this criterion.
+ _re: re.Pattern[str]
+
+ def __init__(
+ self, pattern: str, re_strict: bool = False, ignore_case: bool = False
+ ) -> None:
+ """Initialize criterion.
+
+ Args:
+ pattern: The string pattern.
+ re_strict: Whether to assume the pattern us a Regular Expression.
+ Default is plain text search with wild-card substitution.
+ ignore_case: Whether to ignore case.
+ Default is case sensitive search.
+
+ Raises:
+ re.error: Malformed regular expression.
+ """
+ self._pattern = pattern
+ if re_strict:
+ self._re = self._init_strict_re(pattern, ignore_case)
+ else:
+ self._re = self._init_plain_text(pattern, ignore_case)
+
+ @property
+ def pattern(self) -> str:
+ """The pattern string this criterion is built on."""
+ return self._pattern
+
+ def match(self, node: DTNode) -> bool:
+ """Overrides DTNodeCriterion.match()."""
+ return any(
+ self._re.match(txt) is not None for txt in self.get_haystack(node)
+ )
+
+ def get_haystack(self, node: DTNode) -> Sequence[str]:
+ """Get the textual representation of the haystack to search.
+
+ The criterion is always False when the haystack is empty.
+
+ Returns:
+ The strings that this pattern may match.
+ """
+ del node
+ return []
+
+ def _init_strict_re(
+ self, pattern: str, ignore_case: bool
+ ) -> re.Pattern[str]:
+ # RE strict mode, use pattern (e.g. from command string) as-is.
+ return re.compile(pattern, flags=re.IGNORECASE if ignore_case else 0)
+
+ def _init_plain_text(
+ self, pattern: str, ignore_case: bool
+ ) -> re.Pattern[str]:
+ # Plain text search, escape all.
+ pattern = re.escape(pattern)
+ if r"\*" in pattern:
+ # Convert wild-card to repeated printable.
+ pattern = pattern.replace(r"\*", ".*")
+ # Ensure starts/ends with semantic.
+ pattern = f"^{pattern}$"
+ else:
+ # Convert RE-match to RE-search.
+ pattern = f".*{pattern}.*"
+
+ return re.compile(pattern, flags=re.IGNORECASE if ignore_case else 0)
+
+ def __repr__(self) -> str:
+ return self._re.pattern
+
+
+class DTNodeWithPath(DTNodeTextCriterion):
+ """Match path."""
+
+ def get_haystack(self, node: DTNode) -> Sequence[str]:
+ """Overrides DTNodeTextCriterion.get_haystack()."""
+ return [node.path]
+
+
+class DTNodeWithStatus(DTNodeTextCriterion):
+ """Match status string."""
+
+ def get_haystack(self, node: DTNode) -> Sequence[str]:
+ """Overrides DTNodeTextCriterion.get_haystack()."""
+ return [node.status]
+
+
+class DTNodeWithName(DTNodeTextCriterion):
+ """Match node name."""
+
+ def get_haystack(self, node: DTNode) -> Sequence[str]:
+ """Overrides DTNodeTextCriterion.get_haystack()."""
+ return [node.name]
+
+
+class DTNodeWithUnitName(DTNodeTextCriterion):
+ """Match unit-name."""
+
+ def get_haystack(self, node: DTNode) -> Sequence[str]:
+ """Overrides DTNodeTextCriterion.get_haystack()."""
+ return [node.unit_name]
+
+
+class DTNodeWithCompatible(DTNodeTextCriterion):
+ """Match compatible value."""
+
+ def get_haystack(self, node: DTNode) -> Sequence[str]:
+ """Overrides DTNodeTextCriterion.get_haystack()."""
+ return node.compatibles
+
+
+class DTNodeWithBinding(DTNodeTextCriterion):
+ """Match binding compatible string.
+
+ If the pattern is "*", will match any node with a binding,
+ including bindings without compatible string.
+ """
+
+ def match(self, node: DTNode) -> bool:
+ """Overrides DTNodeCriterion.match()."""
+ if self.pattern == ".*":
+ return node.binding is not None
+ return super().match(node)
+
+ def get_haystack(self, node: DTNode) -> Sequence[str]:
+ """Overrides DTNodeTextCriterion.get_haystack()."""
+ if not node.binding:
+ return []
+
+ haystack = []
+ if node.binding.compatible:
+ haystack.append(node.binding.compatible)
+
+ headline = node.binding.get_headline()
+ if headline:
+ haystack.append(headline)
+
+ return haystack
+
+
+class DTNodeWithVendor(DTNodeTextCriterion):
+ """Match vendor prefix or name."""
+
+ def get_haystack(self, node: DTNode) -> Sequence[str]:
+ """Overrides DTNodeTextCriterion.get_haystack()."""
+ return [node.vendor.prefix, node.vendor.name] if node.vendor else []
+
+
+class DTNodeWithDeviceLabel(DTNodeTextCriterion):
+ """Match device label."""
+
+ def get_haystack(self, node: DTNode) -> Sequence[str]:
+ """Overrides DTNodeTextCriterion.get_haystack()."""
+ return [node.label] if node.label else []
+
+
+class DTNodeWithNodeLabel(DTNodeTextCriterion):
+ """Match DTS labels."""
+
+ def get_haystack(self, node: DTNode) -> Sequence[str]:
+ """Overrides DTNodeTextCriterion.get_haystack()."""
+ return node.labels
+
+
+class DTNodeWithAlias(DTNodeTextCriterion):
+ """Match aliases."""
+
+ def get_haystack(self, node: DTNode) -> Sequence[str]:
+ """Overrides DTNodeTextCriterion.get_haystack()."""
+ return node.aliases
+
+
+class DTNodeWithChosen(DTNodeTextCriterion):
+ """Match chosen."""
+
+ def get_haystack(self, node: DTNode) -> Sequence[str]:
+ """Overrides DTNodeTextCriterion.get_haystack()."""
+ return node.chosen
+
+
+class DTNodeWithBus(DTNodeTextCriterion):
+ """Match nodes with supported bus protocols."""
+
+ def get_haystack(self, node: DTNode) -> Sequence[str]:
+ """Overrides DTNodeTextCriterion.get_haystack()."""
+ return node.buses
+
+
+class DTNodeWithOnBus(DTNodeTextCriterion):
+ """Match nodes with bus of appearance."""
+
+ def get_haystack(self, node: DTNode) -> Sequence[str]:
+ """Overrides DTNodeTextCriterion.get_haystack()."""
+ return [node.on_bus] if node.on_bus else []
+
+
+class DTNodeWithDescription(DTNodeTextCriterion):
+ """Match node with description line by line."""
+
+ def get_haystack(self, node: DTNode) -> Sequence[str]:
+ """Overrides DTNodeTextCriterion.get_haystack()."""
+ if not node.description:
+ return []
+ return node.description.splitlines()
+
+
+class DTNodeAlsoKnownAs(DTNodeTextCriterion):
+ """Match nodes with labels and aliases."""
+
+ def get_haystack(self, node: DTNode) -> Sequence[str]:
+ """Overrides DTNodeTextCriterion.get_haystack()."""
+ aka = [
+ *node.aliases,
+ *node.labels,
+ ]
+ if node.label:
+ aka.append(node.label)
+ return aka
+
+
+class DTNodeIntCriterion(DTNodeCriterion):
+ """Basis for integer-based (expression) criteria."""
+
+ OPERATORS: Mapping[str, Callable[[int, int], bool]] = {
+ "<": operator.lt,
+ ">": operator.gt,
+ "=": operator.eq,
+ ">=": operator.ge,
+ "<=": operator.le,
+ "!=": operator.ne,
+ }
+
+ _operator: Callable[[int, int], bool]
+ _int: Optional[int]
+
+ def __init__(
+ self,
+ criter_op: Optional[Callable[[int, int], bool]],
+ criter_int: Optional[int],
+ ) -> None:
+ """Initialize criterion.
+
+ Args:
+ criter_op: The expression operator,
+ one of DTNodeIntCriterion.OPERATORS.
+ Defaults to equality.
+ criter_int: The integer value the criterion expression should match.
+ None means any integer value will match.
+ """
+ self._operator = criter_op or operator.eq
+ self._int = criter_int
+
+ def match(self, node: DTNode) -> bool:
+ """Overrides DTNodeCriterion.match()."""
+ return any(
+ ((self._int is None) or self._operator(hay, self._int))
+ for hay in self.get_haystack(node)
+ )
+
+ def get_haystack(self, node: DTNode) -> Sequence[int]:
+ """Get the integer representation of the haystack to search.
+
+ The criterion is always False when the haystack is empty.
+
+ Returns:
+ The integers that this expression may match.
+ """
+ del node
+ return []
+
+
+class DTNodeWithUnitAddr(DTNodeIntCriterion):
+ """Match unit-address."""
+
+ def get_haystack(self, node: DTNode) -> Sequence[int]:
+ """Overrides DTNodeIntCriterion.get_haystack()."""
+ return [node.unit_addr] if node.unit_addr is not None else []
+
+
+class DTNodeWithIrqNumber(DTNodeIntCriterion):
+ """Match IRQ number."""
+
+ def get_haystack(self, node: DTNode) -> Sequence[int]:
+ """Overrides DTNodeIntCriterion.get_haystack()."""
+ return [irq.number for irq in node.interrupts]
+
+
+class DTNodeWithIrqPriority(DTNodeIntCriterion):
+ """Match IRQ priority."""
+
+ def get_haystack(self, node: DTNode) -> Sequence[int]:
+ """Overrides DTNodeIntCriterion.get_haystack()."""
+ return [
+ irq.priority for irq in node.interrupts if irq.priority is not None
+ ]
+
+
+class DTNodeWithRegAddr(DTNodeIntCriterion):
+ """Match register address."""
+
+ def get_haystack(self, node: DTNode) -> Sequence[int]:
+ """Overrides DTNodeIntCriterion.get_haystack()."""
+ return [reg.address for reg in node.registers]
+
+
+class DTNodeWithRegSize(DTNodeIntCriterion):
+ """Match register size."""
+
+ def get_haystack(self, node: DTNode) -> Sequence[int]:
+ """Overrides DTNodeIntCriterion.get_haystack()."""
+ return [reg.size for reg in node.registers]
+
+
+class DTNodeWithBindingDepth(DTNodeIntCriterion):
+ """Match child-binding depth."""
+
+ def get_haystack(self, node: DTNode) -> Sequence[int]:
+ """Overrides DTNodeIntCriterion.get_haystack()."""
+ return [node.binding.cb_depth] if node.binding else []
+
+
+class DTWalkableComb(DTWalkable):
+ """Walk an arbitrary subset of a devicetree.
+
+ Permits to define a virtual devicetree as the minimal graph
+ that will contain the paths from a given root to a selected
+ set of leaves.
+
+ The comb is the set of nodes this graph includes.
+ """
+
+ _root: DTNode
+ _comb: Set[DTNode]
+
+ def __init__(self, root: DTNode, leaves: Sequence[DTNode]) -> None:
+ """Initialize the virtual devicetree.
+
+ Args:
+ root: Set the root node from where we'll later on
+ walk the virtual devicetree.
+ leaves: The leaf nodes of the virtual devicetree.
+ """
+ self._root = root
+ self._comb: Set[DTNode] = set()
+ for leaf in leaves:
+ self._comb.update(list(leaf.rwalk()))
+
+ @property
+ def comb(self) -> Set[DTNode]:
+ """All the nodes required to represent this virtual devicetree."""
+ return self._comb
+
+ def walk(
+ self,
+ /,
+ order_by: Optional[DTNodeSorter] = None,
+ reverse: bool = False,
+ enabled_only: bool = False,
+ fixed_depth: int = 0,
+ ) -> Iterator[DTNode]:
+ """Walk from the predefined root node through to all leaves.
+
+ Overrides DTWalkable.walk().
+
+ Args:
+ enabled_only: Ignored, will always walk through to its leaves.
+ fixed_depth: Ignored, will always walk through to its leaves.
+ """
+ return self._walk(self._root, order_by=order_by, reverse=reverse)
+
+ def _walk(
+ self,
+ branch: Optional[DTNode] = None,
+ /,
+ order_by: Optional[DTNodeSorter] = None,
+ reverse: bool = False,
+ ) -> Iterator[DTNode]:
+ if branch in self._comb:
+ yield branch
+ children = branch.children
+ if children:
+ if order_by:
+ children = order_by.sort(children, reverse=reverse)
+ elif reverse:
+ # Reverse DTS-order.
+ children = list(reversed(children))
+ for child in children:
+ yield from self._walk(
+ child, order_by=order_by, reverse=reverse
+ )
diff --git a/src/dtsh/rich/__init__.py b/src/dtsh/rich/__init__.py
new file mode 100644
index 0000000..c9d1cc5
--- /dev/null
+++ b/src/dtsh/rich/__init__.py
@@ -0,0 +1,5 @@
+# Copyright (c) 2023 Christophe Dufaza
+#
+# SPDX-License-Identifier: Apache-2.0
+
+"""Rich Text User Interface."""
diff --git a/src/dtsh/rich/autocomp.py b/src/dtsh/rich/autocomp.py
new file mode 100644
index 0000000..d8172d4
--- /dev/null
+++ b/src/dtsh/rich/autocomp.py
@@ -0,0 +1,226 @@
+# Copyright (c) 2023 Christophe Dufaza
+#
+# SPDX-License-Identifier: Apache-2.0
+
+"""Rich display callback for GNU readline integration."""
+
+
+from typing import Optional, Sequence, Set
+
+import os
+
+from rich.text import Text
+
+from dtsh.io import DTShOutput
+from dtsh.rl import DTShReadline
+from dtsh.autocomp import (
+ DTShAutocomp,
+ RlStateDTShCommand,
+ RlStateDTShOption,
+ RlStateDTPath,
+ RlStateCompatStr,
+ RlStateDTVendor,
+ RlStateDTBus,
+ RlStateDTAlias,
+ RlStateDTChosen,
+ RlStateDTLabel,
+ RlStateFsEntry,
+ RlStateEnum,
+)
+
+from dtsh.rich.theme import DTShTheme
+from dtsh.rich.text import TextUtil
+from dtsh.rich.tui import GridLayout
+
+
+class DTShRichAutocomp(DTShAutocomp):
+ """Rich display callbacks for GNU readline integration."""
+
+ def display(
+ self,
+ out: DTShOutput,
+ states: Sequence[DTShReadline.CompleterState],
+ ) -> None:
+ """Rich display callback.
+
+ Style completions based on their semantic.
+
+ Implements DTShReadline.DisplayCallback.
+ Overrides DTShAutocomp.display().
+
+ Args:
+ out: Where to display these completions.
+ states: The completer states to display.
+ """
+ grid = GridLayout(2, padding=(0, 4, 0, 0))
+
+ for state in states:
+ if isinstance(state, RlStateDTShCommand):
+ self._rlstates_view_add_dtshcmd(grid, state)
+
+ elif isinstance(state, RlStateDTShOption):
+ self._rlstates_view_add_dtshopt(grid, state)
+
+ elif isinstance(state, RlStateDTPath):
+ self._rlstates_view_add_dtpath(grid, state)
+
+ elif isinstance(state, RlStateCompatStr):
+ self._rlstates_view_add_compatstr(grid, state)
+
+ elif isinstance(state, RlStateDTVendor):
+ self._rlstates_view_add_vendor(grid, state)
+
+ elif isinstance(state, RlStateDTBus):
+ self._rlstates_view_add_bus(grid, state)
+
+ elif isinstance(state, RlStateDTAlias):
+ self._rlstates_view_add_alias(grid, state)
+
+ elif isinstance(state, RlStateDTChosen):
+ self._rlstates_view_add_chosen(grid, state)
+
+ elif isinstance(state, RlStateDTLabel):
+ self._rlstates_view_add_label(grid, state)
+
+ elif isinstance(state, RlStateFsEntry):
+ self._rlstates_view_add_fspath(grid, state)
+
+ elif isinstance(state, RlStateEnum):
+ self._rlstates_view_add_enum(grid, state)
+
+ else:
+ grid.add_row(state.rlstr, None)
+
+ out.write(grid)
+
+ def _rlstates_view_add_dtshcmd(
+ self, grid: GridLayout, state: RlStateDTShCommand
+ ) -> None:
+ grid.add_row(TextUtil.bold(state.cmd.name), state.cmd.brief)
+
+ def _rlstates_view_add_dtshopt(
+ self, grid: GridLayout, state: RlStateDTShOption
+ ) -> None:
+ grid.add_row(TextUtil.bold(state.opt.usage), state.opt.brief)
+
+ def _rlstates_view_add_dtpath(
+ self, grid: GridLayout, state: RlStateDTPath
+ ) -> None:
+ txt = TextUtil.mk_text(state.node.name, DTShTheme.STYLE_DT_NODE_NAME)
+ if not state.node.enabled:
+ TextUtil.dim(txt)
+ grid.add_row(txt, None)
+
+ def _rlstates_view_add_compatstr(
+ self, grid: GridLayout, state: RlStateCompatStr
+ ) -> None:
+ txt_desc: Optional[Text] = None
+ if state.bindings:
+ # The compatible string associates bindings,
+ # look for description.
+ if len(state.bindings) == 1:
+ # Single binding, use its description if any.
+ binding = state.bindings.pop()
+ if binding.description:
+ txt_desc = TextUtil.mk_headline(
+ binding.description, DTShTheme.STYLE_DT_BINDING_DESC
+ )
+ else:
+ headlines: Set[str] = set()
+ buses: Set[str] = set()
+
+ for binding in state.bindings:
+ if binding.on_bus:
+ buses.add(binding.on_bus)
+ headline = binding.get_headline()
+ if headline:
+ headlines.add(headline)
+
+ if len(headlines) == 1:
+ # All associated bindings have the same description
+ # headline, use it.
+ txt_desc = TextUtil.mk_headline(
+ headlines.pop(), DTShTheme.STYLE_DT_BINDING_DESC
+ )
+ elif buses:
+ # Tell user about different buses of appearance.
+ txt_desc = TextUtil.assemble(
+ TextUtil.italic("Available for different buses: "),
+ TextUtil.mk_text(
+ ", ".join(buses), DTShTheme.STYLE_DT_BUS
+ ),
+ )
+
+ txt_compat = TextUtil.mk_text(
+ state.compatstr, DTShTheme.STYLE_DT_COMPAT_STR
+ )
+
+ grid.add_row(txt_compat, txt_desc)
+
+ def _rlstates_view_add_vendor(
+ self, grid: GridLayout, state: RlStateDTVendor
+ ) -> None:
+ grid.add_row(
+ TextUtil.mk_text(state.prefix, DTShTheme.STYLE_DT_COMPAT_STR),
+ TextUtil.mk_text(state.vendor, DTShTheme.STYLE_DT_VENDOR_NAME),
+ )
+
+ def _rlstates_view_add_bus(
+ self, grid: GridLayout, state: RlStateDTBus
+ ) -> None:
+ grid.add_row(
+ TextUtil.mk_text(state.proto, DTShTheme.STYLE_DT_BUS),
+ None,
+ )
+
+ def _rlstates_view_add_alias(
+ self, grid: GridLayout, state: RlStateDTAlias
+ ) -> None:
+ txt = TextUtil.mk_text(state.alias, DTShTheme.STYLE_DT_ALIAS)
+ if not state.node.enabled:
+ TextUtil.dim(txt)
+ grid.add_row(txt, None)
+
+ def _rlstates_view_add_chosen(
+ self, grid: GridLayout, state: RlStateDTChosen
+ ) -> None:
+ txt = TextUtil.mk_text(state.chosen, DTShTheme.STYLE_DT_CHOSEN)
+ if not state.node.enabled:
+ TextUtil.dim(txt)
+ grid.add_row(txt, None)
+
+ def _rlstates_view_add_label(
+ self, grid: GridLayout, state: RlStateDTLabel
+ ) -> None:
+ txt_label = TextUtil.mk_text(state.label, DTShTheme.STYLE_DT_NODE_LABEL)
+ if not state.node.enabled:
+ TextUtil.dim(txt_label)
+ if state.node.description:
+ txt_desc = TextUtil.mk_headline(
+ state.node.description, DTShTheme.STYLE_DT_BINDING_DESC
+ )
+ if not state.node.enabled:
+ TextUtil.dim(txt_desc)
+ else:
+ txt_desc = None
+ grid.add_row(txt_label, txt_desc)
+
+ def _rlstates_view_add_fspath(
+ self, layout: GridLayout, state: RlStateFsEntry
+ ) -> None:
+ if state.dirent.is_dir():
+ txt = TextUtil.mk_text(
+ f"{state.dirent.name}{os.sep}",
+ style=DTShTheme.STYLE_FS_DIR,
+ )
+ else:
+ txt = TextUtil.mk_text(
+ state.dirent.name,
+ style=DTShTheme.STYLE_FS_FILE,
+ )
+ layout.add_row(txt, None)
+
+ def _rlstates_view_add_enum(
+ self, grid: GridLayout, state: RlStateEnum
+ ) -> None:
+ grid.add_row(TextUtil.bold(state.value), state.brief)
diff --git a/src/dtsh/rich/io.py b/src/dtsh/rich/io.py
new file mode 100644
index 0000000..f58b7af
--- /dev/null
+++ b/src/dtsh/rich/io.py
@@ -0,0 +1,494 @@
+# Copyright (c) 2023 Christophe Dufaza
+#
+# SPDX-License-Identifier: Apache-2.0
+
+"""Rich I/O streams for devicetree shells.
+
+- rich VT
+- rich redirection streams for SVG and HTML
+
+Rich I/O streams implementations are based on the rich.console module.
+"""
+
+
+from typing import Any, IO, List, Mapping, Optional
+
+import os
+
+from rich.console import Console, PagerContext
+from rich.measure import Measurement
+from rich.theme import Theme
+from rich.terminal_theme import (
+ SVG_EXPORT_THEME,
+ DEFAULT_TERMINAL_THEME,
+ MONOKAI,
+ DIMMED_MONOKAI,
+ NIGHT_OWLISH,
+ TerminalTheme,
+)
+
+from dtsh.config import DTShConfig
+from dtsh.io import DTShVT, DTShOutput, DTShRedirect
+
+from dtsh.rich.theme import DTShTheme
+from dtsh.rich.svg import SVGContentsFmt, SVGContents
+
+_dtshconf: DTShConfig = DTShConfig.getinstance()
+_theme: DTShTheme = DTShTheme.getinstance()
+
+
+class DTShRichVT(DTShVT):
+ """Rich terminal for devicetree shells."""
+
+ _console: Console
+ _pager: Optional[PagerContext]
+
+ def __init__(self) -> None:
+ """Initialize VT."""
+ super().__init__()
+ self._console = Console(theme=Theme(_theme.styles), highlight=False)
+ self._pager = None
+
+ def write(self, *args: Any, **kwargs: Any) -> None:
+ """Write to rich console.
+
+ Overrides DTShOutput.write().
+
+ Args:
+ *args: Positional arguments, Console.print() semantic.
+ **kwargs: Keyword arguments, Console.print() semantic.
+
+ """
+ self._console.print(*args, **kwargs)
+
+ def flush(self) -> None:
+ """Flush rich console output without sending LF.
+
+ Overrides DTShOutput.flush().
+ """
+ self._console.print("", end="")
+
+ def clear(self) -> None:
+ """Overrides DTShVT.clear()."""
+ self._console.clear()
+
+ def pager_enter(self) -> None:
+ """Overrides DTShOutput.pager_enter()."""
+ if not self._pager:
+ self._console.clear()
+ self._pager = self._console.pager(styles=True, links=True)
+ # We have to explicitly enter the context since we need
+ # more control on it than the context manager would permit.
+ self._pager.__enter__() # pylint: disable=unnecessary-dunder-call
+
+ def pager_exit(self) -> None:
+ """Overrides DTShOutput.pager_exit()."""
+ if self._pager:
+ self._pager.__exit__(None, None, None)
+ self._pager = None
+
+
+class DTShOutputFileText(DTShOutput):
+ """Text output file for commands output redirection."""
+
+ _out: IO[str]
+ _console: Console
+
+ def __init__(self, path: str, append: bool) -> None:
+ """Initialize output file.
+
+ Args:
+ path: The output file path.
+ append: Whether to redirect the command's output in "append" mode.
+
+ Raises:
+ DTShRedirect.Error: Invalid path or permission errors.
+ """
+ try:
+ # We can't use a context manager here, we just want to open
+ # the file for later subsequent writes.
+ self._out = open( # pylint: disable=consider-using-with
+ path,
+ "a" if append else "w",
+ encoding="utf-8",
+ )
+ if append:
+ # Insert blank line between command outputs.
+ self._out.write(os.linesep)
+ except OSError as e:
+ raise DTShRedirect.Error(e.strerror) from e
+
+ self._console = Console(
+ highlight=False,
+ theme=Theme(_theme.styles),
+ record=True,
+ # Set the console's width to the configured maximum,
+ # we'll strip the rich segments on flush.
+ width=_dtshconf.pref_redir2_maxwidth,
+ )
+
+ def write(self, *args: Any, **kwargs: Any) -> None:
+ """Capture command's output.
+
+ Overrides DTShOutput.write().
+
+ Args:
+ *args: Positional arguments, Console.print() semantic.
+ **kwargs: Keyword arguments, Console.print() semantic.
+ """
+ with self._console.capture():
+ self._console.print(*args, **kwargs)
+
+ def flush(self) -> None:
+ """Format (HTML) the captured output and write it
+ to the redirection file.
+
+ Overrides DTShOutput.flush().
+ """
+ contents = self._console.export_text()
+ # Exported lines are padded up to the (maximum) console width:
+ # strip these trailing whitespaces, which could make the text file
+ # unreadable.
+ for line_nopad in (line.rstrip() for line in contents.splitlines()):
+ print(line_nopad, file=self._out)
+ self._out.close()
+
+
+class DTShOutputFileHtml(DTShOutput):
+ """HTML output file for commands output redirection."""
+
+ _out: IO[str]
+ _append: bool
+ _console: Console
+
+ def __init__(self, path: str, append: bool) -> None:
+ """Initialize output file.
+
+ Args:
+ path: The output file path.
+ append: Whether to redirect the command's output in "append" mode.
+
+ Raises:
+ DTShRedirect.Error: Invalid path or permission errors.
+ """
+ try:
+ self._append = append
+ self._out = open( # pylint: disable=consider-using-with
+ path,
+ "r+" if append else "w",
+ encoding="utf-8",
+ )
+ except OSError as e:
+ raise DTShRedirect.Error(e.strerror) from e
+
+ self._console = Console(
+ highlight=False,
+ theme=Theme(_theme.styles),
+ record=True,
+ # Set the console's width to the configured maximum,
+ # we'll post-process the generated HTML document on flush.
+ width=_dtshconf.pref_redir2_maxwidth,
+ )
+
+ if self._append:
+ # Write a blank line to the captured output
+ # as a commands separator when we append.
+ self.write()
+
+ def write(self, *args: Any, **kwargs: Any) -> None:
+ """Capture command's output.
+
+ Overrides DTShOutput.write().
+
+ Args:
+ *args: Positional arguments, Console.print() semantic.
+ **kwargs: Keyword arguments, Console.print() semantic.
+ """
+ with self._console.capture():
+ self._console.print(*args, **kwargs)
+
+ def flush(self) -> None:
+ """Format (HTML) the captured output and write it
+ to the redirection file.
+
+ Overrides DTShOutput.flush().
+ """
+ # Text and bakcround colors.
+ theme = DTSH_EXPORT_THEMES.get(
+ _dtshconf.pref_html_theme, DEFAULT_TERMINAL_THEME
+ )
+
+ html_fmt = DTSH_HTML_FORMAT.replace(
+ "|font_family|", _dtshconf.pref_html_font_family
+ )
+
+ html = self._console.export_html(
+ theme=theme,
+ code_format=html_fmt,
+ # Use inline CSS styles in "append" mode.
+ inline_styles=self._append,
+ )
+
+ # The generated HTML pad lines with withe spaces
+ # up to the console's width, which is ugly if you
+ # want to re-use the HTML source: clean this up.
+ html_lines: List[str] = [line.rstrip() for line in html.splitlines()]
+
+ # Index of the first line we'll write to the HTML output file:
+ # - either 0, pointing to the first line for the current redirection
+ # contents, if we're creating a new file
+ # - or, in "append" mode, the index of the line containing
+ # the
tag that represents the actual command's output
+ #
+ # Since this
tag appears immediately before the HTML epilog,
+ # we'll then just have to write the current re-direction's contents
+ # starting from this index.
+ i_output: int = 0
+
+ if self._append:
+ # Appending to an existing file: seek to the appropriate
+ # point of insertion.
+ self._seek_last_content()
+ self._out.write(os.linesep)
+
+ # Find command's output contents.
+ for i, line in enumerate(html_lines):
+ if line.find("
None:
+ # Offset for the point of insertion, just before the HTML epilog.
+ offset: int = self._out.tell()
+ line = self._out.readline()
+ while line and not line.startswith("
+
+
{code}
+
+
"):
+ offset = self._out.tell()
+ line = self._out.readline()
+ self._out.seek(offset, os.SEEK_SET)
+
+
+class DTShOutputFileSVG(DTShOutput):
+ """SVG output file for commands output redirection."""
+
+ _out: IO[str]
+ _append: bool
+ _console: Console
+
+ _maxwidth: int
+ _width: int
+
+ def __init__(self, path: str, append: bool) -> None:
+ """Initialize output file.
+
+ Args:
+ path: The output file path.
+ append: Whether to redirect the command's output in "append" mode.
+
+ Raises:
+ DTShRedirect.Error: Invalid path or permission errors.
+ """
+ try:
+ self._append = append
+ self._out = open( # pylint: disable=consider-using-with
+ path,
+ "r+" if append else "w",
+ encoding="utf-8",
+ )
+ except OSError as e:
+ raise DTShRedirect.Error(e.strerror) from e
+
+ # Maximum width allowed in preferences.
+ self._maxwidth = _dtshconf.pref_redir2_maxwidth
+
+ # Width required to output the command's output without wrapping or
+ # cropping, up to the configured maximum.
+ self._width = 0
+
+ self._console = Console(
+ highlight=False,
+ theme=Theme(_theme.styles),
+ record=True,
+ # Set the console's width to the configured maximum,
+ # we'll shrink it on flush.
+ width=self._maxwidth,
+ )
+
+ if self._append:
+ # Write a blank line to the captured output
+ # as a commands separator when we append.
+ self.write()
+
+ def write(self, *args: Any, **kwargs: Any) -> None:
+ """Capture command's output.
+
+ Overrides DTShOutput.write().
+
+ Args:
+ *args: Positional arguments, Console.print() semantic.
+ **kwargs: Keyword arguments, Console.print() semantic.
+ """
+ # Update required width.
+ for arg in args:
+ if (
+ isinstance(arg, str)
+ # Aka RichCast.
+ or hasattr(arg, "__rich__")
+ # Aka ConsoleRenderable.
+ or hasattr(arg, "__rich_console__")
+ ):
+ measure = Measurement.get(
+ self._console, self._console.options, arg
+ )
+ if (measure.maximum > self._width) and not (
+ measure.maximum > self._maxwidth
+ ):
+ self._width = measure.maximum
+
+ with self._console.capture():
+ self._console.print(*args, **kwargs)
+
+ def flush(self) -> None:
+ """Format (SVG) the captured output and write it
+ to the redirection file.
+
+ Overrides DTShOutput.flush().
+ """
+ # Text and bakcround colors.
+ theme = DTSH_EXPORT_THEMES.get(
+ _dtshconf.pref_svg_theme, DEFAULT_TERMINAL_THEME
+ )
+
+ # Shrink the console's width, to prevent the SVG document from being
+ # unnecessarily wide.
+ self._console.width = self._width
+
+ # Get captured command output as SVG contents lines.
+ contents: List[str] = self._console.export_svg(
+ theme=theme,
+ title="",
+ code_format=SVGContentsFmt.get_format(
+ _dtshconf.pref_svg_font_family
+ ),
+ font_aspect_ratio=_dtshconf.pref_svg_font_ratio,
+ ).splitlines()
+
+ svg: SVGContents
+ try:
+ if self._append:
+ svg = self._svg_append(contents)
+ else:
+ svg = self._svg_create(contents)
+
+ except SVGContentsFmt.Error as e:
+ raise DTShRedirect.Error(str(e)) from e
+
+ self._svg_write(svg)
+ self._out.close()
+
+ def _svg_create(self, contents: List[str]) -> SVGContents:
+ svg: SVGContents = SVGContents(contents)
+ svg.top_padding_correction()
+ return svg
+
+ def _svg_append(self, contents: List[str]) -> SVGContents:
+ svgnew: SVGContents = SVGContents(contents)
+ svgnew.top_padding_correction()
+
+ svg: SVGContents = self._svg_read()
+ svg.append(svgnew)
+ return svg
+
+ def _svg_read(self) -> SVGContents:
+ contents: List[str] = self._out.read().splitlines()
+ self._out.seek(0, os.SEEK_SET)
+
+ # Padding correction was already done.
+ svg = SVGContents(contents)
+ return svg
+
+ def _svg_write(self, svg: SVGContents) -> None:
+ self._svg_write_prolog()
+ self._svg_writeln()
+
+ self._svg_write_styles(svg)
+ self._svg_writeln()
+
+ self._svg_write_defs(svg)
+ self._svg_writeln()
+
+ self._svg_write_chrome(svg)
+ self._svg_writeln()
+
+ self._svg_write_gterms(svg)
+ self._svg_write_epilog()
+
+ def _svg_write_prolog(self) -> None:
+ print(SVGContentsFmt.PROLOG, file=self._out)
+
+ def _svg_write_styles(self, svg: SVGContents) -> None:
+ print(SVGContentsFmt.CSS_STYLES_BEGIN, file=self._out)
+ for line in svg.styles:
+ print(line, file=self._out)
+ print(SVGContentsFmt.CSS_STYLES_END, file=self._out)
+
+ def _svg_write_defs(self, svg: SVGContents) -> None:
+ print(SVGContentsFmt.SVG_DEFS_BEGIN, file=self._out)
+ for line in svg.defs:
+ print(line, file=self._out)
+ print(SVGContentsFmt.SVG_DEFS_END, file=self._out)
+
+ def _svg_write_chrome(self, svg: SVGContents) -> None:
+ print(SVGContentsFmt.MARK_CHROME, file=self._out)
+ print(svg.rect, file=self._out)
+
+ def _svg_write_gterms(self, svg: SVGContents) -> None:
+ for gterm in svg.gterms:
+ print(SVGContentsFmt.MARK_GTERM_BEGIN, file=self._out)
+ for line in gterm.contents:
+ print(line, file=self._out)
+ print(SVGContentsFmt.MARK_GTERM_END, file=self._out)
+ self._svg_writeln()
+
+ def _svg_write_epilog(self) -> None:
+ print(SVGContentsFmt.EPILOG, file=self._out)
+
+ def _svg_writeln(self) -> None:
+ print(file=self._out)
+
+
+DTSH_HTML_FORMAT = """\
+
+
+
+