[英]Why does LF and CRLF behave differently with /^\s*$/gm regex?
我一直在 Windows 上看到这个问题。 当我尝试清除 Unix 上每一行上的任何空格时:
const input =
`===
HELLO
WOLRD
===`
console.log(input.replace(/^\s+$/gm, ''))
这产生了我所期望的:
===
HELLO
WOLRD
===
即如果有空行上的空格,它们会被删除。 另一方面,在 Windows 上,正则表达式会清除整个字符串。 为了显示:
const input =
`===
HELLO
WOLRD
===`.replace(/\r?\n/g, '\r\n')
console.log(input.replace(/^\s+$/gm, ''))
(模板文字在 JS 中总是只打印\\n
,所以我不得不用\\r\\n
替换来模拟 Windows( ?
在\\r
只是为了确保那些不相信的人)。结果:
===
HELLO
WOLRD
===
整条线都没了! 但是我的正则表达式有^
和$
设置了m
标志,所以它有点像/^-to-$/m
。 \\r
和\\r\\n
之间的区别是什么使它产生不同的结果?
当我做一些日志记录时
console.log(input.replace(/^\s*$/gm, (m) => {
console.log('matched')
return ''
}))
随着 \\r\\n 我看到
matched
matched
matched
matched
matched
matched
===
HELLO
WOLRD
===
并且只有 \\n
matched
matched
matched
===
HELLO
WOLRD
===
TL;DR包含空格和换行符的模式也将匹配\\r\\n
序列的字符部分,如果你允许的话。
首先,让我们实际检查一下替换时哪些字符存在,哪些不存在。 从仅使用换行符的字符串开始:
const inputLF = `=== HELLO WOLRD ===`.replace(/\\r?\\n/g, "\\n"); console.log('------------ INPUT ') console.log(inputLF); console.log('------------') debugPrint(inputLF, 2); debugPrint(inputLF, 3); debugPrint(inputLF, 4); debugPrint(inputLF, 5); const replaceLF = inputLF.replace(/^\\s+$/gm, ''); console.log('------------ REPLACEMENT') console.log(replaceLF); console.log('------------') debugPrint(replaceLF, 2); debugPrint(replaceLF, 3); debugPrint(replaceLF, 4); debugPrint(replaceLF, 5); console.log(`charcode ${replaceLF.charCodeAt(2)} : ${replaceLF.charAt(2)}`); console.log(`charcode ${replaceLF.charCodeAt(3)} : ${replaceLF.charAt(3)}`); console.log(`charcode ${replaceLF.charCodeAt(4)} : ${replaceLF.charAt(4)}`); console.log(`charcode ${replaceLF.charCodeAt(5)} : ${replaceLF.charAt(5)}`); console.log('------------') console.log('inputLF === replaceLF :', inputLF === replaceLF) function debugPrint(str, charIndex) { console.log(`index: ${charIndex} charcode: ${str.charCodeAt(charIndex)} character: ${str.charAt(charIndex)}` ); }
每行以字符代码 10 结尾,它是换行 (LF) 字符,用\\n
表示在字符串文字中。 在替换之前和之后,两个字符串是相同的——不仅看起来相同而且实际上彼此相等,因此替换什么也没做。
现在让我们检查另一种情况:
const inputCRLF = `=== HELLO WOLRD ===`.replace(/\\r?\\n/g, "\\r\\n") console.log('------------ INPUT ') console.log(inputCRLF); console.log('------------') debugPrint(inputCRLF, 2); debugPrint(inputCRLF, 3); debugPrint(inputCRLF, 4); debugPrint(inputCRLF, 5); debugPrint(inputCRLF, 6); debugPrint(inputCRLF, 7); const replaceCRLF = inputCRLF.replace(/^\\s+$/gm, '');; console.log('------------ REPLACEMENT') console.log(replaceCRLF); console.log('------------') debugPrint(replaceCRLF, 2); debugPrint(replaceCRLF, 3); debugPrint(replaceCRLF, 4); debugPrint(replaceCRLF, 5); function debugPrint(str, charIndex) { console.log(`index: ${charIndex} charcode: ${str.charCodeAt(charIndex)} character: ${str.charAt(charIndex)}` ); }
这次每一行都以字符代码 13 结尾,这是回车 (CR) 字符,用\\r
表示在字符串文字中,然后是 LF。 替换后,不是具有=\\r\\n\\r\\nH
序列,而是不仅仅是=\\r\\nH
。 让我们来看看为什么。
以下是 MDN关于元字符^
:
匹配输入的开头。 如果 multiline 标志设置为 true,也会在换行符后立即匹配。
这是 MDN 关于元字符$
匹配输入的结尾。 如果 multiline 标志设置为 true,则还匹配紧接在换行符之前的字符。
所以他们在和换行符前匹配。 其中,MDN 表示 LF或CR。 如果我们测试包含不同换行符的字符串,就可以看到这一点:
const stringLF = "hello\\nworld"; const stringCRLF = "hello\\r\\nworld"; const regexStart = /^\\s/m; const regexEnd = /\\s$/m; console.log(regexStart.exec(stringLF)); console.log(regexStart.exec(stringCRLF)); console.log(regexEnd.exec(stringLF)); console.log(regexEnd.exec(stringCRLF));
如果我们尝试匹配换行符附近的空格,如果有 LF,这不会匹配任何内容,但它确实将 CR 与 CRLF 匹配。 因此,在这种情况下, $
将在此处匹配:
"hello\r\nworld"
^^ what `^\s` matches
"hello\r\nworld"
^^ what `\s$` matches
所以^
和$
都将 CRLF 序列中的任何一个识别为行尾。 当您进行搜索和替换时,这将有所作为。 由于您的正则表达式指定^\\s+$
这意味着当您有一行完全是\\r\\n
它匹配. 但有一个不明显的原因:
const re = /^\\s+$/m; const sringLF = "hello\\n\\nworld"; const stringCRLF = "hello\\r\\n\\r\\nworld"; console.log(re.exec(sringLF)); console.log(re.exec(stringCRLF));
因此,正则表达式不匹配\\r\\n
而是匹配其他两个换行符之间的\\n\\r
(两个空白字符)。 这是因为+
是急切的,并且会尽可能多地消耗字符序列。 这是正则表达式引擎将尝试的内容。 为简洁起见有些简化:
input = "hello\r\n\r\nworld
regex = /^\s+$/
Step 1
hello[\r]\n\r\nworld
matches `^`, symbol satisfied -> continue with next symbol in regex
Step 2
hello[\r\n]\r\nworld
matches `^\s+` -> continue matching to satisfy `+` quantifier
Step 3
hello[\r\n\r]\nworld
matches `^\s+` -> continue matching to satisfy `+` quantifier
Step 4
hello[\r\n\r\n]world
matches `^\s+` -> continue matching to satisfy `+` quantifier
Step 5
hello[\r\n\r\nw]orld
does not match `\s` -> backtrack
Step 6
hello[\r\n\r\n]world
matches `^\s+`, quantifier satisfied -> continue to next symbol in regex
Step 7
hello[\r\n\r\nw]orld
does not match `$` in `^\s+$` -> backtrack
Step 8
hello[\r\n\r\n]world
matches `^\s+$`, last symbol satisfied -> finish
最后,这里有一些隐藏的东西 - 匹配空格很重要。 这是因为它与大多数其他符号的行为不同,因为它明确匹配换行符,而.
不会:
匹配除行终止符以外的任何单个字符
因此,如果您指定\\s$
这将与\\r\\n
中的 CR 匹配,因为正则表达式引擎被迫为\\s
和$
寻找匹配项,因此它会在\\n
之前找到\\r
。 但是,对于许多其他模式不会发生这种情况,因为$
通常在CR之前(或字符串末尾)时会得到满足。
与^\\s
相同,它将在 CRLF 中的 LF 满足的换行符后显式查找空格字符,但是如果您不寻找它,那么它会在 LF 之后愉快地匹配:
const stringLF = "hello\\nworld"; const stringCRLF = "hello\\r\\nworld"; const regexStartAll = /^./mg; const regexEndAll = /.$/gm; console.log(stringLF.match(regexStartAll)); console.log(stringCRLF.match(regexStartAll)); console.log(stringLF.match(regexEndAll)); console.log(stringCRLF.match(regexEndAll));
因此,所有这一切都意味着^\\s+$
具有一些不直观的行为,但一旦您了解正则表达式引擎与您告诉它的完全匹配,就会完全一致。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.