如何使用PowerShell替换文件中的多个字符串

129

我正在编写一个定制配置文件的脚本。我想在该文件中替换多个字符串实例,并尝试使用PowerShell完成此任务。

对于单个替换,它可以正常工作,但进行多个替换非常慢,因为每次都需要再次解析整个文件,而此文件非常大。脚本如下:

$original_file = 'path\filename.abc'
$destination_file =  'path\filename.abc.new'
(Get-Content $original_file) | Foreach-Object {
    $_ -replace 'something1', 'something1new'
    } | Set-Content $destination_file

我想要类似这样的东西,但是我不知道怎么写:

$original_file = 'path\filename.abc'
$destination_file =  'path\filename.abc.new'
(Get-Content $original_file) | Foreach-Object {
    $_ -replace 'something1', 'something1aa'
    $_ -replace 'something2', 'something2bb'
    $_ -replace 'something3', 'something3cc'
    $_ -replace 'something4', 'something4dd'
    $_ -replace 'something5', 'something5dsf'
    $_ -replace 'something6', 'something6dfsfds'
    } | Set-Content $destination_file
6个回答

193

一个选项是将 -replace 操作链接在一起。每行末尾的 ` 转义换行符,导致 PowerShell 继续解析下一行的表达式:

$original_file = 'path\filename.abc'
$destination_file =  'path\filename.abc.new'
(Get-Content $original_file) | Foreach-Object {
    $_ -replace 'something1', 'something1aa' `
       -replace 'something2', 'something2bb' `
       -replace 'something3', 'something3cc' `
       -replace 'something4', 'something4dd' `
       -replace 'something5', 'something5dsf' `
       -replace 'something6', 'something6dfsfds'
    } | Set-Content $destination_file

另一个选项是分配一个中间变量:

$x = $_ -replace 'something1', 'something1aa'
$x = $x -replace 'something2', 'something2bb'
...
$x

原始文件 $original_file 是否等于目标文件 $destination_file?也就是说,我是否正在修改与我的源文件相同的文件? - cquadrini
由于PowerShell cmdlet流式传输其输入/输出的方式,我不认为在同一管道中写入相同的文件会起作用。但是,您可以执行类似于$c = Get-Content $original_file; $c | ... | Set-Content $original_file的操作。 - dahlbyk
你是否遇到过使用 Set-Content 时出现文件编码问题,无法保持原始编码?例如 UTF-8 或 ANSI 编码。 - Kiquenet
3
PowerShell 在这方面确实不太好用。你必须自己检测编码,比如使用 https://github.com/dahlbyk/posh-git/blob/869d4c5159797755bc04749db47b166136e59132/install.ps1#L23-L37 - dahlbyk
这个解决方案对我来说失败了。它处理的时间更长,并且生成的文件非常大,而不是现有文件中只有30行短文。 - SouthSun

31

为了让George Howarth的代码能够正确地多次替换,您需要删除断点,将输出分配给一个变量($line),然后输出该变量:

$lookupTable = @{
    'something1' = 'something1aa'
    'something2' = 'something2bb'
    'something3' = 'something3cc'
    'something4' = 'something4dd'
    'something5' = 'something5dsf'
    'something6' = 'something6dfsfds'
}

$original_file = 'path\filename.abc'
$destination_file =  'path\filename.abc.new'

Get-Content -Path $original_file | ForEach-Object {
    $line = $_

    $lookupTable.GetEnumerator() | ForEach-Object {
        if ($line -match $_.Key)
        {
            $line = $line -replace $_.Key, $_.Value
        }
    }
   $line
} | Set-Content -Path $destination_file

2
这是目前为止我见过的最好方法。唯一的问题是,我必须先将整个文件内容读入变量中,才能使用相同的源/目标文件路径。 - angularsen
这看起来像是最好的答案,尽管我见过一些奇怪的行为,它会错误地匹配。例如,在哈希表中使用十六进制值作为字符串(0x0、0x1、0x100、0x10000)的情况下,0x10000将匹配0x1。 - user705185

20

使用 PowerShell 3 版本,您可以将 replace 调用链接在一起:

 (Get-Content $sourceFile) | ForEach-Object {
    $_.replace('something1', 'something1').replace('somethingElse1', 'somethingElse2')
 } | Set-Content $destinationFile

运行良好 + 流畅的口味 - hdoghmen
只要您不需要正则表达式。 - nloewen
问题中提到了正则表达式吗? - Ian Robertson

13

假设每行只能有一个'something1''something2'等,您可以使用查找表:

$lookupTable = @{
    'something1' = 'something1aa'
    'something2' = 'something2bb'
    'something3' = 'something3cc'
    'something4' = 'something4dd'
    'something5' = 'something5dsf'
    'something6' = 'something6dfsfds'
}

$original_file = 'path\filename.abc'
$destination_file =  'path\filename.abc.new'

Get-Content -Path $original_file | ForEach-Object {
    $line = $_

    $lookupTable.GetEnumerator() | ForEach-Object {
        if ($line -match $_.Key)
        {
            $line -replace $_.Key, $_.Value
            break
        }
    }
} | Set-Content -Path $destination_file
如果你可以拥有多个这样的东西,只需删除if语句中的break即可。

我看到TroyBramley在最后一行之前添加了$line,以便写入任何没有更改的行。好的。在我的情况下,我只更改了需要替换的每一行。 - cliffclof

11
第三个选项,对于一个管道化的一行代码,是嵌套-replaces:
PS> ("ABC" -replace "B","C") -replace "C","D"
ADD

并且:

PS> ("ABC" -replace "C","D") -replace "B","C"
ACD

这样做可以保留执行顺序,易于阅读,并且可以很好地适应管道。我更喜欢使用括号进行显式控制、自我文档化等。虽然没有它们也可以工作,但你有多少信任呢?

-Replace是一个比较运算符,它接受一个对象并返回一个经过修改的对象。这就是为什么您可以像上面显示的那样堆叠或嵌套它们。

请参见:

help about_operators

1

只是一个通用的可重复使用的解决方案:

function Replace-String {
    [CmdletBinding()][OutputType([string])] param(
        [Parameter(Mandatory = $True, ValueFromPipeLine = $True)]$InputObject,
        [Parameter(Mandatory = $True, Position = 0)][Array]$Pair,
        [Alias('CaseSensitive')][switch]$MatchCase
    )
    for ($i = 0; $i -lt $Pair.get_Count()) {
        if ($Pair[$i] -is [Array]) {
            $InputObject = $InputObject |Replace-String -MatchCase:$MatchCase $Pair[$i++]
        }
        else {
            $Regex = $Pair[$i++]
            $Substitute = if ($i -lt $Pair.get_Count() -and $Pair[$i] -isnot [Array]) { $Pair[$i++] }
            if ($MatchCase) { $InputObject = $InputObject -cReplace $Regex, $Substitute }
            else            { $InputObject = $InputObject -iReplace $Regex, $Substitute }
        }
    }
    $InputObject
}; Set-Alias Replace Replace-String

使用方法:

$lookupTable |Replace 'something1', 'something1aa', 'something2', 'something2bb', 'something3', 'something3cc'

或者:

$lookupTable |Replace ('something1', 'something1aa'), ('something2', 'something2bb'), ('something3', 'something3cc')

例子:

'hello world' |Replace ('h','H'), ' ', ('w','W')
HelloWorld

我已经为此创建了一个正式的PowerShell请求:#15876 Make -Replace operator support multiple Regex/Substitution pairs - iRon

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接