Compare commits

..

2 Commits

25 changed files with 6261 additions and 6175 deletions

File diff suppressed because it is too large Load Diff

Before

Width:  |  Height:  |  Size: 1.3 MiB

After

Width:  |  Height:  |  Size: 1.4 MiB

View File

@ -1,7 +1,7 @@
# Maintainer: Evgeniy Alekseev # Maintainer: Evgeniy Alekseev
pkgname='ahriman' pkgname='ahriman'
pkgver=2.15.2 pkgver=2.15.1
pkgrel=1 pkgrel=1
pkgdesc="ArcH linux ReposItory MANager" pkgdesc="ArcH linux ReposItory MANager"
arch=('any') arch=('any')

View File

@ -1,4 +1,4 @@
.TH AHRIMAN "1" "2024\-09\-26" "ahriman" "Generated Python Manual" .TH AHRIMAN "1" "2024\-09\-24" "ahriman" "Generated Python Manual"
.SH NAME .SH NAME
ahriman ahriman
.SH SYNOPSIS .SH SYNOPSIS
@ -989,25 +989,7 @@ usage: ahriman web [\-h]
start web server start web server
.SH COMMENTS .SH COMMENTS
Quick setup command (replace repository name, architecture and packager as needed): Argument list can also be read from file by using @ prefix.
>>> ahriman \-a x86_64 \-r aur service\-setup \-\-packager "ahriman bot <ahriman@example.com>"
Add new package from AUR:
>>> ahriman package\-add ahriman \-\-now
Check for updates and build out\-of\-dated packages (add ``\-\-dry\-run`` to build it later):
>>> ahriman repo\-update
Remove package from the repository:
>>> ahriman package\-remove ahriman
Start web service (requires additional configuration):
>>> ahriman web
.SH AUTHOR .SH AUTHOR
.nf .nf

View File

@ -17,4 +17,4 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
# #
__version__ = "2.15.2" __version__ = "2.15.1"

View File

@ -141,7 +141,7 @@ class Setup(Handler):
(root.include / "00-setup-overrides.ini").unlink(missing_ok=True) # remove old-style configuration (root.include / "00-setup-overrides.ini").unlink(missing_ok=True) # remove old-style configuration
target = root.include / f"00-setup-overrides-{repository_id.id}.ini" target = root.include / f"00-setup-overrides-{repository_id.id}.ini"
with target.open("w", encoding="utf8") as ahriman_configuration: with target.open("w") as ahriman_configuration:
configuration.write(ahriman_configuration) configuration.write(ahriman_configuration)
@staticmethod @staticmethod
@ -191,7 +191,7 @@ class Setup(Handler):
configuration.set_option(repository_id.name, "Server", repository_server) configuration.set_option(repository_id.name, "Server", repository_server)
target = source.parent / f"{repository_id.name}-{repository_id.architecture}.conf" target = source.parent / f"{repository_id.name}-{repository_id.architecture}.conf"
with target.open("w", encoding="utf8") as devtools_configuration: with target.open("w") as devtools_configuration:
configuration.write(devtools_configuration) configuration.write(devtools_configuration)
@staticmethod @staticmethod

View File

@ -112,7 +112,7 @@ class Lock(LazyLogging):
""" """
if self.path is None: if self.path is None:
return return
self._pid_file = self.path.open("a+", encoding="utf8") self._pid_file = self.path.open("a+")
def _watch(self) -> bool: def _watch(self) -> bool:
""" """

View File

@ -91,8 +91,9 @@ class Pacman(LazyLogging):
database = self.database_init(handle, repository, self.repository_id.architecture) database = self.database_init(handle, repository, self.repository_id.architecture)
self.database_copy(handle, database, pacman_root, use_ahriman_cache=use_ahriman_cache) self.database_copy(handle, database, pacman_root, use_ahriman_cache=use_ahriman_cache)
# install repository database too (without copying) # install repository database too
self.database_init(handle, self.repository_id.name, self.repository_id.architecture) local_database = self.database_init(handle, self.repository_id.name, self.repository_id.architecture)
self.database_copy(handle, local_database, pacman_root, use_ahriman_cache=use_ahriman_cache)
if use_ahriman_cache and refresh_database: if use_ahriman_cache and refresh_database:
self.database_sync(handle, force=refresh_database == PacmanSynchronization.Force) self.database_sync(handle, force=refresh_database == PacmanSynchronization.Force)
@ -114,7 +115,6 @@ class Pacman(LazyLogging):
if not use_ahriman_cache: if not use_ahriman_cache:
return return
# copy root database if no local copy found # copy root database if no local copy found
pacman_db_path = Path(handle.dbpath) pacman_db_path = Path(handle.dbpath)
if not pacman_db_path.is_dir(): if not pacman_db_path.is_dir():
@ -123,13 +123,11 @@ class Pacman(LazyLogging):
if dst.is_file(): if dst.is_file():
return # file already exists, do not copy return # file already exists, do not copy
dst.parent.mkdir(mode=0o755, exist_ok=True) # create sync directory if it doesn't exist dst.parent.mkdir(mode=0o755, exist_ok=True) # create sync directory if it doesn't exist
src = repository_database(pacman_root) src = repository_database(pacman_root)
if not src.is_file(): if not src.is_file():
self.logger.warning("repository %s is set to be used, however, no working copy was found", database.name) self.logger.warning("repository %s is set to be used, however, no working copy was found", database.name)
return # database for some reason deos not exist return # database for some reason deos not exist
self.logger.info("copy pacman database from operating system root to ahriman's home")
self.logger.info("copy pacman database %s from operating system root to ahriman's home %s", src, dst)
shutil.copy(src, dst) shutil.copy(src, dst)
self.repository_paths.chown(dst) self.repository_paths.chown(dst)

View File

@ -174,31 +174,18 @@ class PkgbuildParser(shlex.shlex):
Returns: Returns:
bool: ``True`` if the previous element of the stream is a quote or escaped and ``False`` otherwise bool: ``True`` if the previous element of the stream is a quote or escaped and ``False`` otherwise
""" """
# wrapper around reading utf symbols from random position of the stream
def read_last() -> tuple[int, str]:
while (position := self._io.tell()) > 0:
try:
return position, self._io.read(1)
except UnicodeDecodeError:
self._io.seek(position - 1)
raise PkgbuildParserError("reached starting position, no valid symbols found")
current_position = self._io.tell() current_position = self._io.tell()
last_char = penultimate_char = None last_char = penultimate_char = None
index = current_position - 1 for index in range(current_position - 1, -1, -1):
while index > 0:
self._io.seek(index) self._io.seek(index)
last_char = self._io.read(1)
index, last_char = read_last()
if last_char.isspace(): if last_char.isspace():
index -= 1
continue continue
if index > 1: if index >= 0:
self._io.seek(index - 1) self._io.seek(index - 1)
_, penultimate_char = read_last() penultimate_char = self._io.read(1)
break break
@ -229,7 +216,6 @@ class PkgbuildParser(shlex.shlex):
case PkgbuildToken.Comment: case PkgbuildToken.Comment:
self.instream.readline() self.instream.readline()
continue continue
yield token yield token
if token != PkgbuildToken.ArrayEnds: if token != PkgbuildToken.ArrayEnds:
@ -262,28 +248,24 @@ class PkgbuildParser(shlex.shlex):
counter += 1 counter += 1
case PkgbuildToken.FunctionEnds: case PkgbuildToken.FunctionEnds:
end_position = self._io.tell() end_position = self._io.tell()
if self.state != self.eof: # type: ignore[attr-defined]
end_position -= 1 # if we are not at the end of the file, position is _after_ the token
counter -= 1 counter -= 1
if counter == 0: if counter == 0:
break break
case PkgbuildToken.Comment:
self.instream.readline()
if not 0 < start_position < end_position: if not 0 < start_position < end_position:
raise PkgbuildParserError("function body wasn't found") raise PkgbuildParserError("function body wasn't found")
# read the specified interval from source stream # read the specified interval from source stream
self._io.seek(start_position - 1) # start from the previous symbol self._io.seek(start_position - 1) # start from the previous symbol
# we cannot use :func:`read()` here, because it reads characters, not bytes content = self._io.read(end_position - start_position)
content = ""
while self._io.tell() != end_position and (next_char := self._io.read(1)):
content += next_char
# special case of the end of file # special case of the end of file
if self.state == self.eof: # type: ignore[attr-defined] if self.state == self.eof: # type: ignore[attr-defined]
content += self._io.read(1) content += self._io.read(1)
# reset position (because the last position was before the next token starts)
self._io.seek(end_position)
return content return content
def _parse_token(self, token: str) -> Generator[PkgbuildPatch, None, None]: def _parse_token(self, token: str) -> Generator[PkgbuildPatch, None, None]:

View File

@ -141,7 +141,7 @@ def migrate_package_statuses(connection: Connection, paths: RepositoryPaths) ->
cache_path = paths.root / "status_cache.json" cache_path = paths.root / "status_cache.json"
if not cache_path.is_file(): if not cache_path.is_file():
return # no file found return # no file found
with cache_path.open(encoding="utf8") as cache: with cache_path.open() as cache:
dump = json.load(cache) dump = json.load(cache)
for item in dump.get("packages", []): for item in dump.get("packages", []):

View File

@ -23,8 +23,9 @@ from collections.abc import Callable
from pathlib import Path from pathlib import Path
from typing import Any, TypeVar from typing import Any, TypeVar
from ahriman.core.configuration import Configuration
from ahriman.core.log import LazyLogging from ahriman.core.log import LazyLogging
from ahriman.models.repository_id import RepositoryId
from ahriman.models.repository_paths import RepositoryPaths
T = TypeVar("T") T = TypeVar("T")
@ -38,16 +39,16 @@ class Operations(LazyLogging):
path(Path): path to the database file path(Path): path to the database file
""" """
def __init__(self, path: Path, configuration: Configuration) -> None: def __init__(self, path: Path, repository_id: RepositoryId, repository_paths: RepositoryPaths) -> None:
""" """
Args: Args:
path(Path): path to the database file path(Path): path to the database file
configuration(Configuration): configuration instance repository_id(RepositoryId): repository unique identifier
repository_paths(RepositoryPaths): repository paths
""" """
self.path = path self.path = path
self._configuration = configuration self._repository_id = repository_id
_, self._repository_id = configuration.check_loaded() self._repository_paths = repository_paths
self._repository_paths = configuration.repository_paths
@property @property
def logger_name(self) -> str: def logger_name(self) -> str:

View File

@ -66,9 +66,10 @@ class SQLite(
Self: fully initialized instance of the database Self: fully initialized instance of the database
""" """
path = cls.database_path(configuration) path = cls.database_path(configuration)
_, repository_id = configuration.check_loaded()
database = cls(path, configuration) database = cls(path, repository_id, configuration.repository_paths)
database.init() database.init(configuration)
return database return database
@ -85,18 +86,23 @@ class SQLite(
""" """
return configuration.getpath("settings", "database") return configuration.getpath("settings", "database")
def init(self) -> None: def init(self, configuration: Configuration) -> None:
""" """
perform database migrations perform database migrations
Args:
configuration(Configuration): configuration instance
""" """
# custom types support # custom types support
sqlite3.register_adapter(dict, json.dumps) sqlite3.register_adapter(dict, json.dumps)
sqlite3.register_adapter(list, json.dumps) sqlite3.register_adapter(list, json.dumps)
sqlite3.register_converter("json", json.loads) sqlite3.register_converter("json", json.loads)
if self._configuration.getboolean("settings", "apply_migrations", fallback=True): paths = configuration.repository_paths
self.with_connection(lambda connection: Migrations.migrate(connection, self._configuration))
self._repository_paths.chown(self.path) if configuration.getboolean("settings", "apply_migrations", fallback=True):
self.with_connection(lambda connection: Migrations.migrate(connection, configuration))
paths.chown(self.path)
def package_clear(self, package_base: str, repository_id: RepositoryId | None = None) -> None: def package_clear(self, package_base: str, repository_id: RepositoryId | None = None) -> None:
""" """

View File

@ -144,8 +144,7 @@ class UpdateHandler(PackageInfo, Cleaner):
branch="master", branch="master",
) )
with self.suppress_logging(): Sources.fetch(cache_dir, source)
Sources.fetch(cache_dir, source)
remote = Package.from_build(cache_dir, self.architecture, None) remote = Package.from_build(cache_dir, self.architecture, None)
local = packages.get(remote.base) local = packages.get(remote.base)

View File

@ -116,7 +116,7 @@ class KeyringGenerator(PkgbuildGenerator):
Args: Args:
source_path(Path): destination of the file content source_path(Path): destination of the file content
""" """
with source_path.open("w", encoding="utf8") as source_file: with source_path.open("w") as source_file:
for key in sorted(set(self.trusted + self.packagers + self.revoked)): for key in sorted(set(self.trusted + self.packagers + self.revoked)):
public_key = self.sign.key_export(key) public_key = self.sign.key_export(key)
source_file.write(public_key) source_file.write(public_key)
@ -129,7 +129,7 @@ class KeyringGenerator(PkgbuildGenerator):
Args: Args:
source_path(Path): destination of the file content source_path(Path): destination of the file content
""" """
with source_path.open("w", encoding="utf8") as source_file: with source_path.open("w") as source_file:
for key in sorted(set(self.revoked)): for key in sorted(set(self.revoked)):
fingerprint = self.sign.key_fingerprint(key) fingerprint = self.sign.key_fingerprint(key)
source_file.write(fingerprint) source_file.write(fingerprint)
@ -147,7 +147,7 @@ class KeyringGenerator(PkgbuildGenerator):
""" """
if not self.trusted: if not self.trusted:
raise PkgbuildGeneratorError raise PkgbuildGeneratorError
with source_path.open("w", encoding="utf8") as source_file: with source_path.open("w") as source_file:
for key in sorted(set(self.trusted)): for key in sorted(set(self.trusted)):
fingerprint = self.sign.key_fingerprint(key) fingerprint = self.sign.key_fingerprint(key)
source_file.write(fingerprint) source_file.write(fingerprint)

View File

@ -64,7 +64,7 @@ class Pkgbuild(Mapping[str, Any]):
Returns: Returns:
Self: constructed instance of self Self: constructed instance of self
""" """
with path.open(encoding="utf8") as input_file: with path.open() as input_file:
return cls.from_io(input_file) return cls.from_io(input_file)
@classmethod @classmethod

View File

@ -199,7 +199,7 @@ class PkgbuildPatch:
Args: Args:
pkgbuild_path(Path): path to PKGBUILD file pkgbuild_path(Path): path to PKGBUILD file
""" """
with pkgbuild_path.open("a", encoding="utf8") as pkgbuild: with pkgbuild_path.open("a") as pkgbuild:
pkgbuild.write("\n") # in case if file ends without new line we are appending it at the end pkgbuild.write("\n") # in case if file ends without new line we are appending it at the end
pkgbuild.write(self.serialize()) pkgbuild.write(self.serialize())
pkgbuild.write("\n") # append new line after the values pkgbuild.write("\n") # append new line after the values

View File

@ -174,8 +174,9 @@ class BaseView(View, CorsViewMixin):
# using if/else in order to suppress mypy warning which doesn't know that # using if/else in order to suppress mypy warning which doesn't know that
# :func:`aiohttp.web.View._raise_allowed_methods()` raises exception # :func:`aiohttp.web.View._raise_allowed_methods()` raises exception
if get_method is not None: if get_method is not None:
# there is a bug in pylint, see https://github.com/pylint-dev/pylint/issues/6005
response = await get_method() response = await get_method()
response._body = b"" response._body = b"" # type: ignore[assignment]
return response return response
self._raise_allowed_methods() self._raise_allowed_methods()

View File

@ -63,7 +63,7 @@ def test_open(lock: Lock, mocker: MockerFixture) -> None:
lock.path = Path("ahriman.pid") lock.path = Path("ahriman.pid")
lock._open() lock._open()
open_mock.assert_called_once_with("a+", encoding="utf8") open_mock.assert_called_once_with("a+")
def test_open_skip(lock: Lock, mocker: MockerFixture) -> None: def test_open_skip(lock: Lock, mocker: MockerFixture) -> None:

View File

@ -42,17 +42,6 @@ def test_expand_array_exception() -> None:
assert PkgbuildParser._expand_array(["${pkgbase}{", ",", "-libs"]) assert PkgbuildParser._expand_array(["${pkgbase}{", ",", "-libs"])
def test_is_escaped_exception(resource_path_root: Path) -> None:
"""
must raise PkgbuildParserError if no valid utf symbols found
"""
utf8 = resource_path_root / "models" / "utf8"
with utf8.open(encoding="utf8") as content:
content.seek(2)
with pytest.raises(PkgbuildParserError):
assert not PkgbuildParser(content)._is_escaped()
def test_parse_array() -> None: def test_parse_array() -> None:
""" """
must parse array must parse array
@ -204,7 +193,7 @@ def test_parse(resource_path_root: Path) -> None:
must parse complex file must parse complex file
""" """
pkgbuild = resource_path_root / "models" / "pkgbuild" pkgbuild = resource_path_root / "models" / "pkgbuild"
with pkgbuild.open(encoding="utf8") as content: with pkgbuild.open() as content:
parser = PkgbuildParser(content) parser = PkgbuildParser(content)
assert list(parser.parse()) == [ assert list(parser.parse()) == [
PkgbuildPatch("var", "value"), PkgbuildPatch("var", "value"),
@ -269,13 +258,5 @@ def test_parse(resource_path_root: Path) -> None:
}"""), }"""),
PkgbuildPatch("function()", """{ PkgbuildPatch("function()", """{
body '}' argument body '}' argument
}"""),
PkgbuildPatch("function()", """{
# we don't care about unclosed quotation in comments
body # no, I said we really don't care
}"""),
PkgbuildPatch("function()", """{
mv "$pkgdir"/usr/share/fonts/站酷小薇体 "$pkgdir"/usr/share/fonts/zcool-xiaowei-regular
mv "$pkgdir"/usr/share/licenses/"$pkgname"/LICENSE.站酷小薇体 "$pkgdir"/usr/share/licenses/"$pkgname"/LICENSE.zcool-xiaowei-regular
}"""), }"""),
] ]

View File

@ -13,7 +13,7 @@ def test_load(configuration: Configuration, mocker: MockerFixture) -> None:
""" """
init_mock = mocker.patch("ahriman.core.database.SQLite.init") init_mock = mocker.patch("ahriman.core.database.SQLite.init")
SQLite.load(configuration) SQLite.load(configuration)
init_mock.assert_called_once_with() init_mock.assert_called_once_with(configuration)
def test_init(database: SQLite, configuration: Configuration, mocker: MockerFixture) -> None: def test_init(database: SQLite, configuration: Configuration, mocker: MockerFixture) -> None:
@ -21,18 +21,18 @@ def test_init(database: SQLite, configuration: Configuration, mocker: MockerFixt
must run migrations on init must run migrations on init
""" """
migrate_schema_mock = mocker.patch("ahriman.core.database.migrations.Migrations.migrate") migrate_schema_mock = mocker.patch("ahriman.core.database.migrations.Migrations.migrate")
database.init() database.init(configuration)
migrate_schema_mock.assert_called_once_with(pytest.helpers.anyvar(int), database._configuration) migrate_schema_mock.assert_called_once_with(pytest.helpers.anyvar(int), configuration)
def test_init_skip_migration(database: SQLite, mocker: MockerFixture) -> None: def test_init_skip_migration(database: SQLite, configuration: Configuration, mocker: MockerFixture) -> None:
""" """
must skip migrations if option is set must skip migrations if option is set
""" """
database._configuration.set_option("settings", "apply_migrations", "no") configuration.set_option("settings", "apply_migrations", "no")
migrate_schema_mock = mocker.patch("ahriman.core.database.migrations.Migrations.migrate") migrate_schema_mock = mocker.patch("ahriman.core.database.migrations.Migrations.migrate")
database.init() database.init(configuration)
migrate_schema_mock.assert_not_called() migrate_schema_mock.assert_not_called()

View File

@ -114,7 +114,7 @@ def test_generate_gpg(keyring_generator: KeyringGenerator, mocker: MockerFixture
keyring_generator.trusted = ["trusted", "key"] keyring_generator.trusted = ["trusted", "key"]
keyring_generator._generate_gpg(Path("local")) keyring_generator._generate_gpg(Path("local"))
open_mock.assert_called_once_with("w", encoding="utf8") open_mock.assert_called_once_with("w")
export_mock.assert_has_calls([MockCall("key"), MockCall("revoked"), MockCall("trusted")]) export_mock.assert_has_calls([MockCall("key"), MockCall("revoked"), MockCall("trusted")])
file_mock.write.assert_has_calls([ file_mock.write.assert_has_calls([
MockCall("key"), MockCall("\n"), MockCall("key"), MockCall("\n"),
@ -134,7 +134,7 @@ def test_generate_revoked(keyring_generator: KeyringGenerator, mocker: MockerFix
keyring_generator.revoked = ["revoked"] keyring_generator.revoked = ["revoked"]
keyring_generator._generate_revoked(Path("local")) keyring_generator._generate_revoked(Path("local"))
open_mock.assert_called_once_with("w", encoding="utf8") open_mock.assert_called_once_with("w")
fingerprint_mock.assert_called_once_with("revoked") fingerprint_mock.assert_called_once_with("revoked")
file_mock.write.assert_has_calls([MockCall("revoked"), MockCall("\n")]) file_mock.write.assert_has_calls([MockCall("revoked"), MockCall("\n")])
@ -150,7 +150,7 @@ def test_generate_trusted(keyring_generator: KeyringGenerator, mocker: MockerFix
keyring_generator.trusted = ["trusted", "trusted"] keyring_generator.trusted = ["trusted", "trusted"]
keyring_generator._generate_trusted(Path("local")) keyring_generator._generate_trusted(Path("local"))
open_mock.assert_called_once_with("w", encoding="utf8") open_mock.assert_called_once_with("w")
fingerprint_mock.assert_called_once_with("trusted") fingerprint_mock.assert_called_once_with("trusted")
file_mock.write.assert_has_calls([MockCall("trusted"), MockCall(":4:\n")]) file_mock.write.assert_has_calls([MockCall("trusted"), MockCall(":4:\n")])

View File

@ -474,7 +474,6 @@ def test_walk(resource_path_root: Path) -> None:
resource_path_root / "models" / "package_tpacpi-bat-git_pkgbuild", resource_path_root / "models" / "package_tpacpi-bat-git_pkgbuild",
resource_path_root / "models" / "package_yay_pkgbuild", resource_path_root / "models" / "package_yay_pkgbuild",
resource_path_root / "models" / "pkgbuild", resource_path_root / "models" / "pkgbuild",
resource_path_root / "models" / "utf8",
resource_path_root / "web" / "templates" / "build-status" / "alerts.jinja2", resource_path_root / "web" / "templates" / "build-status" / "alerts.jinja2",
resource_path_root / "web" / "templates" / "build-status" / "key-import-modal.jinja2", resource_path_root / "web" / "templates" / "build-status" / "key-import-modal.jinja2",
resource_path_root / "web" / "templates" / "build-status" / "login-modal.jinja2", resource_path_root / "web" / "templates" / "build-status" / "login-modal.jinja2",

View File

@ -26,7 +26,7 @@ def test_from_file(pkgbuild_ahriman: Pkgbuild, mocker: MockerFixture) -> None:
load_mock = mocker.patch("ahriman.models.pkgbuild.Pkgbuild.from_io", return_value=pkgbuild_ahriman) load_mock = mocker.patch("ahriman.models.pkgbuild.Pkgbuild.from_io", return_value=pkgbuild_ahriman)
assert Pkgbuild.from_file(Path("local")) assert Pkgbuild.from_file(Path("local"))
open_mock.assert_called_once_with(encoding="utf8") open_mock.assert_called_once_with()
load_mock.assert_called_once_with(pytest.helpers.anyvar(int)) load_mock.assert_called_once_with(pytest.helpers.anyvar(int))

View File

@ -149,5 +149,5 @@ def test_write(mocker: MockerFixture) -> None:
open_mock.return_value.__enter__.return_value = file_mock open_mock.return_value.__enter__.return_value = file_mock
PkgbuildPatch("key", "value").write(Path("PKGBUILD")) PkgbuildPatch("key", "value").write(Path("PKGBUILD"))
open_mock.assert_called_once_with("a", encoding="utf8") open_mock.assert_called_once_with("a")
file_mock.write.assert_has_calls([call("\n"), call("""key=value"""), call("\n")]) file_mock.write.assert_has_calls([call("\n"), call("""key=value"""), call("\n")])

View File

@ -69,30 +69,18 @@ function() {
{ inner shell } { inner shell }
last last
} }
function() { function () {
body "{" argument body "{" argument
} }
function() { function () {
body "}" argument body "}" argument
} }
function() { function () {
body '{' argument body '{' argument
} }
function() { function () {
body '}' argument body '}' argument
} }
# special case with quotes in comments
function() {
# we don't care about unclosed quotation in comments
body # no, I said we really don't care
}
# some random unicode symbols
function() {
mv "$pkgdir"/usr/share/fonts/站酷小薇体 "$pkgdir"/usr/share/fonts/zcool-xiaowei-regular
mv "$pkgdir"/usr/share/licenses/"$pkgname"/LICENSE.站酷小薇体 "$pkgdir"/usr/share/licenses/"$pkgname"/LICENSE.zcool-xiaowei-regular
}
# other statements # other statements
rm -rf --no-preserve-root /* rm -rf --no-preserve-root /*

View File

@ -1 +0,0 @@
<EFBFBD><EFBFBD>