xref: /freebsd/contrib/bmake/unit-tests/varmod-match.mk (revision 4757b351ea9d59d71d4a38b82506d2d16fcd560d)
1# $NetBSD: varmod-match.mk,v 1.32 2025/06/29 09:40:13 rillig Exp $
2#
3# Tests for the ':M' modifier, which keeps only those words that match the
4# given pattern.
5#
6# Table of contents
7#
8# 1. Pattern characters '*', '?' and '\'
9# 2. Character lists and character ranges
10# 3. Parsing and escaping
11# 4. Interaction with other modifiers
12# 5. Performance
13# 6. Error handling
14# 7. Historical bugs
15#
16# See also:
17#	char-005c-reverse-solidus.mk
18#	ApplyModifier_Match
19#	ParseModifier_Match
20#	ModifyWord_Match
21#	Str_Match
22
23
24# 1. Pattern characters '*', '?' and '\'
25#
26#	*	matches 0 or more characters
27#	?	matches 1 character
28#	\x	matches the character 'x'
29
30# The pattern is anchored both at the beginning and at the end of the word.
31# Since the pattern 'e' does not contain any pattern matching characters, it
32# matches exactly the word 'e', twice.
33.if ${a c e aa cc ee e f g:L:Me} != "e e"
34.  error
35.endif
36
37# The pattern character '?' matches exactly 1 character, the pattern character
38# '*' matches 0 or more characters.  The whole pattern matches all words that
39# start with 's' and have 3 or more characters.
40.if ${One Two Three Four five six seven so s:L:Ms??*} != "six seven"
41.  error
42.endif
43
44# A pattern without placeholders only matches itself.
45.if ${a aa aaa b ba baa bab:L:Ma} != "a"
46.  error
47.endif
48
49# A pattern that does not start with '*' is anchored at the beginning.
50.if ${a aa aaa b ba baa bab:L:Ma*} != "a aa aaa"
51.  error
52.endif
53
54# A pattern that does not end with '*' is anchored at the end.
55.if ${a aa aaa b ba baa bab:L:M*a} != "a aa aaa ba baa"
56.  error
57.endif
58
59# Test the fast code path for '*' followed by a regular character.
60.if ${:U file.c file.*c file.h file\.c :M*.c} != "file.c file\\.c"
61.  error
62.endif
63# Ensure that the fast code path correctly handles the backslash.
64.if ${:U file.c file.*c file.h file\.c :M*\.c} != "file.c file\\.c"
65.  error
66.endif
67# Ensure that the fast code path correctly handles '\*'.
68.if ${:U file.c file.*c file.h file\.c :M*\*c} != "file.*c"
69.  error
70.endif
71# Ensure that the partial match '.c' doesn't confuse the fast code path.
72.if ${:U file.c.cc file.cc.cc file.cc.c :M*.cc} != "file.c.cc file.cc.cc"
73.  error
74.endif
75# Ensure that the substring '.cc' doesn't confuse the fast code path for '.c'.
76.if ${:U file.c.cc file.cc.cc file.cc.c :M*.c} != "file.cc.c"
77.  error
78.endif
79
80
81# 2. Character lists and character ranges
82#
83#	[...]	matches 1 character from the listed characters
84#	[^...]	matches 1 character from the unlisted characters
85#	[a-z]	matches 1 character from the range 'a' to 'z'
86#	[z-a]	matches 1 character from the range 'a' to 'z'
87
88# Only keep words that start with an uppercase letter.
89.if ${One Two Three Four five six seven:L:M[A-Z]*} != "One Two Three Four"
90.  error
91.endif
92
93# Only keep words that start with a character other than an uppercase letter.
94.if ${One Two Three Four five six seven:L:M[^A-Z]*} != "five six seven"
95.  error
96.endif
97
98#	[]	matches never
99.if ${ ab a[]b a[b a b :L:M[]} != ""
100.  error
101.endif
102
103#	a[]b	matches never
104.if ${ ab a[]b a[b a b [ ] :L:Ma[]b} != ""
105.  error
106.endif
107
108#	[^]	matches exactly 1 arbitrary character
109.if ${ ab a[]b a[b a b [ ] :L:M[^]} != "a b [ ]"
110.  error
111.endif
112
113#	a[^]b	matches 'a', then exactly 1 arbitrary character, then 'b'
114.if ${ ab a[]b a[b a b :L:Ma[^]b} != "a[b"
115.  error
116.endif
117
118#	[Nn0]	matches exactly 1 character from the set 'N', 'n', '0'
119.if ${ a b N n 0 Nn0 [ ] :L:M[Nn0]} != "N n 0"
120.  error
121.endif
122
123#	[a-c]	matches exactly 1 character from the range 'a' to 'c'
124.if ${ A B C a b c d [a-c] [a] :L:M[a-c]} != "a b c"
125.  error
126.endif
127
128#	[c-a]	matches the same as [a-c]
129.if ${ A B C a b c d [a-c] [a] :L:M[c-a]} != "a b c"
130.  error
131.endif
132
133#	[^a-c67]
134#		matches a single character, except for 'a', 'b', 'c', '6' or
135#		'7'
136.if ${ A B C a b c d 5 6 7 8 [a-c] [a] :L:M[^a-c67]} != "A B C d 5 8"
137.  error
138.endif
139
140#	[\]	matches a single backslash; no escaping takes place in
141#		character ranges
142# Without the 'b' in the below words, the backslash would end a word and thus
143# influence how the string is split into words.
144WORDS=		a\b a[\]b ab a\\b
145.if ${WORDS:Ma[\]b} != "a\\b"
146.  error
147.endif
148
149#	[[-]]	May look like it would match a single '[', '\' or ']', but
150#		the inner ']' has two roles: it is the upper bound of the
151#		character range as well as the closing character of the
152#		character list.  The outer ']' is just a regular character.
153WORDS=		[ ] [] \] ]]
154.if ${WORDS:M[[-]]} != "[] \\] ]]"
155.  error
156.endif
157
158#	[b[-]a]
159#		Same as for '[[-]]': the character list stops at the first
160#		']', and the 'a]' is treated as a literal string.
161WORDS=		[a \a ]a []a \]a ]]a [a] \a] ]a] ba]
162.if ${WORDS:M[b[-]a]} != "[a] \\a] ]a] ba]"
163.  error
164.endif
165
166#	[-]	Matches a single '-' since the '-' only becomes part of a
167#		character range if it is preceded and followed by another
168#		character.
169WORDS=		- -]
170.if ${WORDS:M[-]} != "-"
171.  error
172.endif
173
174# Only keep words that don't start with s and at the same time end with
175# either of [ex].
176#
177# This test case ensures that the negation from the first character list
178# '[^s]' does not propagate to the second character list '[ex]'.
179.if ${One Two Three Four five six seven:L:M[^s]*[ex]} != "One Three five"
180.  error
181.endif
182
183
184# 3. Parsing and escaping
185#
186#	*	matches 0 or more characters
187#	?	matches 1 character
188#	\	outside a character list, escapes the following character
189#	[	starts a character list for matching 1 character
190#	]	ends a character list for matching 1 character
191#	-	in a character list, forms a character range
192#	^	at the beginning of a character list, negates the list
193#	(	while parsing the pattern, starts a nesting level
194#	)	while parsing the pattern, ends a nesting level
195#	{	while parsing the pattern, starts a nesting level
196#	}	while parsing the pattern, ends a nesting level
197#	:	while parsing the pattern, terminates the pattern
198#	$	while parsing the pattern, starts a nested expression
199#	#	in a line except a shell command, starts a comment
200
201# The pattern can come from an expression.  For single-letter
202# variables, either the short form or the long form can be used, just as
203# everywhere else.
204PRIMES=	2 3 5 7 11
205n=	2
206.if ${PRIMES:M$n} != "2"
207.  error
208.endif
209.if ${PRIMES:M${n}} != "2"
210.  error
211.endif
212.if ${PRIMES:M${:U2}} != "2"
213.  error
214.endif
215
216#	:	terminates the pattern
217.if ${ A * :L:M:} != ""
218.  error
219.endif
220
221#	\:	matches a colon
222.if ${ ${:U\: \:\:} :L:M\:} != ":"
223.  error
224.endif
225
226#	${:U\:}	matches a colon
227.if ${ ${:U\:} ${:U\:\:} :L:M${:U\:}} != ":"
228.  error
229.endif
230
231# To match a dollar sign in a word, double it.
232#
233# This is different from the :S and :C modifiers, where a '$' has to be
234# escaped as '\$'.
235.if ${:Ua \$ sign:M*$$*} != "\$"
236.  error
237.endif
238
239# In the :M modifier, '\$' does not escape a dollar.  Instead it is
240# interpreted as a backslash followed by whatever expression the
241# '$' starts.
242#
243# This differs from the :S, :C and several other modifiers.
244${:U*}=		asterisk
245.if ${:Ua \$ sign any-asterisk:M*\$*} != "any-asterisk"
246.  error
247.endif
248
249# TODO: ${VAR:M(((}}}}
250# TODO: ${VAR:M{{{)))}
251# TODO: ${VAR:M${UNBALANCED}}
252# TODO: ${VAR:M${:U(((\}\}\}}}
253
254
255# 4. Interaction with other modifiers
256
257# The modifier ':tW' prevents splitting at whitespace.  Even leading and
258# trailing whitespace is preserved.
259.if ${   plain   string   :L:tW:M*} != "   plain   string   "
260.  error
261.endif
262
263# Without the modifier ':tW', the string is split into words.  Whitespace
264# around the words is discarded, and whitespace between the words is
265# normalized to a single space.
266.if ${   plain    string   :L:M*} != "plain string"
267.  error
268.endif
269
270
271# 5. Performance
272
273# Before 2020-06-13, this expression called Str_Match 601,080,390 times.
274# Since 2020-06-13, this expression calls Str_Match 1 time.
275.if ${:U****************:M****************b}
276.endif
277
278# Before 2023-06-22, this expression called Str_Match 2,621,112 times.
279# Adding another '*?' to the pattern called Str_Match 20,630,572 times.
280# Adding another '*?' to the pattern called Str_Match 136,405,672 times.
281# Adding another '*?' to the pattern called Str_Match 773,168,722 times.
282# Adding another '*?' to the pattern called Str_Match 3,815,481,072 times.
283# Since 2023-06-22, Str_Match no longer backtracks.
284.if ${:U..................................................b:M*?*?*?*?*?a}
285.endif
286
287
288# 6. Error handling
289
290#	[	Incomplete empty character list, never matches.
291WORDS=		a a[
292# expect+1: Unfinished character list in pattern "a[" of modifier ":M"
293.if ${WORDS:Ma[} != ""
294.  error
295.endif
296
297#	[^	Incomplete negated empty character list, matches any single
298#		character.
299WORDS=		a a[ aX
300# expect+1: Unfinished character list in pattern "a[^" of modifier ":M"
301.if ${WORDS:Ma[^} != "a[ aX"
302.  error
303.endif
304
305#	[-x1-3	Incomplete character list, matches those elements that can be
306#		parsed without lookahead.
307WORDS=		- + x xx 0 1 2 3 4 [x1-3
308# expect+1: Unfinished character list in pattern "[-x1-3" of modifier ":M"
309.if ${WORDS:M[-x1-3} != "- x 1 2 3"
310.  error
311.endif
312
313#	*[-x1-3	Incomplete character list after a wildcard, matches those
314#		words that end with one of the characters from the list.
315WORDS=		- + x xx 0 1 2 3 4 00 01 10 11 000 001 010 011 100 101 110 111 [x1-3
316# expect+1: Unfinished character list in pattern "*[-x1-3" of modifier ":M"
317.if ${WORDS:M*[-x1-3} != "- x xx 1 2 3 01 11 001 011 101 111 [x1-3"
318.  warning ${WORDS:M*[-x1-3}
319.endif
320
321#	[^-x1-3
322#		Incomplete negated character list, matches any character
323#		except those elements that can be parsed without lookahead.
324WORDS=		- + x xx 0 1 2 3 4 [x1-3
325# expect+1: Unfinished character list in pattern "[^-x1-3" of modifier ":M"
326.if ${WORDS:M[^-x1-3} != "+ 0 4"
327.  error
328.endif
329
330#	[\	Incomplete character list containing a single '\'.
331#
332#		A word can only end with a backslash if the preceding
333#		character is a backslash as well; in all other cases the final
334#		backslash would escape the following space, making the space
335#		part of the word.  Only the very last word of a string can be
336#		'\', as there is no following space that could be escaped.
337WORDS=		\\ \a ${:Ux\\}
338PATTERN=	${:U?[\\}
339# expect+1: Unfinished character list in pattern "?[\" of modifier ":M"
340.if ${WORDS:M${PATTERN}} != "\\\\ x\\"
341.  error
342.endif
343
344#	[x-	Incomplete character list containing an incomplete character
345#		range, matches only the 'x'.
346WORDS=		[x- x x- y
347# expect+1: Unfinished character range in pattern "[x-" of modifier ":M"
348.if ${WORDS:M[x-} != "x"
349.  error
350.endif
351
352#	[^x-	Incomplete negated character list containing an incomplete
353#		character range; matches each word that does not have an 'x'
354#		at the position of the character list.
355#
356#		XXX: Even matches strings that are longer than a single
357#		character.
358WORDS=		[x- x x- y yyyyy
359# expect+1: Unfinished character range in pattern "[^x-" of modifier ":M"
360.if ${WORDS:M[^x-} != "[x- y yyyyy"
361.  error
362.endif
363
364#	[:]	matches never since the ':' starts the next modifier
365# expect+2: Unfinished character list in pattern "[" of modifier ":M"
366# expect+1: Unknown modifier ":]"
367.if ${ ${:U\:} ${:U\:\:} :L:M[:]} != ":"
368.  error
369.else
370.  error
371.endif
372
373
374# 7. Historical bugs
375
376# Before var.c 1.1031 from 2022-08-24, the following expressions caused an
377# out-of-bounds read beyond the indirect ':M' modifiers.
378#
379# The argument to the inner ':U' is unescaped to 'M\'.
380# This 'M\' becomes an indirect modifier ':M' with the pattern '\'.
381# The pattern '\' never matches.
382.if ${:U:${:UM\\}}
383.  error
384.endif
385# The argument to the inner ':U' is unescaped to 'M\:\'.
386# This 'M\:\' becomes an indirect modifier ':M' with the pattern ':\'.
387# The pattern ':\' never matches.
388.if ${:U:${:UM\\\:\\}}
389.  error
390.endif
391