mirror of
https://github.com/arcan1s/ahriman.git
synced 2025-04-28 01:07:18 +00:00
expand bash
This commit is contained in:
parent
59af64c303
commit
e553d96d33
@ -28,6 +28,14 @@ ahriman.core.configuration.shell\_interpolator module
|
|||||||
:no-undoc-members:
|
:no-undoc-members:
|
||||||
:show-inheritance:
|
:show-inheritance:
|
||||||
|
|
||||||
|
ahriman.core.configuration.shell\_template module
|
||||||
|
-------------------------------------------------
|
||||||
|
|
||||||
|
.. automodule:: ahriman.core.configuration.shell_template
|
||||||
|
:members:
|
||||||
|
:no-undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
ahriman.core.configuration.validator module
|
ahriman.core.configuration.validator module
|
||||||
-------------------------------------------
|
-------------------------------------------
|
||||||
|
|
||||||
|
@ -270,9 +270,9 @@ class PkgbuildParser(shlex.shlex):
|
|||||||
PkgbuildPatch: extracted a PKGBUILD node
|
PkgbuildPatch: extracted a PKGBUILD node
|
||||||
"""
|
"""
|
||||||
# simple assignment rule
|
# simple assignment rule
|
||||||
if (match := self._STRING_ASSIGNMENT.match(token)) is not None:
|
if m := self._STRING_ASSIGNMENT.match(token):
|
||||||
key = match.group("key")
|
key = m.group("key")
|
||||||
value = match.group("value")
|
value = m.group("value")
|
||||||
yield PkgbuildPatch(key, value)
|
yield PkgbuildPatch(key, value)
|
||||||
return
|
return
|
||||||
|
|
||||||
@ -282,8 +282,8 @@ class PkgbuildParser(shlex.shlex):
|
|||||||
|
|
||||||
match self.get_token():
|
match self.get_token():
|
||||||
# array processing. Arrays will be sent as "key=", "(", values, ")"
|
# array processing. Arrays will be sent as "key=", "(", values, ")"
|
||||||
case PkgbuildToken.ArrayStarts if (match := self._ARRAY_ASSIGNMENT.match(token)) is not None:
|
case PkgbuildToken.ArrayStarts if m := self._ARRAY_ASSIGNMENT.match(token):
|
||||||
key = match.group("key")
|
key = m.group("key")
|
||||||
value = self._parse_array()
|
value = self._parse_array()
|
||||||
yield PkgbuildPatch(key, value)
|
yield PkgbuildPatch(key, value)
|
||||||
|
|
||||||
|
@ -24,16 +24,7 @@ import sys
|
|||||||
from collections.abc import Generator, Mapping, MutableMapping
|
from collections.abc import Generator, Mapping, MutableMapping
|
||||||
from string import Template
|
from string import Template
|
||||||
|
|
||||||
|
from ahriman.core.configuration.shell_template import ShellTemplate
|
||||||
class ExtendedTemplate(Template):
|
|
||||||
"""
|
|
||||||
extension to the default :class:`Template` class, which also enabled braces regex to lookup in sections
|
|
||||||
|
|
||||||
Attributes:
|
|
||||||
braceidpattern(str): regular expression to match a colon inside braces
|
|
||||||
"""
|
|
||||||
|
|
||||||
braceidpattern = r"(?a:[_a-z0-9][_a-z0-9:]*)"
|
|
||||||
|
|
||||||
|
|
||||||
class ShellInterpolator(configparser.Interpolation):
|
class ShellInterpolator(configparser.Interpolation):
|
||||||
@ -60,7 +51,7 @@ class ShellInterpolator(configparser.Interpolation):
|
|||||||
"""
|
"""
|
||||||
def identifiers() -> Generator[tuple[str | None, str], None, None]:
|
def identifiers() -> Generator[tuple[str | None, str], None, None]:
|
||||||
# extract all found identifiers and parse them
|
# extract all found identifiers and parse them
|
||||||
for identifier in ExtendedTemplate(value).get_identifiers():
|
for identifier in ShellTemplate(value).get_identifiers():
|
||||||
match identifier.split(":"):
|
match identifier.split(":"):
|
||||||
case [lookup_option]: # single option from the same section
|
case [lookup_option]: # single option from the same section
|
||||||
yield None, lookup_option
|
yield None, lookup_option
|
||||||
@ -121,7 +112,7 @@ class ShellInterpolator(configparser.Interpolation):
|
|||||||
|
|
||||||
# resolve internal references
|
# resolve internal references
|
||||||
variables = dict(self._extract_variables(parser, value, defaults))
|
variables = dict(self._extract_variables(parser, value, defaults))
|
||||||
internal = ExtendedTemplate(escaped).safe_substitute(variables)
|
internal = ShellTemplate(escaped).safe_substitute(variables)
|
||||||
|
|
||||||
# resolve enriched environment variables by using default Template class
|
# resolve enriched environment variables by using default Template class
|
||||||
environment = Template(internal).safe_substitute(self.environment())
|
environment = Template(internal).safe_substitute(self.environment())
|
||||||
|
158
src/ahriman/core/configuration/shell_template.py
Normal file
158
src/ahriman/core/configuration/shell_template.py
Normal file
@ -0,0 +1,158 @@
|
|||||||
|
#
|
||||||
|
# Copyright (c) 2021-2024 ahriman team.
|
||||||
|
#
|
||||||
|
# This file is part of ahriman
|
||||||
|
# (see https://github.com/arcan1s/ahriman).
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
import fnmatch
|
||||||
|
import re
|
||||||
|
|
||||||
|
from collections.abc import Generator, Mapping
|
||||||
|
from string import Template
|
||||||
|
|
||||||
|
|
||||||
|
class ShellTemplate(Template):
|
||||||
|
"""
|
||||||
|
extension to the default :class:`Template` class, which also adds additional tokens to braced regex and enables
|
||||||
|
bash expansion
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
braceidpattern(str): regular expression to match every character except for closing bracket
|
||||||
|
"""
|
||||||
|
|
||||||
|
braceidpattern = r"(?a:[_a-z0-9][^}]*)"
|
||||||
|
|
||||||
|
_REMOVE_BACK = re.compile(r"^(?P<key>\w+)%(?P<pattern>.+)$")
|
||||||
|
_REMOVE_FRONT = re.compile(r"^(?P<key>\w+)#(?P<pattern>.+)$")
|
||||||
|
_REPLACE = re.compile(r"^(?P<key>\w+)/(?P<pattern>.+)/(?P<replacement>.+)$")
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _remove_back(source: str, pattern: str, *, greedy: bool) -> str:
|
||||||
|
"""
|
||||||
|
resolve "${var%(%)pattern}" constructions
|
||||||
|
|
||||||
|
Args:
|
||||||
|
source(str): source string to match the pattern inside
|
||||||
|
pattern(str): shell expression to match
|
||||||
|
greedy(bool): match as much as possible or not
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: result after removal ``pattern`` from the end of the string
|
||||||
|
"""
|
||||||
|
regex = fnmatch.translate(pattern)
|
||||||
|
compiled = re.compile(regex)
|
||||||
|
|
||||||
|
result = source
|
||||||
|
start_pos = 0
|
||||||
|
|
||||||
|
while m := compiled.search(source, start_pos):
|
||||||
|
result = source[:m.start()]
|
||||||
|
start_pos += m.start() + 1
|
||||||
|
if greedy:
|
||||||
|
break
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _remove_front(source: str, pattern: str, *, greedy: bool) -> str:
|
||||||
|
"""
|
||||||
|
resolve "${var#(#)pattern}" constructions
|
||||||
|
|
||||||
|
Args:
|
||||||
|
source(str): source string to match the pattern inside
|
||||||
|
pattern(str): shell expression to match
|
||||||
|
greedy(bool): match as much as possible or not
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: result after removal ``pattern`` from the start of the string
|
||||||
|
"""
|
||||||
|
regex = fnmatch.translate(pattern)[:-2] # remove \Z at the end of the regex
|
||||||
|
if not greedy:
|
||||||
|
regex = regex.replace("*", "*?")
|
||||||
|
compiled = re.compile(regex)
|
||||||
|
|
||||||
|
m = compiled.match(source)
|
||||||
|
if m is None:
|
||||||
|
return source
|
||||||
|
|
||||||
|
return source[m.end():]
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _replace(source: str, pattern: str, replacement: str, *, greedy: bool) -> str:
|
||||||
|
"""
|
||||||
|
resolve "${var/(/)pattern/replacement}" constructions
|
||||||
|
|
||||||
|
Args:
|
||||||
|
source(str): source string to match the pattern inside
|
||||||
|
pattern(str): shell expression to match
|
||||||
|
replacement(str): new substring
|
||||||
|
greedy(bool): replace as much as possible or not
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: result after replacing ``pattern`` by ``replacement``
|
||||||
|
"""
|
||||||
|
match pattern:
|
||||||
|
case from_back if from_back.startswith("%"):
|
||||||
|
removed = ShellTemplate._remove_back(source, from_back[1:], greedy=False)
|
||||||
|
return removed if removed == source else removed + replacement
|
||||||
|
|
||||||
|
case from_front if from_front.startswith("#"):
|
||||||
|
removed = ShellTemplate._remove_front(source, from_front[1:], greedy=False)
|
||||||
|
return removed if removed == source else replacement + removed
|
||||||
|
|
||||||
|
case regular:
|
||||||
|
regex = fnmatch.translate(regular)[:-2] # remove \Z at the end of the regex
|
||||||
|
compiled = re.compile(regex)
|
||||||
|
return compiled.sub(replacement, source, count=not greedy)
|
||||||
|
|
||||||
|
def shell_substitute(self, mapping: Mapping[str, str], /, **kwargs: str) -> str:
|
||||||
|
"""
|
||||||
|
this method behaves the same as :func:`safe_substitute`, however also expands bash string operations
|
||||||
|
|
||||||
|
Args:
|
||||||
|
mapping(Mapping[str, str]): key-value dictionary of variables
|
||||||
|
**kwargs(str): key-value dictionary of variables passed as kwargs
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: string with replaced values
|
||||||
|
"""
|
||||||
|
substitutions = (
|
||||||
|
(self._REMOVE_BACK, self._remove_back, "%"),
|
||||||
|
(self._REMOVE_FRONT, self._remove_front, "#"),
|
||||||
|
(self._REPLACE, self._replace, "/"),
|
||||||
|
)
|
||||||
|
|
||||||
|
def generator(variables: dict[str, str]) -> Generator[tuple[str, str], None, None]:
|
||||||
|
for identifier in self.get_identifiers():
|
||||||
|
for regex, function, greediness in substitutions:
|
||||||
|
if m := regex.match(identifier):
|
||||||
|
source = variables.get(m.group("key"))
|
||||||
|
if source is None:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# replace pattern with non-greedy
|
||||||
|
pattern = m.group("pattern").removeprefix(greediness)
|
||||||
|
greedy = m.group("pattern").startswith(greediness)
|
||||||
|
# gather all additional args
|
||||||
|
args = {key: value for key, value in m.groupdict().items() if key not in ("key", "pattern")}
|
||||||
|
|
||||||
|
yield identifier, function(source, pattern, **args, greedy=greedy)
|
||||||
|
break
|
||||||
|
|
||||||
|
kwargs.update(mapping)
|
||||||
|
substituted = dict(generator(kwargs))
|
||||||
|
|
||||||
|
return self.safe_substitute(kwargs | substituted)
|
@ -21,9 +21,9 @@ import shlex
|
|||||||
|
|
||||||
from dataclasses import dataclass, fields
|
from dataclasses import dataclass, fields
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from string import Template
|
|
||||||
from typing import Any, Generator, Self
|
from typing import Any, Generator, Self
|
||||||
|
|
||||||
|
from ahriman.core.configuration.shell_template import ShellTemplate
|
||||||
from ahriman.core.utils import dataclass_view, filter_json
|
from ahriman.core.utils import dataclass_view, filter_json
|
||||||
|
|
||||||
|
|
||||||
@ -180,8 +180,8 @@ class PkgbuildPatch:
|
|||||||
This function doesn't support recursive substitution
|
This function doesn't support recursive substitution
|
||||||
"""
|
"""
|
||||||
if isinstance(self.value, str):
|
if isinstance(self.value, str):
|
||||||
return Template(self.value).safe_substitute(variables)
|
return ShellTemplate(self.value).shell_substitute(variables)
|
||||||
return [Template(value).safe_substitute(variables) for value in self.value]
|
return [ShellTemplate(value).shell_substitute(variables) for value in self.value]
|
||||||
|
|
||||||
def view(self) -> dict[str, Any]:
|
def view(self) -> dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import os
|
import os
|
||||||
|
|
||||||
from ahriman.core.configuration import Configuration
|
from ahriman.core.configuration import Configuration
|
||||||
from ahriman.core.configuration.shell_interpolator import ExtendedTemplate, ShellInterpolator
|
from ahriman.core.configuration.shell_interpolator import ShellInterpolator
|
||||||
|
|
||||||
|
|
||||||
def _parser() -> dict[str, dict[str, str]]:
|
def _parser() -> dict[str, dict[str, str]]:
|
||||||
@ -27,14 +27,6 @@ def _parser() -> dict[str, dict[str, str]]:
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def test_extended_template() -> None:
|
|
||||||
"""
|
|
||||||
must match colons in braces
|
|
||||||
"""
|
|
||||||
assert ExtendedTemplate("$key:value").get_identifiers() == ["key"]
|
|
||||||
assert ExtendedTemplate("${key:value}").get_identifiers() == ["key:value"]
|
|
||||||
|
|
||||||
|
|
||||||
def test_extract_variables() -> None:
|
def test_extract_variables() -> None:
|
||||||
"""
|
"""
|
||||||
must extract variables list
|
must extract variables list
|
||||||
|
81
tests/ahriman/core/configuration/test_shell_template.py
Normal file
81
tests/ahriman/core/configuration/test_shell_template.py
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
from ahriman.core.configuration.shell_template import ShellTemplate
|
||||||
|
|
||||||
|
|
||||||
|
def test_shell_template_braceidpattern() -> None:
|
||||||
|
"""
|
||||||
|
must match colons in braces
|
||||||
|
"""
|
||||||
|
assert ShellTemplate("$k:value").get_identifiers() == ["k"]
|
||||||
|
assert ShellTemplate("${k:value}").get_identifiers() == ["k:value"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_remove_back() -> None:
|
||||||
|
"""
|
||||||
|
must remove substring from the back
|
||||||
|
"""
|
||||||
|
assert ShellTemplate("${k%removeme}").shell_substitute({"k": "please removeme"}) == "please "
|
||||||
|
assert ShellTemplate("${k%removeme*}").shell_substitute({"k": "please removeme removeme"}) == "please removeme "
|
||||||
|
assert ShellTemplate("${k%removem?}").shell_substitute({"k": "please removeme removeme"}) == "please removeme "
|
||||||
|
|
||||||
|
assert ShellTemplate("${k%%removeme}").shell_substitute({"k": "please removeme removeme"}) == "please removeme "
|
||||||
|
assert ShellTemplate("${k%%removeme*}").shell_substitute({"k": "please removeme removeme"}) == "please "
|
||||||
|
assert ShellTemplate("${k%%removem?}").shell_substitute({"k": "please removeme removeme"}) == "please removeme "
|
||||||
|
|
||||||
|
assert ShellTemplate("${k%removeme}").shell_substitute({}) == "${k%removeme}"
|
||||||
|
assert ShellTemplate("${k%%removeme}").shell_substitute({}) == "${k%%removeme}"
|
||||||
|
|
||||||
|
assert ShellTemplate("${k%r3m0v3m3}").shell_substitute({"k": "please removeme"}) == "please removeme"
|
||||||
|
assert ShellTemplate("${k%%r3m0v3m3}").shell_substitute({"k": "please removeme"}) == "please removeme"
|
||||||
|
|
||||||
|
|
||||||
|
def test_remove_front() -> None:
|
||||||
|
"""
|
||||||
|
must remove substring from the front
|
||||||
|
"""
|
||||||
|
assert ShellTemplate("${k#removeme}").shell_substitute({"k": "removeme please"}) == " please"
|
||||||
|
assert ShellTemplate("${k#*removeme}").shell_substitute({"k": "removeme removeme please"}) == " removeme please"
|
||||||
|
assert ShellTemplate("${k#removem?}").shell_substitute({"k": "removeme removeme please"}) == " removeme please"
|
||||||
|
|
||||||
|
assert ShellTemplate("${k##removeme}").shell_substitute({"k": "removeme removeme please"}) == " removeme please"
|
||||||
|
assert ShellTemplate("${k##*removeme}").shell_substitute({"k": "removeme removeme please"}) == " please"
|
||||||
|
assert ShellTemplate("${k##removem?}").shell_substitute({"k": "removeme removeme please"}) == " removeme please"
|
||||||
|
|
||||||
|
assert ShellTemplate("${k#removeme}").shell_substitute({}) == "${k#removeme}"
|
||||||
|
assert ShellTemplate("${k##removeme}").shell_substitute({}) == "${k##removeme}"
|
||||||
|
|
||||||
|
assert ShellTemplate("${k#r3m0v3m3}").shell_substitute({"k": "removeme please"}) == "removeme please"
|
||||||
|
assert ShellTemplate("${k##r3m0v3m3}").shell_substitute({"k": "removeme please"}) == "removeme please"
|
||||||
|
|
||||||
|
|
||||||
|
def test_replace() -> None:
|
||||||
|
"""
|
||||||
|
must perform regular replacement
|
||||||
|
"""
|
||||||
|
assert ShellTemplate("${k/in/out}").shell_substitute({"k": "in replace in"}) == "out replace in"
|
||||||
|
assert ShellTemplate("${k/in*/out}").shell_substitute({"k": "in replace in"}) == "out"
|
||||||
|
assert ShellTemplate("${k/*in/out}").shell_substitute({"k": "in replace in replace"}) == "out replace"
|
||||||
|
assert ShellTemplate("${k/i?/out}").shell_substitute({"k": "in replace in"}) == "out replace in"
|
||||||
|
|
||||||
|
assert ShellTemplate("${k//in/out}").shell_substitute({"k": "in replace in"}) == "out replace out"
|
||||||
|
assert ShellTemplate("${k//in*/out}").shell_substitute({"k": "in replace in"}) == "out"
|
||||||
|
assert ShellTemplate("${k//*in/out}").shell_substitute({"k": "in replace in replace"}) == "out replace"
|
||||||
|
assert ShellTemplate("${k//i?/out}").shell_substitute({"k": "in replace in replace"}) == "out replace out replace"
|
||||||
|
|
||||||
|
assert ShellTemplate("${k/in/out}").shell_substitute({}) == "${k/in/out}"
|
||||||
|
assert ShellTemplate("${k//in/out}").shell_substitute({}) == "${k//in/out}"
|
||||||
|
|
||||||
|
|
||||||
|
def test_replace_back() -> None:
|
||||||
|
"""
|
||||||
|
must replace substring from the back
|
||||||
|
"""
|
||||||
|
assert ShellTemplate("${k/%in/out}").shell_substitute({"k": "in replace in"}) == "in replace out"
|
||||||
|
assert ShellTemplate("${k/%in/out}").shell_substitute({"k": "in replace in "}) == "in replace in "
|
||||||
|
|
||||||
|
|
||||||
|
def test_replace_front() -> None:
|
||||||
|
"""
|
||||||
|
must replace substring from the front
|
||||||
|
"""
|
||||||
|
assert ShellTemplate("${k/#in/out}").shell_substitute({"k": "in replace in"}) == "out replace in"
|
||||||
|
assert ShellTemplate("${k/#in/out}").shell_substitute({"k": " in replace in"}) == " in replace in"
|
Loading…
Reference in New Issue
Block a user