diff --git a/NEWS b/NEWS index 7956f910..eb3e4240 100644 --- a/NEWS +++ b/NEWS @@ -2,6 +2,10 @@ ## Yash 2.62 (Unreleased) + - In the `${parameter/pattern/replacement}` parameter expansion, an + empty pattern is now matched and replaced like any other match. + This allows `${var/#/prefix}` and `${var/%/suffix}` to prefix and + suffix the value, respectively. - [line-editing] Updated the completion script for tar to support GNU tar 1.35 and libarchive (bsdtar) 3.8.7. - Updated the sample initialization script (yashrc): diff --git a/NEWS.ja b/NEWS.ja index da1561e9..2e8af57b 100644 --- a/NEWS.ja +++ b/NEWS.ja @@ -2,6 +2,10 @@ ## Yash 2.62 (未リリース) + - パラメータ展開 `${parameter/pattern/replacement}` で、空のパターンも + 他のマッチと同様にマッチ・置換されるようになった。これにより + `${var/#/prefix}` および `${var/%/suffix}` で値の先頭・末尾に + 文字列を付け加えられる - [行編集] tar の補完を GNU tar 1.35 および libarchive (bsdtar 3.8.7) に合わせて更新 - 初期化スクリプト (yashrc) のサンプルを更新: diff --git a/doc/expand.txt b/doc/expand.txt index c0b72f40..900c6762 100644 --- a/doc/expand.txt +++ b/doc/expand.txt @@ -336,6 +336,8 @@ If {{word1}} matches more than one part of the value, only the first part is replaced. The shortest matching is replaced if more than one matching is possible for the same starting point in the value. +An empty {{word1}} matches the empty string at the beginning of the value, so ++${{{parameter}}/""/{{word2}}}+ prefixes {{word2}} to the value. + This modifier cannot be used in the link:posix.html[POSIXly-correct mode]. @@ -343,16 +345,23 @@ This modifier cannot be used in the link:posix.html[POSIXly-correct mode]. This is similar to +/{{word1}}/{{word2}}+ above. The only difference is that {{word1}} matches only at the beginning of the value being expanded. +An empty {{word1}} matches the empty string at the beginning of the value, so ++${{{parameter}}/#/{{word2}}}+ prefixes {{word2}} to the value. +/%{{word1}}/{{word2}}+:: This is similar to +/{{word1}}/{{word2}}+ above. The only difference is that {{word1}} matches only at the end of the value being expanded. +An empty {{word1}} matches the empty string at the end of the value, so ++${{{parameter}}/%/{{word2}}}+ suffixes {{word2}} to the value. +//{{word1}}/{{word2}}+:: This is similar to +/{{word1}}/{{word2}}+ above. The only difference is that all matched parts are replaced if {{word1}} matches more than one part of the value. +An empty {{word1}} matches the empty string at the beginning of the value, +between each two adjacent characters, and at the end of the value, so all +those empty matches are replaced with {{word2}}. +:/{{word1}}/{{word2}}+:: This is similar to +/{{word1}}/{{word2}}+ above. diff --git a/doc/ja/expand.txt b/doc/ja/expand.txt index df3432ca..ff991835 100644 --- a/doc/ja/expand.txt +++ b/doc/ja/expand.txt @@ -195,18 +195,18 @@ echo "${array[2,-2]}" この加工指定は +%{{単語}}+ と同様ですが、マッチの仕方が複数通りある場合はできるだけ長くマッチさせる点が異なります。 +/{{単語1}}/{{単語2}}+:: -{{単語1}}をパターンとして見たとき、それがいま展開しようとしている値の一部にマッチするならば、そのマッチする部分を{{単語2}}に置き換えます。結果として、このパラメータ展開はマッチした部分を{{単語2}}に置き換えた値に展開されます。マッチする箇所が複数ある場合は、最初の箇所が選ばれます。マッチの仕方が複数通りある場合はできるだけ長くマッチさせます。 +{{単語1}}をパターンとして見たとき、それがいま展開しようとしている値の一部にマッチするならば、そのマッチする部分を{{単語2}}に置き換えます。結果として、このパラメータ展開はマッチした部分を{{単語2}}に置き換えた値に展開されます。マッチする箇所が複数ある場合は、最初の箇所が選ばれます。マッチの仕方が複数通りある場合はできるだけ長くマッチさせます。{{単語1}}が空文字列の場合は値の先頭の空文字列にマッチするので、+${{{パラメータ名}}/""/{{単語2}}}+ は値の先頭に{{単語2}}を付け加えた値に展開されます。 + この加工指定は link:posix.html[POSIX 準拠モード]では使えません。 +/#{{単語1}}/{{単語2}}+:: -この加工指定は +/{{単語1}}/{{単語2}}+ と同様ですが、いま展開しようとしている値の先頭部分にしかマッチしない点が異なります。 +この加工指定は +/{{単語1}}/{{単語2}}+ と同様ですが、いま展開しようとしている値の先頭部分にしかマッチしない点が異なります。{{単語1}}が空文字列の場合は値の先頭の空文字列にマッチするので、+${{{パラメータ名}}/#/{{単語2}}}+ は値の先頭に{{単語2}}を付け加えた値に展開されます。 +/%{{単語1}}/{{単語2}}+:: -この加工指定は +/{{単語1}}/{{単語2}}+ と同様ですが、いま展開しようとしている値の末尾部分にしかマッチしない点が異なります。 +この加工指定は +/{{単語1}}/{{単語2}}+ と同様ですが、いま展開しようとしている値の末尾部分にしかマッチしない点が異なります。{{単語1}}が空文字列の場合は値の末尾の空文字列にマッチするので、+${{{パラメータ名}}/%/{{単語2}}}+ は値の末尾に{{単語2}}を付け加えた値に展開されます。 +//{{単語1}}/{{単語2}}+:: -この加工指定は +/{{単語1}}/{{単語2}}+ と同様ですが、マッチする箇所が複数ある場合は最初の箇所だけではなく全ての箇所を{{単語2}}に置き換える点が異なります。 +この加工指定は +/{{単語1}}/{{単語2}}+ と同様ですが、マッチする箇所が複数ある場合は最初の箇所だけではなく全ての箇所を{{単語2}}に置き換える点が異なります。{{単語1}}が空文字列の場合は値の先頭・末尾および隣接する各文字の間の空文字列にマッチするので、それら全てが{{単語2}}に置き換えられます。 +:/{{単語1}}/{{単語2}}+:: この加工指定は +/{{単語1}}/{{単語2}}+ と同様ですが、いま展開しようとしている値全体にマッチする場合しか対象としない点が異なります。 diff --git a/tests/param-y.tst b/tests/param-y.tst index f853ffdb..f03cb376 100644 --- a/tests/param-y.tst +++ b/tests/param-y.tst @@ -455,7 +455,7 @@ a='123/456/789' b='1*2?3' HOME=/ bracket ${a/4*6/x} ${a/\//y} ${a/\//} ${a/\/} bracket ${a/#*3/x} ${a/#456/y} bracket ${a/%7*/x} ${a/%456/y} -bracket ${a//4*6/x} ${a//\//y} ${a//\//} ${a//\/} +bracket ${a//4*6/x} ${a//\//y} ${a//\//} ${a//\/} ${a//[[:digit:]]/x} bracket ${a:/1*9/x} ${a:/2*9/x} ${a:/1*8/x} bracket ${b/\**\?/x} ${b/"*"?'?'/x} bracket ${a/5/~/\*'*'"*"} ${a//~} @@ -465,7 +465,7 @@ __IN__ [123/x/789][123y456/789][123456/789][123456/789] [x/456/789][123/456/789] [123/456/x][123/456/789] -[123/x/789][123y456y789][123456789][123456789] +[123/x/789][123y456y789][123456789][123456789][xxx/xxx/xxx] [x][123/456/789][123/456/789] [1x3][1x3] [123/4/***6/789][123456789] @@ -473,6 +473,26 @@ __IN__ __OUT__ # XXX: Should the last one (${a/*/"$b"}) expand to 1*2?3 rather than 1_2_3? +test_oE '${a/b/c} with empty match' +a='123/456/789' b='' +bracket ${a/#/x} ${a/%/x} +bracket ${a/""/x} ${a//""/x} +bracket ${a//*/x} ${b//*/x} +bracket ${a/#1*/x} ${a/%9*/x} +bracket ${a/#2/x} ${a/%8/x} +set -- 1 22 333 +bracket "${@/#/p}" "${@/%/s}" +bracket ${b/#/x} ${b/%/x} ${b//""/x} +__IN__ +[x123/456/789][123/456/789x] +[x123/456/789][x1x2x3x/x4x5x6x/x7x8x9x] +[x][x] +[x][123/456/78x] +[123/456/789][123/456/789] +[p1][p22][p333][1s][22s][333s] +[x][x][x] +__OUT__ + test_oE 'scalar parameter index' a='1-2-3' bracket @ "${a[@]}" diff --git a/xfnmatch.c b/xfnmatch.c index 8b35c591..a66c71fa 100644 --- a/xfnmatch.c +++ b/xfnmatch.c @@ -687,8 +687,10 @@ xfnmresult_T wmatch_longest( /* Substitutes part of string `s' that matches pre-compiled pattern `xfnm' * with string `repl'. If `substall' is true, all matching substrings in `s' are - * substituted. Otherwise, only the first match is substituted. The resulting - * string is returned as a newly-malloced string. */ + * substituted. Otherwise, only the first match is substituted. An empty match + * is substituted like any other match; when substituting all matches, one + * character is skipped after each empty match to ensure progress. The + * resulting string is returned as a newly-malloced string. */ wchar_t *xfnm_subst(const xfnmatch_T *restrict xfnm, const wchar_t *restrict s, const wchar_t *restrict repl, bool substall) { @@ -711,11 +713,18 @@ wchar_t *xfnm_subst(const xfnmatch_T *restrict xfnm, const wchar_t *restrict s, wb_init(&buf); do { xfnmresult_T result = xfnm_wmatch(xfnm, &s[i]); - if (result.start == (size_t) -1 || result.start >= result.end) + if (result.start == (size_t) -1) break; wb_ncat(&buf, &s[i], result.start); wb_cat(&buf, repl); i += result.end; + if (s[i] == L'\0') + break; + if (result.start == result.end) { + /* empty match: skip one character to ensure progress */ + wb_wccat(&buf, s[i]); + i++; + } } while (substall); return wb_towcs(wb_cat(&buf, &s[i])); }