dbrownell-toolsdirectory 0.7.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,78 @@
1
+ Metadata-Version: 2.3
2
+ Name: dbrownell-toolsdirectory
3
+ Version: 0.7.2
4
+ Summary: Creates functionality to manipulate the shell in support of tools defined within a tools directory.
5
+ Author: David Brownell
6
+ Author-email: David Brownell <github@DavidBrownell.com>
7
+ License: MIT
8
+ Classifier: Operating System :: MacOS
9
+ Classifier: Operating System :: Microsoft :: Windows
10
+ Classifier: Operating System :: POSIX :: Linux
11
+ Classifier: Programming Language :: Python
12
+ Classifier: Programming Language :: Python :: 3.14
13
+ Requires-Dist: python-dotenv>=1.2.1
14
+ Requires-Dist: semantic-version>=2.10.0
15
+ Requires-Dist: typer>=0.20.1
16
+ Requires-Python: >=3.14
17
+ Project-URL: Documentation, https://github.com/davidbrownell/dbrownell_ToolsDirectory
18
+ Project-URL: Homepage, https://github.com/davidbrownell/dbrownell_ToolsDirectory
19
+ Project-URL: Repository, https://github.com/davidbrownell/dbrownell_ToolsDirectory
20
+ Description-Content-Type: text/markdown
21
+
22
+ **Project:**
23
+ [![License](https://img.shields.io/github/license/davidbrownell/dbrownell_ToolsDirectory?color=dark-green)](https://github.com/davidbrownell/dbrownell_ToolsDirectory/blob/master/LICENSE)
24
+
25
+ **Package:**
26
+ [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/dbrownell_ToolsDirectory?color=dark-green)](https://pypi.org/project/dbrownell_ToolsDirectory/)
27
+ [![PyPI - Version](https://img.shields.io/pypi/v/dbrownell_ToolsDirectory?color=dark-green)](https://pypi.org/project/dbrownell_ToolsDirectory/)
28
+ [![PyPI - Downloads](https://img.shields.io/pypi/dm/dbrownell_ToolsDirectory)](https://pypistats.org/packages/dbrownell-toolsdirectory)
29
+
30
+ **Development:**
31
+ [![uv](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/uv/main/assets/badge/v0.json)](https://github.com/astral-sh/uv)
32
+ [![CI](https://github.com/davidbrownell/dbrownell_ToolsDirectory/actions/workflows/CICD.yml/badge.svg)](https://github.com/davidbrownell/dbrownell_ToolsDirectory/actions/workflows/CICD.yml)
33
+ [![Code Coverage](https://img.shields.io/endpoint?url=https://gist.githubusercontent.com/davidbrownell/f15146b1b8fdc0a5d45ac0eb786a84f7/raw/dbrownell_ToolsDirectory_code_coverage.json)](https://github.com/davidbrownell/dbrownell_ToolsDirectory/actions)
34
+ [![GitHub commit activity](https://img.shields.io/github/commit-activity/y/davidbrownell/dbrownell_ToolsDirectory?color=dark-green)](https://github.com/davidbrownell/dbrownell_ToolsDirectory/commits/main/)
35
+
36
+ <!-- Content above this delimiter will be copied to the generated README.md file. DO NOT REMOVE THIS COMMENT, as it will cause regeneration to fail. -->
37
+
38
+ ## Contents
39
+ - [Overview](#overview)
40
+ - [Installation](#installation)
41
+ - [Development](#development)
42
+ - [Additional Information](#additional-information)
43
+ - [License](#license)
44
+
45
+ ## Overview
46
+ TODO: Complete this section
47
+
48
+ ### How to use `dbrownell_ToolsDirectory`
49
+ TODO: Complete this section
50
+
51
+ <!-- Content below this delimiter will be copied to the generated README.md file. DO NOT REMOVE THIS COMMENT, as it will cause regeneration to fail. -->
52
+
53
+ ## Installation
54
+
55
+ | Installation Method | Command |
56
+ | --- | --- |
57
+ | Via [uv](https://github.com/astral-sh/uv) | `uv add dbrownell_ToolsDirectory` |
58
+ | Via [pip](https://pip.pypa.io/en/stable/) | `pip install dbrownell_ToolsDirectory` |
59
+
60
+
61
+
62
+ ## Development
63
+ Please visit [Contributing](https://github.com/davidbrownell/dbrownell_ToolsDirectory/blob/main/CONTRIBUTING.md) and [Development](https://github.com/davidbrownell/dbrownell_ToolsDirectory/blob/main/DEVELOPMENT.md) for information on contributing to this project.
64
+
65
+ ## Additional Information
66
+ Additional information can be found at these locations.
67
+
68
+ | Title | Document | Description |
69
+ | --- | --- | --- |
70
+ | Code of Conduct | [CODE_OF_CONDUCT.md](https://github.com/davidbrownell/dbrownell_ToolsDirectory/blob/main/CODE_OF_CONDUCT.md) | Information about the norms, rules, and responsibilities we adhere to when participating in this open source community. |
71
+ | Contributing | [CONTRIBUTING.md](https://github.com/davidbrownell/dbrownell_ToolsDirectory/blob/main/CONTRIBUTING.md) | Information about contributing to this project. |
72
+ | Development | [DEVELOPMENT.md](https://github.com/davidbrownell/dbrownell_ToolsDirectory/blob/main/DEVELOPMENT.md) | Information about development activities involved in making changes to this project. |
73
+ | Governance | [GOVERNANCE.md](https://github.com/davidbrownell/dbrownell_ToolsDirectory/blob/main/GOVERNANCE.md) | Information about how this project is governed. |
74
+ | Maintainers | [MAINTAINERS.md](https://github.com/davidbrownell/dbrownell_ToolsDirectory/blob/main/MAINTAINERS.md) | Information about individuals who maintain this project. |
75
+ | Security | [SECURITY.md](https://github.com/davidbrownell/dbrownell_ToolsDirectory/blob/main/SECURITY.md) | Information about how to privately report security issues associated with this project. |
76
+
77
+ ## License
78
+ `dbrownell_ToolsDirectory` is licensed under the <a href="https://choosealicense.com/licenses/MIT/" target="_blank">MIT</a> license.
@@ -0,0 +1,57 @@
1
+ **Project:**
2
+ [![License](https://img.shields.io/github/license/davidbrownell/dbrownell_ToolsDirectory?color=dark-green)](https://github.com/davidbrownell/dbrownell_ToolsDirectory/blob/master/LICENSE)
3
+
4
+ **Package:**
5
+ [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/dbrownell_ToolsDirectory?color=dark-green)](https://pypi.org/project/dbrownell_ToolsDirectory/)
6
+ [![PyPI - Version](https://img.shields.io/pypi/v/dbrownell_ToolsDirectory?color=dark-green)](https://pypi.org/project/dbrownell_ToolsDirectory/)
7
+ [![PyPI - Downloads](https://img.shields.io/pypi/dm/dbrownell_ToolsDirectory)](https://pypistats.org/packages/dbrownell-toolsdirectory)
8
+
9
+ **Development:**
10
+ [![uv](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/uv/main/assets/badge/v0.json)](https://github.com/astral-sh/uv)
11
+ [![CI](https://github.com/davidbrownell/dbrownell_ToolsDirectory/actions/workflows/CICD.yml/badge.svg)](https://github.com/davidbrownell/dbrownell_ToolsDirectory/actions/workflows/CICD.yml)
12
+ [![Code Coverage](https://img.shields.io/endpoint?url=https://gist.githubusercontent.com/davidbrownell/f15146b1b8fdc0a5d45ac0eb786a84f7/raw/dbrownell_ToolsDirectory_code_coverage.json)](https://github.com/davidbrownell/dbrownell_ToolsDirectory/actions)
13
+ [![GitHub commit activity](https://img.shields.io/github/commit-activity/y/davidbrownell/dbrownell_ToolsDirectory?color=dark-green)](https://github.com/davidbrownell/dbrownell_ToolsDirectory/commits/main/)
14
+
15
+ <!-- Content above this delimiter will be copied to the generated README.md file. DO NOT REMOVE THIS COMMENT, as it will cause regeneration to fail. -->
16
+
17
+ ## Contents
18
+ - [Overview](#overview)
19
+ - [Installation](#installation)
20
+ - [Development](#development)
21
+ - [Additional Information](#additional-information)
22
+ - [License](#license)
23
+
24
+ ## Overview
25
+ TODO: Complete this section
26
+
27
+ ### How to use `dbrownell_ToolsDirectory`
28
+ TODO: Complete this section
29
+
30
+ <!-- Content below this delimiter will be copied to the generated README.md file. DO NOT REMOVE THIS COMMENT, as it will cause regeneration to fail. -->
31
+
32
+ ## Installation
33
+
34
+ | Installation Method | Command |
35
+ | --- | --- |
36
+ | Via [uv](https://github.com/astral-sh/uv) | `uv add dbrownell_ToolsDirectory` |
37
+ | Via [pip](https://pip.pypa.io/en/stable/) | `pip install dbrownell_ToolsDirectory` |
38
+
39
+
40
+
41
+ ## Development
42
+ Please visit [Contributing](https://github.com/davidbrownell/dbrownell_ToolsDirectory/blob/main/CONTRIBUTING.md) and [Development](https://github.com/davidbrownell/dbrownell_ToolsDirectory/blob/main/DEVELOPMENT.md) for information on contributing to this project.
43
+
44
+ ## Additional Information
45
+ Additional information can be found at these locations.
46
+
47
+ | Title | Document | Description |
48
+ | --- | --- | --- |
49
+ | Code of Conduct | [CODE_OF_CONDUCT.md](https://github.com/davidbrownell/dbrownell_ToolsDirectory/blob/main/CODE_OF_CONDUCT.md) | Information about the norms, rules, and responsibilities we adhere to when participating in this open source community. |
50
+ | Contributing | [CONTRIBUTING.md](https://github.com/davidbrownell/dbrownell_ToolsDirectory/blob/main/CONTRIBUTING.md) | Information about contributing to this project. |
51
+ | Development | [DEVELOPMENT.md](https://github.com/davidbrownell/dbrownell_ToolsDirectory/blob/main/DEVELOPMENT.md) | Information about development activities involved in making changes to this project. |
52
+ | Governance | [GOVERNANCE.md](https://github.com/davidbrownell/dbrownell_ToolsDirectory/blob/main/GOVERNANCE.md) | Information about how this project is governed. |
53
+ | Maintainers | [MAINTAINERS.md](https://github.com/davidbrownell/dbrownell_ToolsDirectory/blob/main/MAINTAINERS.md) | Information about individuals who maintain this project. |
54
+ | Security | [SECURITY.md](https://github.com/davidbrownell/dbrownell_ToolsDirectory/blob/main/SECURITY.md) | Information about how to privately report security issues associated with this project. |
55
+
56
+ ## License
57
+ `dbrownell_ToolsDirectory` is licensed under the <a href="https://choosealicense.com/licenses/MIT/" target="_blank">MIT</a> license.
@@ -0,0 +1,100 @@
1
+ [project]
2
+ name = "dbrownell-toolsdirectory"
3
+ version = "0.7.2"
4
+ # ^^^^^
5
+ # Wheel names will be generated according to this value. Do not manually modify this value; instead
6
+ # update it according to committed changes by running this command from the root of the repository:
7
+ #
8
+ # uv run python -m AutoGitSemVer.scripts.UpdatePythonVersion ./pyproject.toml ./src
9
+
10
+ description = "Creates functionality to manipulate the shell in support of tools defined within a tools directory."
11
+ readme = "README.md"
12
+ authors = [
13
+ { name = "David Brownell", email = "github@DavidBrownell.com" }
14
+ ]
15
+ requires-python = ">= 3.14"
16
+ dependencies = [
17
+ "python-dotenv>=1.2.1",
18
+ "semantic-version>=2.10.0",
19
+ "typer>=0.20.1",
20
+ ]
21
+ classifiers = [
22
+ "Operating System :: MacOS",
23
+ "Operating System :: Microsoft :: Windows",
24
+ "Operating System :: POSIX :: Linux",
25
+ "Programming Language :: Python",
26
+ "Programming Language :: Python :: 3.14",
27
+ ]
28
+
29
+ [project.license]
30
+ text = "MIT"
31
+
32
+ [project.urls]
33
+ Homepage = "https://github.com/davidbrownell/dbrownell_ToolsDirectory"
34
+ Documentation = "https://github.com/davidbrownell/dbrownell_ToolsDirectory"
35
+ Repository = "https://github.com/davidbrownell/dbrownell_ToolsDirectory"
36
+
37
+ [build-system]
38
+ requires = ["uv_build>=0.9.18,<0.10.0"]
39
+ build-backend = "uv_build"
40
+
41
+ [dependency-groups]
42
+ dev = [
43
+ "autogitsemver>=0.9.2",
44
+ "dbrownell-commitemojis>=0.2.0",
45
+ "pre-commit>=4.5.1",
46
+ "pyfakefs>=6.0.0",
47
+ "pytest>=9.0.2",
48
+ "pytest-cov>=7.0.0",
49
+ "ruff>=0.14.10",
50
+ "ty>=0.0.8",
51
+ ]
52
+
53
+ [tool.pytest.ini_options]
54
+ addopts = "--verbose -vv --capture=no --cov=dbrownell_ToolsDirectory --cov-report html --cov-report term --cov-report xml:coverage.xml --cov-fail-under=95.0"
55
+ python_files = [
56
+ "**/*Test.py",
57
+ ]
58
+
59
+ [tool.ruff]
60
+ line-length = 110
61
+
62
+ [tool.ruff.lint]
63
+ exclude = ["tests/**"]
64
+
65
+ select = ["ALL"]
66
+
67
+ ignore = [
68
+ "ANN002", # Missing type annotation for `*args`
69
+ "ANN003", # Missing type annotation for `**kwargs`
70
+ "BLE001", # Do not catch blind exception: `Exception`
71
+ "COM812", # Trailing comma missing
72
+ "D104", # Missing docstring in public package
73
+ "D105", # Missing docstring in magic method
74
+ "D107", # Missing docstring in `__init__` method
75
+ "D202", # No blank lines allowed after function docstring
76
+ "E501", # Line too long
77
+ "FIX002", # Line contains TODO, consider resolving the issue
78
+ "I001", # Import block is un-sorted or un-formatted
79
+ "N802", # Function name `xxx` should be lowercase
80
+ "N999", # Invalid module name
81
+ "RSE102", # Unnecessary parentheses on raise exception
82
+ "S101", # Use of assert detected
83
+ "TC006", # Add quotes to type expression in `typing.cast()`
84
+ "TD002", # Missing author in TODO
85
+ "TD003", # Missing issue link for this TODO
86
+ "TRY002", # Create your own exception
87
+ "TRY300", # Consider moving this statement to an `else` block
88
+ "UP032", # Use f-string instead of `format` call
89
+ ]
90
+
91
+ [tool.ruff.lint.mccabe]
92
+ max-complexity = 15
93
+
94
+ [tool.ruff.lint.pylint]
95
+ max-args = 10
96
+ max-branches = 20
97
+ max-returns = 20
98
+
99
+ [tool.uv.build-backend]
100
+ module-name = "dbrownell_ToolsDirectory"
@@ -0,0 +1,127 @@
1
+ # noqa: D100
2
+ import io
3
+ import logging
4
+ import os
5
+ import re
6
+
7
+ from typing import cast, TYPE_CHECKING
8
+
9
+ from dbrownell_Common.ContextlibEx import ExitStack
10
+ from dotenv import dotenv_values
11
+
12
+ from dbrownell_ToolsDirectory.Shell.Commands import Augment, Command, EchoOff, Raw, Set
13
+
14
+ if TYPE_CHECKING:
15
+ from pathlib import Path
16
+
17
+ from dbrownell_Common.Streams.DoneManager import DoneManager
18
+
19
+ from dbrownell_ToolsDirectory.ToolInfo import ToolInfo
20
+
21
+
22
+ # ----------------------------------------------------------------------
23
+ def CreateShellCommands(
24
+ dm: DoneManager,
25
+ tool_infos: list[ToolInfo],
26
+ ) -> list[Command]:
27
+ r"""Create shell commands to set up the environment for the specified tools.
28
+
29
+ For each tool, this function looks for one or more ``.env`` files (as
30
+ determined by :meth:`ToolInfo.GeneratePotentialEnvFiles`) and loads
31
+ environment variables from those files.
32
+
33
+ When reading values from a ``.env`` file, any path segment that begins
34
+ with ``./`` or ``.\`` is treated as relative to the directory that
35
+ contains the ``.env`` file itself. The leading ``./`` or ``.\`` is
36
+ replaced with the absolute path to the ``.env`` file's directory plus
37
+ the appropriate path separator.
38
+
39
+ Parent-directory references such as ``../`` are not resolved or
40
+ rewritten by this logic; they are left in the value as-is. Only
41
+ current-directory references using ``./`` or ``.\`` are expanded.
42
+
43
+ Example:
44
+ * Given an environment file ``/tools/Tool1/Tool1.env`` containing::
45
+ RELATIVE_PATH=./bin
46
+ the value will be expanded to::
47
+ /tools/Tool1/bin
48
+
49
+ """
50
+
51
+ with dm.Nested("Creating shell commands...") as shell_dm:
52
+ relative_path_re = re.compile(r"(?<!\.)(?P<current_dir>\.[/\\])")
53
+
54
+ # ----------------------------------------------------------------------
55
+ def RelativePathReplacement(_: re.Match, env_filename: Path) -> str:
56
+ return str(env_filename.parent) + os.path.sep
57
+
58
+ # ----------------------------------------------------------------------
59
+
60
+ # Update dotenv's logger to suppress output
61
+ logger_sink = io.StringIO()
62
+
63
+ handler = logging.StreamHandler(logger_sink)
64
+
65
+ with ExitStack(handler.close):
66
+ logger = logging.getLogger("dotenv.main")
67
+
68
+ logger.propagate = False
69
+ logger.setLevel(logging.WARNING)
70
+
71
+ logger.handlers.clear()
72
+
73
+ logger.addHandler(handler)
74
+ with ExitStack(lambda: logger.removeHandler(handler)):
75
+ # Create the commands
76
+ commands: list[Command] = [EchoOff()]
77
+
78
+ for tool_info in tool_infos:
79
+ commands.append(Augment("PATH", str(tool_info.binary_directory)))
80
+
81
+ # Search for and apply environment files
82
+ env_config: dict[str, str] = {}
83
+
84
+ for potential_env_file in tool_info.GeneratePotentialEnvFiles():
85
+ if not potential_env_file.is_file():
86
+ continue
87
+
88
+ logger_sink.seek(0)
89
+ logger_sink.truncate(0)
90
+
91
+ # Load the content
92
+ try:
93
+ with potential_env_file.open("r", encoding="utf-8") as f:
94
+ this_env_config = dotenv_values(stream=f)
95
+
96
+ logger_content = logger_sink.getvalue()
97
+ if logger_content:
98
+ raise Exception(logger_content.rstrip()) # noqa: TRY301
99
+
100
+ except Exception as ex:
101
+ shell_dm.WriteError(
102
+ f"Unable to process the environment file '{potential_env_file}': {ex}\n"
103
+ )
104
+ continue
105
+
106
+ # Populate the placeholders
107
+ for key, value in this_env_config.items():
108
+ assert value is not None, key
109
+
110
+ this_env_config[key] = relative_path_re.sub(
111
+ lambda m, potential_env_file=potential_env_file: RelativePathReplacement(
112
+ m,
113
+ potential_env_file,
114
+ ),
115
+ value,
116
+ )
117
+
118
+ # Commit the results
119
+ env_config.update(cast(dict[str, str], this_env_config))
120
+
121
+ if env_config:
122
+ for key, value in env_config.items():
123
+ commands.append(Set(key, value))
124
+
125
+ commands.append(Raw("\n"))
126
+
127
+ return commands
@@ -0,0 +1,74 @@
1
+ @echo off
2
+
3
+ REM Create a temporary file that contains the output produced by dbrownell_ToolsDirectory.
4
+ call :CreateTempScriptName
5
+
6
+ uv run python -m dbrownell_ToolsDirectory "%_DBROWNELL_TOOLS_DIRECTORY_TEMP_SCRIPT_NAME%" batch %*
7
+ set _DBROWNELL_TOOLS_SCRIPT_GENERATION_RETURN_CODE=%ERRORLEVEL%
8
+
9
+ REM Invoke the script
10
+ if exist "%_DBROWNELL_TOOLS_DIRECTORY_TEMP_SCRIPT_NAME%" (
11
+ call "%_DBROWNELL_TOOLS_DIRECTORY_TEMP_SCRIPT_NAME%"
12
+ )
13
+ set _DBROWNELL_TOOLS_SCRIPT_EXECUTION_RETURN_CODE=%ERRORLEVEL%
14
+
15
+ REM Process errors
16
+ if "%_DBROWNELL_TOOLS_SCRIPT_GENERATION_RETURN_CODE%" NEQ "0" (
17
+ @echo.
18
+ @echo ERROR: Errors were encountered and the tool directories have not been activated.
19
+ @echo ERROR:
20
+ @echo ERROR: [dbrownell_ToolsDirectory failed]
21
+ @echo ERROR:
22
+
23
+ goto ErrorExit
24
+ )
25
+
26
+ if "%_DBROWNELL_TOOLS_SCRIPT_EXECUTION_RETURN_CODE%" NEQ "0" (
27
+ @echo.
28
+ @echo ERROR: Errors were encountered and the tool directories have not been activated.
29
+ @echo ERROR:
30
+ @echo ERROR: [%_DBROWNELL_TOOLS_DIRECTORY_TEMP_SCRIPT_NAME% failed]
31
+ @echo ERROR:
32
+ @echo.
33
+
34
+ goto ErrorExit
35
+ )
36
+
37
+ REM Success
38
+ del "%_DBROWNELL_TOOLS_DIRECTORY_TEMP_SCRIPT_NAME%"
39
+
40
+ @echo.
41
+ @echo SUCCESS: The tool directories have been activated.
42
+ @echo.
43
+ @echo.
44
+
45
+ @REM ----------------------------------------------------------------------
46
+ set _DBROWNELL_TOOLS_DIRECTORY_RETURN_CODE=0
47
+ goto Exit
48
+
49
+ @REM ----------------------------------------------------------------------
50
+ :ErrorExit
51
+
52
+ set _DBROWNELL_TOOLS_DIRECTORY_RETURN_CODE=-1
53
+ goto Exit
54
+
55
+ @REM ----------------------------------------------------------------------
56
+ :Exit
57
+ set _DBROWNELL_TOOLS_DIRECTORY_TEMP_SCRIPT_NAME=
58
+ set _DBROWNELL_TOOLS_SCRIPT_GENERATION_RETURN_CODE=
59
+ set _DBROWNELL_TOOLS_SCRIPT_EXECUTION_RETURN_CODE=
60
+
61
+ exit /B %_DBROWNELL_TOOLS_DIRECTORY_RETURN_CODE%
62
+
63
+ @REM ----------------------------------------------------------------------
64
+ @REM |
65
+ @REM | Internal Functions
66
+ @REM |
67
+ @REM ----------------------------------------------------------------------
68
+ :CreateTempScriptName
69
+ setlocal EnableDelayedExpansion
70
+ set _filename=%CD%\ExecuteImpl-!RANDOM!-!Time:~6,5!.cmd
71
+ endlocal & set _DBROWNELL_TOOLS_DIRECTORY_TEMP_SCRIPT_NAME=%_filename%
72
+
73
+ if exist "%_DBROWNELL_TOOLS_DIRECTORY_TEMP_SCRIPT_NAME%" goto CreateTempScriptName
74
+ goto :EOF
@@ -0,0 +1,225 @@
1
+ # noqa: D100
2
+ import textwrap
3
+
4
+ from dbrownell_Common.Types import override
5
+
6
+ from dbrownell_ToolsDirectory.Shell.Commands import (
7
+ Message,
8
+ Call,
9
+ Execute,
10
+ Set,
11
+ Augment,
12
+ Exit,
13
+ ExitOnError,
14
+ EchoOff,
15
+ PersistError,
16
+ PushDirectory,
17
+ PopDirectory,
18
+ Raw,
19
+ )
20
+ from dbrownell_ToolsDirectory.Shell.CommandVisitor import CommandVisitor
21
+
22
+
23
+ # ----------------------------------------------------------------------
24
+ class BashCommandVisitor(CommandVisitor):
25
+ """Command visitor for Linux bash scripts."""
26
+
27
+ # ----------------------------------------------------------------------
28
+ def __init__(self) -> None:
29
+ super().__init__()
30
+
31
+ self._message_substitution_lookup: dict[str, str] = {
32
+ "$": r"\$",
33
+ '"': r"\"",
34
+ "`": r"\\\`",
35
+ }
36
+
37
+ # ----------------------------------------------------------------------
38
+ @override
39
+ def OnMessage( # noqa: D102
40
+ self,
41
+ command: Message,
42
+ ) -> str | None:
43
+ output: list[str] = []
44
+
45
+ for command_line in command.value.split("\n"):
46
+ if not command_line.strip():
47
+ output.append('echo ""')
48
+ continue
49
+
50
+ line = command_line
51
+
52
+ for source, dest in self._message_substitution_lookup.items():
53
+ line = line.replace(source, dest)
54
+
55
+ output.append(f'echo "{line}"')
56
+
57
+ return " && ".join(output) + "\n"
58
+
59
+ # ----------------------------------------------------------------------
60
+ @override
61
+ def OnCall( # noqa: D102
62
+ self,
63
+ command: Call,
64
+ ) -> str | None:
65
+ result = f"source {command.command_line}\n"
66
+ if command.exit_on_error:
67
+ exit_on_error_result = self.Accept(
68
+ ExitOnError(use_return_statement=command.exit_via_return_statement)
69
+ )
70
+
71
+ if exit_on_error_result:
72
+ result += exit_on_error_result
73
+
74
+ return result
75
+
76
+ # ----------------------------------------------------------------------
77
+ @override
78
+ def OnExecute( # noqa: D102
79
+ self,
80
+ command: Execute,
81
+ ) -> str | None:
82
+ result = command.command_line + "\n"
83
+
84
+ if command.exit_on_error:
85
+ exit_on_error_result = self.Accept(
86
+ ExitOnError(use_return_statement=command.exit_via_return_statement)
87
+ )
88
+
89
+ if exit_on_error_result:
90
+ result += exit_on_error_result
91
+
92
+ return result
93
+
94
+ # ----------------------------------------------------------------------
95
+ @override
96
+ def OnSet( # noqa: D102
97
+ self,
98
+ command: Set,
99
+ ) -> str | None:
100
+ if command.value_or_values is None:
101
+ return f"unset {command.name}\n"
102
+
103
+ values = ":".join(command.EnumValues())
104
+
105
+ values = values.removeprefix('"')
106
+ values = values.removesuffix('"')
107
+
108
+ return f'export {command.name}="{values}"\n'
109
+
110
+ # ----------------------------------------------------------------------
111
+ @override
112
+ def OnAugment( # noqa: D102
113
+ self,
114
+ command: Augment,
115
+ ) -> str | None:
116
+ if command.append_values:
117
+ add_statement_template = f"{{value}}:${{{{{command.name}}}}}"
118
+ else:
119
+ add_statement_template = f"${{{{{command.name}}}}}:{{value}}"
120
+
121
+ add_statement_template = f'export {command.name}="{add_statement_template}"'
122
+
123
+ statement_template = (
124
+ f'[[ ":${{{{{command.name}}}}}:" != *":{{value}}:"* ]] && ' + add_statement_template
125
+ )
126
+
127
+ statements: list[str] = [statement_template.format(value=value) for value in command.EnumValues()]
128
+
129
+ return "\n".join(statements) + "\n"
130
+
131
+ # ----------------------------------------------------------------------
132
+ @override
133
+ def OnExit( # noqa: D102
134
+ self,
135
+ command: Exit,
136
+ ) -> str | None:
137
+ return textwrap.dedent(
138
+ """\
139
+ {success}
140
+ {error}
141
+ return {return_code}
142
+ """,
143
+ ).format(
144
+ success=textwrap.dedent(
145
+ """\
146
+ if [[ $? -eq 0 ]]; then
147
+ read -p "Press [Enter] to continue"
148
+ fi
149
+ """,
150
+ ).rstrip()
151
+ if command.pause_on_success
152
+ else "",
153
+ error=textwrap.dedent(
154
+ """\
155
+ if [[ $? -ne 0 ]]; then
156
+ read -p "Press [Enter] to continue"
157
+ fi
158
+ """,
159
+ ).rstrip()
160
+ if command.pause_on_error
161
+ else "",
162
+ return_code=command.return_code or 0,
163
+ )
164
+
165
+ # ----------------------------------------------------------------------
166
+ @override
167
+ def OnExitOnError( # noqa: D102
168
+ self,
169
+ command: ExitOnError,
170
+ ) -> str | None:
171
+ variable_name = f"${command.variable_name}" if command.variable_name else "$?"
172
+
173
+ return textwrap.dedent(
174
+ f"""\
175
+ error_code={variable_name}
176
+ if [[ $error_code -ne 0 ]]; then
177
+ {"return" if command.use_return_statement else "exit"} {command.return_code or "$error_code"}
178
+ fi
179
+ """,
180
+ )
181
+
182
+ # ----------------------------------------------------------------------
183
+ @override
184
+ def OnEchoOff( # noqa: D102
185
+ self,
186
+ command: EchoOff, # noqa: ARG002
187
+ ) -> str | None:
188
+ return "set +x\n\n"
189
+
190
+ # ----------------------------------------------------------------------
191
+ @override
192
+ def OnPersistError( # noqa: D102
193
+ self,
194
+ command: PersistError,
195
+ ) -> str | None:
196
+ return f"{command.variable_name}=$?\n"
197
+
198
+ # ----------------------------------------------------------------------
199
+ @override
200
+ def OnPushDirectory( # noqa: D102
201
+ self,
202
+ command: PushDirectory,
203
+ ) -> str | None:
204
+ if command.value is None:
205
+ directory = """$( cd "$( dirname "${BASH_SOURCE[0]}" )" > /dev/null 2>&1 && pwd )"""
206
+ else:
207
+ directory = f'"{command.value.as_posix()}"'
208
+
209
+ return f"pushd {directory} > /dev/null\n"
210
+
211
+ # ----------------------------------------------------------------------
212
+ @override
213
+ def OnPopDirectory( # noqa: D102
214
+ self,
215
+ command: PopDirectory, # noqa: ARG002
216
+ ) -> str | None:
217
+ return "popd > /dev/null\n"
218
+
219
+ # ----------------------------------------------------------------------
220
+ @override
221
+ def OnRaw( # noqa: D102
222
+ self,
223
+ command: Raw,
224
+ ) -> str | None:
225
+ return command.value.removesuffix("\n") + "\n"