Skip to content

Commit 89cd1a4

Browse files
Handle multi-line tag values (#412)
Handle multi-line tag values Fixes #410. RELEASE NOTES BEGIN specfile can now handle multi-line tag values (enclosed in a macro body, e.g. %shrink). RELEASE NOTES END Reviewed-by: Maja Massarini Reviewed-by: Nikola Forró
2 parents 514fbde + b20d072 commit 89cd1a4

File tree

5 files changed

+111
-37
lines changed

5 files changed

+111
-37
lines changed

specfile/macro_definitions.py

Lines changed: 1 addition & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
from specfile.conditions import process_conditions
1010
from specfile.formatter import formatted
1111
from specfile.types import SupportsIndex
12-
from specfile.utils import UserList
12+
from specfile.utils import UserList, count_brackets
1313

1414
if TYPE_CHECKING:
1515
from specfile.specfile import Specfile
@@ -303,35 +303,6 @@ def pop(lines):
303303
else:
304304
return line
305305

306-
def count_brackets(s):
307-
bc = pc = 0
308-
chars = list(s)
309-
while chars:
310-
c = chars.pop(0)
311-
if c == "\\" and chars:
312-
chars.pop(0)
313-
continue
314-
if c == "%" and chars:
315-
c = chars.pop(0)
316-
if c == "{":
317-
bc += 1
318-
elif c == "(":
319-
pc += 1
320-
continue
321-
if c == "{" and bc > 0:
322-
bc += 1
323-
continue
324-
if c == "}" and bc > 0:
325-
bc -= 1
326-
continue
327-
if c == "(" and pc > 0:
328-
pc += 1
329-
continue
330-
if c == ")" and pc > 0:
331-
pc -= 1
332-
continue
333-
return bc, pc
334-
335306
md_regex = re.compile(
336307
r"""
337308
^

specfile/tags.py

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
from specfile.macros import Macros
2424
from specfile.sections import Section
2525
from specfile.types import SupportsIndex
26-
from specfile.utils import UserList, split_conditional_macro_expansion
26+
from specfile.utils import UserList, count_brackets, split_conditional_macro_expansion
2727

2828
if TYPE_CHECKING:
2929
from specfile.specfile import Specfile
@@ -489,6 +489,13 @@ def parse(cls, section: Section, context: Optional["Specfile"] = None) -> "Tags"
489489
New instance of `Tags` class.
490490
"""
491491

492+
def pop(lines):
493+
line = lines.pop(0)
494+
if isinstance(line, str):
495+
return line, True
496+
else:
497+
return line
498+
492499
def regex_pattern(tag):
493500
name_regex = get_tag_name_regex(tag)
494501
return rf"^(?P<n>{name_regex})(?P<s>\s*:\s*)(?P<v>.+)"
@@ -498,7 +505,8 @@ def regex_pattern(tag):
498505
tag_regexes = [re.compile(regex_pattern(t), re.IGNORECASE) for t in TAG_NAMES]
499506
data = []
500507
buffer: List[str] = []
501-
for line, valid in lines:
508+
while lines:
509+
line, valid = pop(lines)
502510
ws = ""
503511
tokens = re.split(r"([^\S\n]+)$", line, maxsplit=1)
504512
if len(tokens) > 1:
@@ -507,10 +515,21 @@ def regex_pattern(tag):
507515
# find out if there is a match for one of the tag regexes
508516
m = next((m for m in (r.match(line) for r in tag_regexes) if m), None)
509517
if m:
518+
value = m.group("v")
519+
if not suffix:
520+
bc, pc = count_brackets(value)
521+
while (bc > 0 or pc > 0) and lines:
522+
value += ws
523+
line, _ = pop(lines)
524+
tokens = re.split(r"([^\S\n]+)$", line, maxsplit=1)
525+
if len(tokens) > 1:
526+
line, ws, _ = tokens
527+
value += "\n" + line
528+
bc, pc = count_brackets(value)
510529
data.append(
511530
Tag(
512531
m.group("n"),
513-
m.group("v"),
532+
value,
514533
m.group("s"),
515534
Comments.parse(buffer),
516535
valid,
@@ -534,8 +553,10 @@ def get_raw_section_data(self) -> List[str]:
534553
result = []
535554
for tag in self.data:
536555
result.extend(tag.comments.get_raw_data())
537-
result.append(
538-
f"{tag._prefix}{tag.name}{tag._separator}{tag.value}{tag._suffix}"
556+
result.extend(
557+
f"{tag._prefix}{tag.name}{tag._separator}{tag.value}{tag._suffix}".split(
558+
"\n"
559+
)
539560
)
540561
result.extend(self._remainder)
541562
return result

specfile/utils.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,45 @@ def get_filename_from_location(location: str) -> str:
254254
return location[slash + 1 :].split("=")[-1]
255255

256256

257+
def count_brackets(string: str) -> Tuple[int, int]:
258+
"""
259+
Counts non-pair brackets in %{...} and %(...) expressions appearing in the given string.
260+
261+
Args:
262+
string: Input string.
263+
264+
Returns:
265+
The count of non-pair curly braces and the count of non-pair parentheses.
266+
"""
267+
bc = pc = 0
268+
chars = list(string)
269+
while chars:
270+
c = chars.pop(0)
271+
if c == "\\" and chars:
272+
chars.pop(0)
273+
continue
274+
if c == "%" and chars:
275+
c = chars.pop(0)
276+
if c == "{":
277+
bc += 1
278+
elif c == "(":
279+
pc += 1
280+
continue
281+
if c == "{" and bc > 0:
282+
bc += 1
283+
continue
284+
if c == "}" and bc > 0:
285+
bc -= 1
286+
continue
287+
if c == "(" and pc > 0:
288+
pc += 1
289+
continue
290+
if c == ")" and pc > 0:
291+
pc -= 1
292+
continue
293+
return bc, pc
294+
295+
257296
def split_conditional_macro_expansion(value: str) -> Tuple[str, str, str]:
258297
"""
259298
Splits conditional macro expansion into its body and prefix and suffix of it.

tests/unit/test_tags.py

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,11 @@ def test_parse():
4343
"Epoch: 1",
4444
"%endif",
4545
"",
46+
"License: %{shrink:",
47+
" MIT AND",
48+
" (MIT OR Apache-2.0)",
49+
" }",
50+
"",
4651
"Requires: make ",
4752
"Requires(post): bash",
4853
"",
@@ -62,6 +67,13 @@ def test_parse():
6267
assert not tags[1].comments
6368
assert tags.release.comments[0].prefix == " # "
6469
assert tags.epoch.name == "Epoch"
70+
assert tags[-6].name == "License"
71+
assert (
72+
tags[-6].value == "%{shrink:\n"
73+
" MIT AND\n"
74+
" (MIT OR Apache-2.0)\n"
75+
" }"
76+
)
6577
assert tags.requires.value == "make"
6678
assert "requires(post)" in tags
6779
assert tags[-4].name == "Requires(post)"
@@ -102,11 +114,20 @@ def test_get_raw_section_data():
102114
Comments([Comment("this is a valid comment", " # ")]),
103115
),
104116
Tag("Epoch", "1", ": ", Comments([], ["", "%if 0"])),
117+
Tag(
118+
"License",
119+
"%{shrink:\n"
120+
" MIT AND\n"
121+
" (MIT OR Apache-2.0)\n"
122+
" }",
123+
": ",
124+
Comments([], ["%endif", ""]),
125+
),
105126
Tag(
106127
"Requires",
107128
"make",
108129
": ",
109-
Comments([], ["%endif", ""]),
130+
Comments([], [""]),
110131
True,
111132
"",
112133
" ",
@@ -141,6 +162,11 @@ def test_get_raw_section_data():
141162
"Epoch: 1",
142163
"%endif",
143164
"",
165+
"License: %{shrink:",
166+
" MIT AND",
167+
" (MIT OR Apache-2.0)",
168+
" }",
169+
"",
144170
"Requires: make ",
145171
"Requires(post): bash",
146172
"",

tests/unit/test_utils.py

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
import pytest
55

6-
from specfile.utils import EVR, NEVR, NEVRA, get_filename_from_location
6+
from specfile.utils import EVR, NEVR, NEVRA, count_brackets, get_filename_from_location
77

88

99
@pytest.mark.parametrize(
@@ -32,6 +32,23 @@ def test_get_filename_from_location(location, filename):
3232
assert get_filename_from_location(location) == filename
3333

3434

35+
@pytest.mark.parametrize(
36+
"string, count",
37+
[
38+
("", (0, 0)),
39+
("%macro", (0, 0)),
40+
("%{macro}", (0, 0)),
41+
("%{{macro}}", (0, 0)),
42+
("%{{macro}", (1, 0)),
43+
("%{macro:", (1, 0)),
44+
("%(echo %{v}", (0, 1)),
45+
("%(echo %{v} | cut -d. -f3)", (0, 0)),
46+
],
47+
)
48+
def test_count_brackets(string, count):
49+
assert count_brackets(string) == count
50+
51+
3552
def test_EVR_compare():
3653
assert EVR(version="0") == EVR(version="0")
3754
assert EVR(version="0", release="1") != EVR(version="0", release="2")

0 commit comments

Comments
 (0)