Skip to content

Commit b941012

Browse files
authored
Substitute empty match in ${var/foo/bar} (#244)
2 parents 9d6655b + 2759232 commit b941012

6 files changed

Lines changed: 55 additions & 9 deletions

File tree

NEWS

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22

33
## Yash 2.62 (Unreleased)
44

5+
- In the `${parameter/pattern/replacement}` parameter expansion, an
6+
empty pattern is now matched and replaced like any other match.
7+
This allows `${var/#/prefix}` and `${var/%/suffix}` to prefix and
8+
suffix the value, respectively.
59
- [line-editing] Updated the completion script for tar to support GNU
610
tar 1.35 and libarchive (bsdtar) 3.8.7.
711
- Updated the sample initialization script (yashrc):

NEWS.ja

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22

33
## Yash 2.62 (未リリース)
44

5+
- パラメータ展開 `${parameter/pattern/replacement}` で、空のパターンも
6+
他のマッチと同様にマッチ・置換されるようになった。これにより
7+
`${var/#/prefix}` および `${var/%/suffix}` で値の先頭・末尾に
8+
文字列を付け加えられる
59
- [行編集] tar の補完を GNU tar 1.35 および libarchive (bsdtar 3.8.7)
610
に合わせて更新
711
- 初期化スクリプト (yashrc) のサンプルを更新:

doc/expand.txt

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -336,23 +336,32 @@ If {{word1}} matches more than one part of the value, only the first part is
336336
replaced.
337337
The shortest matching is replaced if more than one matching is possible for
338338
the same starting point in the value.
339+
An empty {{word1}} matches the empty string at the beginning of the value, so
340+
+${{{parameter}}/""/{{word2}}}+ prefixes {{word2}} to the value.
339341
+
340342
This modifier cannot be used in the link:posix.html[POSIXly-correct mode].
341343

342344
+/#{{word1}}/{{word2}}+::
343345
This is similar to +/{{word1}}/{{word2}}+ above.
344346
The only difference is that {{word1}} matches only at the beginning of the
345347
value being expanded.
348+
An empty {{word1}} matches the empty string at the beginning of the value, so
349+
+${{{parameter}}/#/{{word2}}}+ prefixes {{word2}} to the value.
346350

347351
+/%{{word1}}/{{word2}}+::
348352
This is similar to +/{{word1}}/{{word2}}+ above.
349353
The only difference is that {{word1}} matches only at the end of the value
350354
being expanded.
355+
An empty {{word1}} matches the empty string at the end of the value, so
356+
+${{{parameter}}/%/{{word2}}}+ suffixes {{word2}} to the value.
351357

352358
+//{{word1}}/{{word2}}+::
353359
This is similar to +/{{word1}}/{{word2}}+ above.
354360
The only difference is that all matched parts are replaced if {{word1}}
355361
matches more than one part of the value.
362+
An empty {{word1}} matches the empty string at the beginning of the value,
363+
between each two adjacent characters, and at the end of the value, so all
364+
those empty matches are replaced with {{word2}}.
356365

357366
+:/{{word1}}/{{word2}}+::
358367
This is similar to +/{{word1}}/{{word2}}+ above.

doc/ja/expand.txt

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -195,18 +195,18 @@ echo "${array[2,-2]}"
195195
この加工指定は +%{{単語}}+ と同様ですが、マッチの仕方が複数通りある場合はできるだけ長くマッチさせる点が異なります。
196196

197197
+/{{単語1}}/{{単語2}}+::
198-
{{単語1}}をパターンとして見たとき、それがいま展開しようとしている値の一部にマッチするならば、そのマッチする部分を{{単語2}}に置き換えます。結果として、このパラメータ展開はマッチした部分を{{単語2}}に置き換えた値に展開されます。マッチする箇所が複数ある場合は、最初の箇所が選ばれます。マッチの仕方が複数通りある場合はできるだけ長くマッチさせます。
198+
{{単語1}}をパターンとして見たとき、それがいま展開しようとしている値の一部にマッチするならば、そのマッチする部分を{{単語2}}に置き換えます。結果として、このパラメータ展開はマッチした部分を{{単語2}}に置き換えた値に展開されます。マッチする箇所が複数ある場合は、最初の箇所が選ばれます。マッチの仕方が複数通りある場合はできるだけ長くマッチさせます。{{単語1}}が空文字列の場合は値の先頭の空文字列にマッチするので、+${{{パラメータ名}}/""/{{単語2}}}+ は値の先頭に{{単語2}}を付け加えた値に展開されます。
199199
+
200200
この加工指定は link:posix.html[POSIX 準拠モード]では使えません。
201201

202202
+/#{{単語1}}/{{単語2}}+::
203-
この加工指定は +/{{単語1}}/{{単語2}}+ と同様ですが、いま展開しようとしている値の先頭部分にしかマッチしない点が異なります。
203+
この加工指定は +/{{単語1}}/{{単語2}}+ と同様ですが、いま展開しようとしている値の先頭部分にしかマッチしない点が異なります。{{単語1}}が空文字列の場合は値の先頭の空文字列にマッチするので、+${{{パラメータ名}}/#/{{単語2}}}+ は値の先頭に{{単語2}}を付け加えた値に展開されます。
204204

205205
+/%{{単語1}}/{{単語2}}+::
206-
この加工指定は +/{{単語1}}/{{単語2}}+ と同様ですが、いま展開しようとしている値の末尾部分にしかマッチしない点が異なります。
206+
この加工指定は +/{{単語1}}/{{単語2}}+ と同様ですが、いま展開しようとしている値の末尾部分にしかマッチしない点が異なります。{{単語1}}が空文字列の場合は値の末尾の空文字列にマッチするので、+${{{パラメータ名}}/%/{{単語2}}}+ は値の末尾に{{単語2}}を付け加えた値に展開されます。
207207

208208
+//{{単語1}}/{{単語2}}+::
209-
この加工指定は +/{{単語1}}/{{単語2}}+ と同様ですが、マッチする箇所が複数ある場合は最初の箇所だけではなく全ての箇所を{{単語2}}に置き換える点が異なります。
209+
この加工指定は +/{{単語1}}/{{単語2}}+ と同様ですが、マッチする箇所が複数ある場合は最初の箇所だけではなく全ての箇所を{{単語2}}に置き換える点が異なります。{{単語1}}が空文字列の場合は値の先頭・末尾および隣接する各文字の間の空文字列にマッチするので、それら全てが{{単語2}}に置き換えられます。
210210

211211
+:/{{単語1}}/{{単語2}}+::
212212
この加工指定は +/{{単語1}}/{{単語2}}+ と同様ですが、いま展開しようとしている値全体にマッチする場合しか対象としない点が異なります。

tests/param-y.tst

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -455,7 +455,7 @@ a='123/456/789' b='1*2?3' HOME=/
455455
bracket ${a/4*6/x} ${a/\//y} ${a/\//} ${a/\/}
456456
bracket ${a/#*3/x} ${a/#456/y}
457457
bracket ${a/%7*/x} ${a/%456/y}
458-
bracket ${a//4*6/x} ${a//\//y} ${a//\//} ${a//\/}
458+
bracket ${a//4*6/x} ${a//\//y} ${a//\//} ${a//\/} ${a//[[:digit:]]/x}
459459
bracket ${a:/1*9/x} ${a:/2*9/x} ${a:/1*8/x}
460460
bracket ${b/\**\?/x} ${b/"*"?'?'/x}
461461
bracket ${a/5/~/\*'*'"*"} ${a//~}
@@ -465,14 +465,34 @@ __IN__
465465
[123/x/789][123y456/789][123456/789][123456/789]
466466
[x/456/789][123/456/789]
467467
[123/456/x][123/456/789]
468-
[123/x/789][123y456y789][123456789][123456789]
468+
[123/x/789][123y456y789][123456789][123456789][xxx/xxx/xxx]
469469
[x][123/456/789][123/456/789]
470470
[1x3][1x3]
471471
[123/4/***6/789][123456789]
472472
[1_2_3][1_2_3]
473473
__OUT__
474474
# XXX: Should the last one (${a/*/"$b"}) expand to 1*2?3 rather than 1_2_3?
475475

476+
test_oE '${a/b/c} with empty match'
477+
a='123/456/789' b=''
478+
bracket ${a/#/x} ${a/%/x}
479+
bracket ${a/""/x} ${a//""/x}
480+
bracket ${a//*/x} ${b//*/x}
481+
bracket ${a/#1*/x} ${a/%9*/x}
482+
bracket ${a/#2/x} ${a/%8/x}
483+
set -- 1 22 333
484+
bracket "${@/#/p}" "${@/%/s}"
485+
bracket ${b/#/x} ${b/%/x} ${b//""/x}
486+
__IN__
487+
[x123/456/789][123/456/789x]
488+
[x123/456/789][x1x2x3x/x4x5x6x/x7x8x9x]
489+
[x][x]
490+
[x][123/456/78x]
491+
[123/456/789][123/456/789]
492+
[p1][p22][p333][1s][22s][333s]
493+
[x][x][x]
494+
__OUT__
495+
476496
test_oE 'scalar parameter index'
477497
a='1-2-3'
478498
bracket @ "${a[@]}"

xfnmatch.c

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -687,8 +687,10 @@ xfnmresult_T wmatch_longest(
687687

688688
/* Substitutes part of string `s' that matches pre-compiled pattern `xfnm'
689689
* with string `repl'. If `substall' is true, all matching substrings in `s' are
690-
* substituted. Otherwise, only the first match is substituted. The resulting
691-
* string is returned as a newly-malloced string. */
690+
* substituted. Otherwise, only the first match is substituted. An empty match
691+
* is substituted like any other match; when substituting all matches, one
692+
* character is skipped after each empty match to ensure progress. The
693+
* resulting string is returned as a newly-malloced string. */
692694
wchar_t *xfnm_subst(const xfnmatch_T *restrict xfnm, const wchar_t *restrict s,
693695
const wchar_t *restrict repl, bool substall)
694696
{
@@ -711,11 +713,18 @@ wchar_t *xfnm_subst(const xfnmatch_T *restrict xfnm, const wchar_t *restrict s,
711713
wb_init(&buf);
712714
do {
713715
xfnmresult_T result = xfnm_wmatch(xfnm, &s[i]);
714-
if (result.start == (size_t) -1 || result.start >= result.end)
716+
if (result.start == (size_t) -1)
715717
break;
716718
wb_ncat(&buf, &s[i], result.start);
717719
wb_cat(&buf, repl);
718720
i += result.end;
721+
if (s[i] == L'\0')
722+
break;
723+
if (result.start == result.end) {
724+
/* empty match: skip one character to ensure progress */
725+
wb_wccat(&buf, s[i]);
726+
i++;
727+
}
719728
} while (substall);
720729
return wb_towcs(wb_cat(&buf, &s[i]));
721730
}

0 commit comments

Comments
 (0)