使用 regsub 从字符串末尾进行非贪婪匹配

Non-greedy match from end of string with regsub

我的文件夹路径如下:

/h/apps/new/app/k1999

我想使用以下正则表达式删除 /app/k1999 部分:

set folder "/h/apps/new/app/k1999"
regsub {\/app.+$} $folder "" new_folder

但结果是/h:删除的元素太多。

我注意到我应该使用非贪婪匹配,所以我将代码更改为:

regsub {\/app.+?$} $folder "" new_folder

但结果仍然是/h。 上面的代码有什么问题?

非贪婪只是意味着它将尝试匹配最少数量的字符并在整个正则表达式不匹配时增加该数量。相反 - 贪婪 - 意味着它将尝试匹配尽可能多的字符,如果整个正则表达式不匹配则减少该数量。

正则表达式中的

$ 表示字符串的结尾。因此 something.+$something.+?$ 是等价的,只是在匹配之前要多重试。

在您的例子中,/app.+/apps 匹配,这是您的字符串中第一次出现 /app。您可以通过更明确地添加 /app:

之后的 / 来修复它
regsub {/app/.+$} $folder "" new_folder

如果你想匹配 app 作为一个完整的词,你可以使用 word boundaries 在 Tcl 中是 \m\M:

\m
matches only at the beginning of a word
\M
matches only at the end of a word

我们只需要 \M 因为 / 是一个非单词字符,我们不需要 \m:

set folder "/h/apps/new/app/k1999"
regsub {/app\M.+$} $folder "" newfolder
puts $newfolder

IDEONE demo

结果:/h/apps/new(我们删除了整个单词 app 到结尾的所有内容。)

如果你只想删除路径中的一部分字符串,你可以使用否定 class [^/]+ 来确保你只删除 match a subpart of a path:

regsub {/app/[^/]+} $folder "" newfolder

可以使用正则表达式替换操作从路径名中删除目录后缀,但这并不意味着您应该.

file join {*}[lmap dir [file split $folder] {if {$dir ne {app}} {set dir} break}]
# -> /h/apps/new

路径名是一个字符串,但更准确地说,它是一个目录名列表:

file split $folder
# -> / h apps new app k1999

您想要的是目录名称的子列表,但不包括名为 "app" 的目录。

lmap dir [file split $folder] {if {$dir ne {app}} {set dir} break}
# -> / h apps new

(可以根据需要测试目录名称;有两种可能性是 {$dir ni {foo app bar}} 跳过其他名称,或 {![string match app-* $dir]} 任何以 "app-" 开头的名称。)

当你得到你想要的目录名称列表时,你将它的元素再次连接回路径名,如上所述。

那么为什么要这样做而不是使用正则表达式替换操作呢?这个问题很好的说明了问题。除非是 RE 专家或非常仔细地阅读文档,否则很可能会根据直觉来制定正则表达式。在最坏的情况下,它第一次起作用。如果没有,人们很想修补它直到它出现。任何未被充分理解的(是的, 一个词)RE 似乎大部分时间都有效,偶尔会出现误报和误报,以保持有趣。

拆分、截断、合并。不会出错的。如果确实如此,它显然是错误的,迫使你修复它。

文档:break, file, if, lmap, set

正则表达式引擎总是尽快开始匹配;贪婪不会影响这一点。这意味着在这种情况下,它总是过早开始匹配;你想要最后一场比赛,而不是第一场比赛。

如果您使用regexp -all -indices -inline,您可以找到最后一场比赛的开始位置。这样您就可以删除实际上不需要的部分(例如,将其替换为空字符串:

set folder "/h/apps/new/app/k1999"
set indices [regexp -all -indices -inline {/app} $folder]
# This gets this value: {2 5} {11 14}

# If we have indices — if we had a match — we can do the rest of our processing
if {[llength $indices] > 0} {
    # Get the '11'; the first sub-element of the last element
    set index [lindex $indices end 0]
    # Replace '/app/k1999' with the empty string
    set newfolder [string replace $folder $index end ""]
} else {
    set newfolder $folder;   # In case there's no match...
}