1 ## oils_failures_allowed: 0
2
3 #### s ~ regex and s !~ regex
4 shopt -s ysh:upgrade
5
6 var s = 'foo'
7 if (s ~ '.([[:alpha:]]+)') { # ERE syntax
8 echo matches
9 argv.py $[_group(0)] $[_group(1)]
10 }
11 if (s !~ '[[:digit:]]+') {
12 echo "does not match"
13 argv.py $[_group(0)] $[_group(1)]
14 }
15
16 if (s ~ '[[:digit:]]+') {
17 echo "matches"
18 }
19 # Should be cleared now
20 # should this be Undef rather than ''?
21 try {
22 var x = _group(0)
23 }
24 if (_status === 3) {
25 echo 'got expected status 3'
26 }
27
28 try {
29 var y = _group(1)
30 }
31 if (_status === 3) {
32 echo 'got expected status 3'
33 }
34
35 ## STDOUT:
36 matches
37 ['foo', 'oo']
38 does not match
39 ['foo', 'oo']
40 got expected status 3
41 got expected status 3
42 ## END
43
44 #### Invalid regex has libc error message
45
46 shopt -s ysh:upgrade
47
48 # Hm it's hard to test this, we can't get stderr of YSH from within YSH?
49 #redir 2>err.txt {
50 # if ('abc' ~ '+') {
51 # echo 'bad'
52 # }
53 #}
54
55 if ('abc' ~ '+') {
56 echo 'bad'
57 }
58
59 ## status: 2
60 ## STDOUT:
61 ## END
62
63 #### Eggex flags to ignore case are respected
64 shopt -s ysh:upgrade
65
66 # based on Python's spelling
67 var pat = / 'abc' ; i /
68 var pat2 = / @pat 'def' ; reg_icase / # this is allowed
69
70 if ('-abcdef-' ~ pat2) {
71 echo 'yes'
72 }
73
74 if ('-ABCDEF-' ~ pat2) {
75 echo 'yes'
76 }
77
78 if ('ABCDE' ~ pat2) {
79 echo 'BUG'
80 }
81
82 ## STDOUT:
83 yes
84 yes
85 ## END
86
87 #### Eggex flags to treat newlines as special are respected
88 shopt -s ysh:upgrade
89
90 if (u'abc123\n' ~ / digit %end /) {
91 echo 'BUG'
92 }
93 if (u'abc\n123' ~ / %start digit /) {
94 echo 'BUG'
95 }
96
97 if (u'abc123\n' ~ / digit %end ; reg_newline /) {
98 echo 'yes'
99 }
100 if (u'abc\n123' ~ / %start digit ; reg_newline /) {
101 echo 'yes'
102 }
103
104 if (u'\n' ~ / . /) {
105 echo 'yes'
106 }
107 if (u'\n' ~ / !digit /) {
108 echo 'yes'
109 }
110
111 if (u'\n' ~ / . ; reg_newline /) {
112 echo 'BUG'
113 }
114 if (u'\n' ~ / !digit ; reg_newline /) {
115 echo 'BUG'
116 }
117
118 ## STDOUT:
119 yes
120 yes
121 yes
122 yes
123 ## END
124
125 #### leftMatch() combined with REG_NEWLINE - interaction with ^
126 shopt --set ysh:upgrade
127
128 # REG_NEWLINE
129 # . does NOT match newline
130 # [^a] does NOT match newline
131 # ^ matches the empty string after a newline (regardless of REG_NOTBOL)
132 # $ matches the empty string before a newline (regardless of REG_NOTBOL)
133 #
134 # note: REG_NOTBOL is not used in leftMatch()
135
136 var lines = '''
137 one
138 2
139 three
140 '''
141
142 if (0) {
143 = lines
144 = ${p}
145 echo ---
146 }
147
148 proc show-matches(; pattern) {
149 var pos = 0
150 while (true) {
151 var m = lines.leftMatch(pattern, pos=pos)
152 #var m = lines.search(pattern, pos=pos)
153
154 if (m is null) {
155 return
156 }
157
158 #pp test_ ([m.group(0), m.group(1), m.group(2)])
159 pp test_ ([pos, m.group(0)])
160
161 setvar pos = m.end(0)
162 }
163 }
164
165 var p1 = / <capture [a-z] as letter> /
166 var p2 = / <capture [a-z] as letter> ; ; reg_newline /
167
168 show-matches (p1)
169 echo
170 show-matches (p2)
171
172 ## STDOUT:
173 (List) [0,"o"]
174 (List) [1,"n"]
175 (List) [2,"e"]
176
177 (List) [0,"o"]
178 (List) [1,"n"]
179 (List) [2,"e"]
180 ## END
181
182 #### Bug fix: leftMatch() can be used with a|b (ERE precedence is respected)
183 shopt --set ysh:upgrade
184
185 var lines = '''
186 one
187 2
188 three
189 '''
190
191 proc show-matches(; pattern) {
192 var pos = 0
193 while (true) {
194 var m = lines.leftMatch(pattern, pos=pos)
195 #var m = lines.search(pattern, pos=pos)
196
197 if (m is null) {
198 return
199 }
200
201 #pp test_ ([m.group(0), m.group(1), m.group(2)])
202 pp test_ ([pos, m.group(0)])
203
204 setvar pos = m.end(0)
205 }
206 }
207
208 # Alternation requires special logic
209 var p3 = / <capture [0-9] as number> | <capture [a-z] as letter> /
210 var p4 = / <capture [0-9] as number> | <capture [a-z] as letter> ; ; reg_newline /
211
212 var m = lines.leftMatch(p3, pos=3)
213 assert [m is null]
214
215 var m = lines.leftMatch(p3, pos=4)
216 pp test_ ([m.group(0), m.group(1), m.group(2)])
217
218 var m = lines.leftMatch(p3, pos=5)
219 assert [m is null]
220
221 var m = lines.leftMatch(p3, pos=6)
222 pp test_ ([m.group(0), m.group(1), m.group(2)])
223
224 echo
225 show-matches (p3)
226
227 echo
228 show-matches (p4)
229
230 ## STDOUT:
231 (List) ["2","2",null]
232 (List) ["t",null,"t"]
233
234 (List) [0,"o"]
235 (List) [1,"n"]
236 (List) [2,"e"]
237
238 (List) [0,"o"]
239 (List) [1,"n"]
240 (List) [2,"e"]
241 ## END
242
243 #### Positional captures with _group
244 shopt -s ysh:upgrade
245
246 var x = 'zz 2020-08-20'
247
248 if [[ $x =~ ([[:digit:]]+)-([[:digit:]]+) ]] {
249 argv.py "${BASH_REMATCH[@]}"
250 }
251
252 # THIS IS A NO-OP. The variable is SHADOWED by the special name.
253 # I think that's OK.
254 setvar BASH_REMATCH = :| reset |
255
256 if (x ~ /<capture d+> '-' <capture d+>/) {
257 argv.py "${BASH_REMATCH[@]}"
258 argv.py $[_group(0)] $[_group(1)] $[_group(2)]
259
260 # TODO: Also test _start() and _end()
261 }
262 ## STDOUT:
263 ['2020-08', '2020', '08']
264 ['2020-08', '2020', '08']
265 ['2020-08', '2020', '08']
266 ## END
267
268 #### _group() returns null when group doesn't match
269 shopt -s ysh:upgrade
270
271 var pat = / <capture 'a'> | <capture 'b'> /
272 if ('b' ~ pat) {
273 echo "$[_group(1)] $[_group(2)]"
274 }
275 ## STDOUT:
276 null b
277 ## END
278
279 #### _start() and _end()
280 shopt -s ysh:upgrade
281
282 var s = 'foo123bar'
283 if (s ~ /digit+/) {
284 echo start=$[_start(0)] end=$[_end(0)]
285 }
286 echo ---
287
288 if (s ~ / <capture [a-z]+> <capture digit+> /) {
289 echo start=$[_start(1)] end=$[_end(1)]
290 echo start=$[_start(2)] end=$[_end(2)]
291 }
292 echo ---
293
294 if (s ~ / <capture [a-z]+> | <capture digit+> /) {
295 echo start=$[_start(1)] end=$[_end(1)]
296 echo start=$[_start(2)] end=$[_end(2)]
297 }
298
299 ## STDOUT:
300 start=3 end=6
301 ---
302 start=0 end=3
303 start=3 end=6
304 ---
305 start=0 end=3
306 start=-1 end=-1
307 ## END
308
309 #### Str->search() method returns value.Match object
310
311 shopt --set parse_subexpr_shorthand
312
313 var s = '= Hi5- Bye6-'
314
315 var m = s.search(/ <capture [a-z]+ > <capture d+> '-' ; i /)
316 echo "g0 $[m.start(0)] $[m.end(0)] $[m.group(0)]"
317 echo "g1 $[m.start(1)] $[m.end(1)] $[m.group(1)]"
318 echo "g2 $[m.start(2)] $[m.end(2)] $[m.group(2)]"
319
320 echo ---
321
322 var pos = m.end(0) # search from end position
323 var m = s.search(/ <capture [a-z]+ > <capture d+> '-' ; i /, pos=pos)
324 echo "g0 $[m.start(0)] $[m.end(0)] $[m.group(0)]"
325 echo "g1 $[m.start(1)] $[m.end(1)] $[m.group(1)]"
326 echo "g2 $[m.start(2)] $[m.end(2)] $[m.group(2)]"
327
328 ## STDOUT:
329 g0 2 6 Hi5-
330 g1 2 4 Hi
331 g2 4 5 5
332 ---
333 g0 7 12 Bye6-
334 g1 7 10 Bye
335 g2 10 11 6
336 ## END
337
338 #### Str->search() only matches %start ^ when pos == 0
339
340 shopt -s ysh:upgrade
341
342 var anchored = / %start <capture d+> '-' /
343 var free = / <capture d+> '-' /
344
345 var s = '12-34-'
346
347 for pat in ([anchored, free]) {
348 echo "pat=$pat"
349
350 var pos = 0
351 while (true) {
352 var m = s.search(pat, pos=pos)
353 if (not m) {
354 break
355 }
356 echo $[m.group(0)]
357 setvar pos = m.end(0)
358 }
359
360 }
361
362 ## STDOUT:
363 pat=^([[:digit:]]+)-
364 12-
365 pat=([[:digit:]]+)-
366 12-
367 34-
368 ## END
369
370
371 #### search() and leftMatch() accept ERE string
372 shopt --set parse_subexpr_shorthand
373
374 var s = '= hi5- bye6-'
375
376 var m = s.search('([[:alpha:]]+)([[:digit:]]+)-')
377 echo "g0 $[m.start(0)] $[m.end(0)] $[m.group(0)]"
378 echo "g1 $[m.start(1)] $[m.end(1)] $[m.group(1)]"
379 echo "g2 $[m.start(2)] $[m.end(2)] $[m.group(2)]"
380 echo ---
381
382 var m = s[2:].leftMatch('([[:alpha:]]+)([[:digit:]]+)-')
383 echo "g0 $[m.start(0)] $[m.end(0)] $[m.group(0)]"
384 echo "g1 $[m.start(1)] $[m.end(1)] $[m.group(1)]"
385 echo "g2 $[m.start(2)] $[m.end(2)] $[m.group(2)]"
386
387 ## STDOUT:
388 g0 2 6 hi5-
389 g1 2 4 hi
390 g2 4 5 5
391 ---
392 g0 0 4 hi5-
393 g1 0 2 hi
394 g2 2 3 5
395 ## END
396
397 #### Str.leftMatch() can implement lexer pattern
398
399 shopt -s ysh:upgrade
400
401 var lexer = / <capture d+> | <capture [a-z]+> | <capture s+> /
402 #echo $lexer
403
404 proc show-tokens (s) {
405 var pos = 0
406
407 while (true) {
408 echo "pos=$pos"
409
410 var m = s.leftMatch(lexer, pos=pos)
411 if (not m) {
412 break
413 }
414 # TODO: add groups()
415 #var groups = [m.group(1), m.group(2), m.group(3)]
416 echo "$[m.group(1)]/$[m.group(2)]/$[m.group(3)]/"
417
418 echo
419
420 setvar pos = m.end(0)
421 }
422 }
423
424 show-tokens 'ab 12'
425
426 echo '==='
427
428 # There's a token here that doesn't leftMatch()
429 show-tokens 'ab+12'
430
431 ## STDOUT:
432 pos=0
433 null/ab/null/
434
435 pos=2
436 null/null/ /
437
438 pos=3
439 12/null/null/
440
441 pos=5
442 ===
443 pos=0
444 null/ab/null/
445
446 pos=2
447 ## END
448
449 #### Named captures with m.group()
450 shopt -s ysh:all
451
452 var s = 'zz 2020-08-20'
453 var pat = /<capture d+ as year> '-' <capture d+ as month>/
454
455 var m = s.search(pat)
456 argv.py $[m.group('year')] $[m.group('month')]
457 echo $[m.start('year')] $[m.end('year')]
458 echo $[m.start('month')] $[m.end('month')]
459
460 argv.py $[m.group('oops')]
461 echo 'error'
462
463 ## status: 3
464 ## STDOUT:
465 ['2020', '08']
466 3 7
467 8 10
468 ## END
469
470 #### Named captures with _group() _start() _end()
471 shopt -s ysh:all
472
473 var x = 'zz 2020-08-20'
474
475 if (x ~ /<capture d+ as year> '-' <capture d+ as month>/) {
476 argv.py $[_group('year')] $[_group('month')]
477 echo $[_start('year')] $[_end('year')]
478 echo $[_start('month')] $[_end('month')]
479 }
480
481 argv.py $[_group('oops')]
482
483 ## status: 3
484 ## STDOUT:
485 ['2020', '08']
486 3 7
487 8 10
488 ## END
489
490 #### Named Capture Decays Without Name
491 shopt -s ysh:all
492 var pat = /<capture d+ as month>/
493 echo $pat
494
495 if ('123' ~ pat) {
496 echo yes
497 }
498
499 ## STDOUT:
500 ([[:digit:]]+)
501 yes
502 ## END
503
504 #### Nested Named Capture Uses ( ordering
505
506 shopt -s ysh:upgrade
507
508 var Date = /<capture d+ as year> '-' <capture d+ as month>/
509 var Time = /<capture d+ as hour> ':' <capture d+ as minute> (':' <capture d+ as secs>)? /
510
511 var pat = / 'when: ' (<capture Date> | <capture Time as two>) /
512 #echo $pat
513
514 proc show-groups (; m) {
515 echo 0 $[m.group(0)]
516 echo 1 $[m.group(1)] # this is everything except when
517 echo 2 $[m.group(2)]
518 echo
519 echo $[m.group('two')]
520 echo $[m.group('year')] $[m.group('month')]
521 echo $[m.group('hour')] $[m.group('minute')] $[m.group('secs')]
522 }
523
524 var m = 'when: 2023-10'.leftMatch(pat)
525
526 show-groups (m)
527
528 var m = 'when: 23:30'.leftMatch(pat)
529
530 echo ---
531 show-groups (m)
532
533 var m = 'when: 23:30:59'.leftMatch(pat)
534
535 echo ---
536 show-groups (m)
537
538 ## STDOUT:
539 0 when: 2023-10
540 1 2023-10
541 2 2023-10
542
543 null
544 2023 10
545 null null null
546 ---
547 0 when: 23:30
548 1 23:30
549 2 null
550
551 23:30
552 null null
553 23 30 null
554 ---
555 0 when: 23:30:59
556 1 23:30:59
557 2 null
558
559 23:30:59
560 null null
561 23 30 59
562 ## END
563
564 #### Capture with Type Conversion Func
565 shopt -s ysh:upgrade
566
567 var s = 'hi 42-3.14'
568 var pat = / <capture d+: int> '-' <capture d+ '.' d+ : float> /
569
570 if (s ~ pat) {
571 var g1 = _group(1) # Int
572 var g2 = _group(2) # Float
573 echo $[type(g1)] $[type(g2)]
574 }
575
576 var m = s.search(pat)
577 if (m) {
578 echo $[m.group(1) => type()] $[m.group(2) => type()]
579 }
580
581 ## STDOUT:
582 Int Float
583 Int Float
584 ## END
585
586
587 #### Named Capture with Type Conversion Func
588 shopt -s ysh:upgrade
589
590 func floatNegate(x) {
591 return (-float(x))
592 }
593
594 var s = 'hi 42-3.14'
595 var pat = / <capture d+ as left: int> '-' <capture d+ '.' d+ as right: floatNegate> /
596
597 if (s ~ pat) {
598 var g1 = _group('left') # Int
599 var g2 = _group('right') # Float
600 echo $g2
601 echo $[type(g1)] $[type(g2)]
602 }
603
604 var m = s.search(pat)
605 if (m) {
606 echo $[m.group('right')]
607 echo $[m.group('left') => type()] $[m.group('right') => type()]
608 }
609
610 ## STDOUT:
611 -3.14
612 Int Float
613 -3.14
614 Int Float
615 ## END
616
617 #### Can't splice eggex with different flags
618 shopt -s ysh:upgrade
619
620 var pat = / 'abc' ; i /
621 var pat2 = / @pat 'def' ; reg_icase / # this is allowed
622
623 var pat3 = / @pat 'def' /
624 = pat3
625
626 ## status: 1
627 ## STDOUT:
628 ## END
629
630 #### Eggex with translation preference has arbitrary flags
631 shopt -s ysh:upgrade
632
633 # TODO: can provide introspection so users can translate it?
634 # This is kind of a speculative corner of the language.
635
636 var pat = / d+ ; ignorecase ; PCRE /
637
638 # This uses ERE, as a test
639 if ('ab 12' ~ pat) {
640 echo yes
641 }
642
643 ## STDOUT:
644 yes
645 ## END
646
647
648 #### Invalid sh operation on eggex
649 var pat = / d+ /
650 #pat[invalid]=1
651 pat[invalid]+=1
652 ## status: 1
653 ## stdout-json: ""
654
655 #### Long Python Example
656
657 # https://docs.python.org/3/reference/lexical_analysis.html#integer-literals
658
659 # integer ::= decinteger | bininteger | octinteger | hexinteger
660 # decinteger ::= nonzerodigit (["_"] digit)* | "0"+ (["_"] "0")*
661 # bininteger ::= "0" ("b" | "B") (["_"] bindigit)+
662 # octinteger ::= "0" ("o" | "O") (["_"] octdigit)+
663 # hexinteger ::= "0" ("x" | "X") (["_"] hexdigit)+
664 # nonzerodigit ::= "1"..."9"
665 # digit ::= "0"..."9"
666 # bindigit ::= "0" | "1"
667 # octdigit ::= "0"..."7"
668 # hexdigit ::= digit | "a"..."f" | "A"..."F"
669
670 shopt -s ysh:all
671
672 const DecDigit = / [0-9] /
673 const BinDigit = / [0-1] /
674 const OctDigit = / [0-7] /
675 const HexDigit = / [0-9 a-f A-F] / # note: not splicing Digit into character class
676
677 const DecInt = / [1-9] ('_'? DecDigit)* | '0'+ ('_'? '0')* /
678 const BinInt = / '0' [b B] ('_'? BinDigit)+ /
679 const OctInt = / '0' [o O] ('_'? OctDigit)+ /
680 const HexInt = / '0' [x X] ('_'? HexDigit)+ /
681
682 const Integer = / %start (DecInt | BinInt | OctInt | HexInt) %end /
683
684 #echo $Integer
685
686 if ( '123' ~ Integer) { echo 'Y' }
687 if ( 'zzz' !~ Integer) { echo 'N' }
688
689 if ('123_000' ~ Integer) { echo 'Y decimal' }
690 if ('000_123' !~ Integer) { echo 'N decimal' }
691
692 if ( '0b100' ~ Integer) { echo 'Y binary' }
693 if ( '0b102' !~ Integer) { echo 'N binary' }
694
695 if ( '0o755' ~ Integer) { echo 'Y octal' }
696 if ( '0o778' !~ Integer) { echo 'N octal' }
697
698 if ( '0xFF' ~ Integer) { echo 'Y hex' }
699 if ( '0xFG' !~ Integer) { echo 'N hex' }
700
701 ## STDOUT:
702 Y
703 N
704 Y decimal
705 N decimal
706 Y binary
707 N binary
708 Y octal
709 N octal
710 Y hex
711 N hex
712 ## END
713
714 #### Regex in a loop (bug regression)
715
716 shopt --set ysh:all
717
718 var content = [ 1, 2 ]
719 var i = 0
720 while (i < len(content)) {
721 var line = content[i]
722 write $[content[i]]
723 if (str(line) ~ / s* 'imports' s* '=' s* .* /) {
724 exit
725 }
726 setvar i += 1
727 }
728
729 ## STDOUT:
730 1
731 2
732 ## END
733
734
735 #### Regex in a loop depending on var
736
737 shopt --set ysh:all
738
739 var lines = ['foo', 'bar']
740 for line in (lines) {
741 write "line $line"
742
743 # = / $line /
744
745 if ("x$line" ~ / dot @line /) {
746 #if (line ~ / $line /) {
747 write "matched $line"
748 }
749 }
750
751 ## STDOUT:
752 line foo
753 matched foo
754 line bar
755 matched bar
756 ## END
757
758
759 #### Regex with [ (bug regression)
760 shopt --set ysh:all
761
762 if ('[' ~ / '[' /) {
763 echo 'sq'
764 }
765
766 if ('[' ~ / [ '[' ] /) {
767 echo 'char class'
768 }
769
770 # User-reported string
771 if ("a" ~ / s* 'imports' s* '=' s* '[' /) {
772 echo "yes"
773 }
774
775 ## STDOUT:
776 sq
777 char class
778 ## END
779
780 #### Str.replace(Str, Str)
781 shopt --set ysh:all
782
783 var mystr = 'abca'
784 write $[mystr.replace('a', 'A')] # Two matches
785 write $[mystr.replace('b', 'B')] # One match
786 write $[mystr.replace('x', 'y')] # No matches
787
788 write $[mystr.replace('abc', '')] # Empty substitution
789 write $[mystr.replace('', 'new')] # Empty substring
790 ## STDOUT:
791 AbcA
792 aBca
793 abca
794 a
795 newanewbnewcnewanew
796 ## END
797
798 #### Str.replace(Eggex, Str)
799 shopt --set ysh:all
800
801 var mystr = 'mangled----kebab--case'
802 write $[mystr.replace(/ '-'+ /, '-')]
803
804 setvar mystr = 'smaller-to-bigger'
805 write $[mystr.replace(/ '-'+ /, '---')]
806 ## STDOUT:
807 mangled-kebab-case
808 smaller---to---bigger
809 ## END
810
811 #### Str.replace(Eggex, Expr)
812 shopt --set ysh:all
813
814 var mystr = 'name: Bob'
815 write $[mystr.replace(/ 'name: ' <capture dot+> /, ^"Hello $1")]
816 write $[mystr.replace(/ 'name: ' <capture dot+> /, ^"Hello $1 (extracted from '$0')")]
817 ## STDOUT:
818 Hello Bob
819 Hello Bob (extracted from 'name: Bob')
820 ## END
821
822 #### Str.replace(*, Expr), $0
823 shopt --set ysh:all
824
825 # Functionality
826 var mystr = 'class Foo: # this class is called Foo'
827 write $[mystr.replace("Foo", ^"$0Bar")]
828 write $[mystr.replace(/ 'Foo' /, ^"$0Bar")]
829
830 # Edge-cases
831 var dollar0 = "$0"
832 #echo dollar0=$dollar0
833 #echo "0 = $0"
834
835 var expected = "f($dollar0)($dollar0)"
836 #echo "expected = $expected"
837
838 # Eager replacement
839 assert [expected === "foo".replace("o", "($0)")]
840
841 assert ['f(o)(o)' === "foo".replace("o", ^"($0)")]
842
843 func f() { return ( "<$0>" ) }
844 assert ["<$dollar0>" === f()]
845
846 assert ['f<o><o>' === "foo".replace("o", ^[f()])]
847
848 ## STDOUT:
849 class FooBar: # this class is called FooBar
850 class FooBar: # this class is called FooBar
851 ## END
852
853 #### Str.replace(Eggex, Expr), scopes
854 shopt --set ysh:all
855
856 var mystr = '123'
857
858 var anotherVar = 'surprise!'
859 write $[mystr.replace(/ <capture d+> /, ^"Hello $1 ($anotherVar)")]
860
861 var globalName = '456'
862 write $[mystr.replace(/ <capture d+ as globalName> /, ^"Hello $globalName")]
863
864 write $[mystr.replace(/ <capture d+ as localName> /, ^"Hello $localName, $globalName")]
865 ## STDOUT:
866 Hello 123 (surprise!)
867 Hello 123
868 Hello 123, 456
869 ## END
870
871 #### Str.replace(Eggex, *, count)
872 shopt --set ysh:all
873
874 var mystr = '1abc2abc3abc'
875
876 for count in (-2..<4) {
877 write $[mystr.replace('abc', "-", count=count)]
878 write $[mystr.replace('abc', ^"-", count=count)]
879 write $[mystr.replace(/ [a-z]+ /, "-", count=count)]
880 write $[mystr.replace(/ [a-z]+ /, "-", count=count)]
881 }
882 ## STDOUT:
883 1-2-3-
884 1-2-3-
885 1-2-3-
886 1-2-3-
887 1-2-3-
888 1-2-3-
889 1-2-3-
890 1-2-3-
891 1abc2abc3abc
892 1abc2abc3abc
893 1abc2abc3abc
894 1abc2abc3abc
895 1-2abc3abc
896 1-2abc3abc
897 1-2abc3abc
898 1-2abc3abc
899 1-2-3abc
900 1-2-3abc
901 1-2-3abc
902 1-2-3abc
903 1-2-3-
904 1-2-3-
905 1-2-3-
906 1-2-3-
907 ## END
908
909 #### Str.replace(Str, Str), empty new/old strings
910 shopt --set parse_subexpr_shorthand
911
912 var mystr = 'abca'
913 write $[mystr.replace('abc', '')] # Empty substitution
914 write $[mystr.replace('', 'new')] # Empty substring
915 write $[mystr.replace('', 'new', count=1)] # Empty substring, count != -1
916 write $[mystr.replace('', 'new', count=10)] # Empty substring, count too large
917 ## STDOUT:
918 a
919 newanewbnewcnewanew
920 newabca
921 newanewbnewcnewanew
922 ## END
923
924 #### Str.replace(Eggex, Lazy), convert_func
925 shopt --set ysh:all
926
927 var mystr = '123'
928
929 write $[mystr.replace(/ <capture d+ as n : int> /, ^"$[n + 1]")]
930
931 # values automatically get stringified
932 write $[mystr.replace(/ <capture d+ as n : int> /, ^"$1")]
933
934 func not_str(inp) {
935 return ({ "value": inp })
936 }
937
938 # should fail to stringify $1
939 try { call mystr.replace(/ <capture d+ : not_str> /, ^"$1") }
940 write status=$_status
941 ## STDOUT:
942 124
943 123
944 status=3
945 ## END
946
947 #### Str.replace(Eggex, *), eflags
948 shopt --set ysh:all
949
950 var mystr = b'1-2-3\n4-5'
951 write $[mystr.replace(/ d+ /, ^"[$0]")]
952 write $[mystr.replace(/ ^ d+ /, ^"[$0]")]
953 write $[mystr.replace(/ ^ d+ ; reg_newline /, ^"[$0]")]
954 ## STDOUT:
955 [1]-[2]-[3]
956 [4]-[5]
957 [1]-2-3
958 4-5
959 [1]-2-3
960 [4]-5
961 ## END
962
963 #### Str.replace(Eggex, *), guard against infinite loop
964 shopt --set ysh:all
965
966 var mystr = 'foo bar baz'
967 write $[mystr.replace(/ space* /, ' ')]
968 ## status: 3
969 ## STDOUT:
970 ## END
971
972 #### Str.replace(Eggex, *), str cannot contain NUL bytes
973 shopt --set ysh:all
974
975 var mystr = b'foo bar baz\y00'
976 write $[mystr.replace(/ space+ /, ' ')]
977 ## status: 3
978 ## STDOUT:
979 ## END
980
981 #### Str.replace() at top level
982 shopt --set ysh:upgrade
983
984 var s = 'mystr'
985 var pat = / 's' <capture dot> /
986 var template = ^"[$x $0 $1 $x]"
987 pp test_ (template)
988
989 var x = 'x'
990
991 var new = s.replace(pat, template)
992 echo 'replace ' $new
993
994 func myreplace(s, template) {
995 return (s.replace(pat, template))
996 }
997
998 echo myreplace $[myreplace(s, template)]
999
1000 ## STDOUT:
1001 <Expr>
1002 replace my[x st t x]r
1003 myreplace my[x st t x]r
1004 ## END
1005
1006 #### Str.replace() lexical scope with ^""
1007 shopt --set ysh:upgrade
1008
1009 var s = 'mystr'
1010 var pat = / 's' <capture dot> /
1011
1012 proc p {
1013 var x = 'x'
1014 var template = ^"[$x $0 $1 $x]"
1015 pp test_ (template)
1016
1017 var new = s.replace(pat, template)
1018 echo 'replace ' $new
1019
1020 func myreplace(s, template) {
1021 return (s.replace(pat, template))
1022 }
1023
1024 echo myreplace $[myreplace(s, template)]
1025 }
1026
1027 p
1028
1029 ## STDOUT:
1030 <Expr>
1031 replace my[x st t x]r
1032 myreplace my[x st t x]r
1033 ## END
1034
1035 #### Str.replace() lexical scope with ^[]
1036 shopt --set ysh:upgrade
1037
1038 var s = 'mystr'
1039 var pat = / 's' <capture dot> /
1040
1041 proc p {
1042 var x = 'x'
1043 var template = ^['[' ++ x ++ ' ' ++ $0 ++ ' ' ++ $1 ++ ' ' ++ x ++ ']']
1044 pp test_ (template)
1045
1046 var new = s.replace(pat, template)
1047 echo 'replace ' $new
1048
1049 func myreplace(s, template) {
1050 return (s.replace(pat, template))
1051 }
1052
1053 echo myreplace $[myreplace(s, template)]
1054 }
1055
1056 p
1057
1058 ## STDOUT:
1059 <Expr>
1060 replace my[x st t x]r
1061 myreplace my[x st t x]r
1062 ## END