Home > Blockchain >  Powershell replace multiline text
Powershell replace multiline text

Time:10-06

I am trying to replace a multiline text with another multiline text. However no attempt seems to work. The searched text is not being found.

In the searched Directory are some .xml files that contain the searched text.

Here is some code:

Set-Location "D:\path\test"

$file_names = (Get-ChildItem).Name

$oldCode =  @"

       </LinearLayout>



    <TextView
        android:layout_weight="1"
        android:layout_width="wrap_content"
        android:layout_height="0dp"/>


</LinearLayout>
"@

$newCode =  @"
        ANYTHING NEW

     </LinearLayout>



    <TextView
        android:layout_weight="1"
        android:layout_width="wrap_content"
        android:layout_height="0dp"/>


</LinearLayout>
"@

foreach ($name in $file_names)
{
    # None of below code works.

    # Attempt 1: trying to find the code
    Write-Host $name
    $ret_string = (Get-Content -raw -Path $name | Select-String $oldCode -AllMatches | % { $_.matches}).count
    Write-Host $ret_string

    # Attempt 2: trying to actually replace the string
    $fileContent = Get-Content $name -Raw
    Write-Host $fileContent
    $newFileContent = $fileContent -replace $oldCode, $newCode
    Write-Host $newFileContent

    # Attempt 3: another try to replace the string
    ((Get-Content -Path $name -Raw) -replace $oldCode, $newCode) | Set-Content -Path $name
}

Write-Host "Press any key to continue..."
$Host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown")

CodePudding user response:

Select-string and -replace both take regular expressions for their search string, so you have to escape newlines in a multiline search string, as well as any other characters with special meanings in regex. Fortunaely, the [Regex] class, offers the [Regex]::Escape() method:

 $oldCode =  [Regex]::Escape(@"

       </LinearLayout>



    <TextView
        android:layout_weight="1"
        android:layout_width="wrap_content"
        android:layout_height="0dp"/>


</LinearLayout>
"@)


$newCode =  @"
        ANYTHING NEW

     </LinearLayout>



    <TextView
        android:layout_weight="1"
        android:layout_width="wrap_content"
        android:layout_height="0dp"/>


</LinearLayout>
"@

### Now your search string is:
PS > $oldCode
\n\ \ \ \ \ \ \ </LinearLayout>\n\n\n\n\ \ \ \ <TextView\n\ \ \ \ \ \ \ \ android:layout_weight="1"\n\ \ \ \ \ \ \ \ android:layout_width="wrap_content"\n\ \ \ \ \ \ \ \ android:layout_height="0dp"/>\n\n\n</LinearLayout>

### And should work as expected..
.....

Edit: Avoiding whitespace mis-matches:

To avoid whitespace mis-matching, replace literal spaces in your escaped regex with \s :

PS > $oldCode =  [Regex]::Escape(@"
>>
>>        </LinearLayout>
>>
>>
>>
>>     <TextView
>>         android:layout_weight="1"
>>         android:layout_width="wrap_content"
>>         android:layout_height="0dp"/>
>>
>>
>> </LinearLayout>
>> "@)
>>
PS > $oldcode
\n\ \ \ \ \ \ \ </LinearLayout>\n\n\n\n\ \ \ \ <TextView\n\ \ \ \ \ \ \ \ android:layout_weight="1"\n\ \ \ \ \ \ \ \ android:layout_width="wrap_content"\n\ \ \ \ \ \ \ \ android:layout_height="0dp"/>\n\n\n</LinearLayout>
PS >
PS > $BetterRegex = $oldcode -replace '(\\ ) ' , '\s '
PS > $BetterRegex
\n\s </LinearLayout>\n\n\n\n\s <TextView\n\s android:layout_weight="1"\n\s android:layout_width="wrap_content"\n\s android:layout_height="0dp"/>\n\n\n</LinearLayout>
PS > $newcode -match $betterregex
True
PS >

CodePudding user response:

The challenge with multi-line search strings is that there may be a mismatch in newline format (Windows-format CRLF vs. Unix-format LF-only newlines) between the multi-line search string literal and the content of the files:

  • A file may use either format, depending on how it was created.

  • A multi-line string literal uses the same format as that of the enclosing script file.

Note:

  • If all your input files use the same newline format, you may be able to get your code to work if you (re)save your script file with the same newline format as well.

  • Otherwise - if there's a mix of newline formats and/or your code must work on both Windows and non-Windows platforms with files that use platform-native newlines - the solution below is needed.


The solution is to replace the literal newlines in the search string, $oldCode, with regex \r?\n, which matches either newline format:

$oldCode =  @"

       </LinearLayout>



    <TextView
        android:layout_weight="1"
        android:layout_width="wrap_content"
        android:layout_height="0dp"/>


</LinearLayout>
"@ -replace '\r?\n', '\r?\n'

Note:

  • The -replace operation looks like a no-op, but actually replaces the literal newlines - be they CRLF or LF ones - with verbatim \r?\n, which - when later used as regexes with -replace or Select-String - again matches newlines of either format.

  • The above assumes that the multi-line search string either contains no regex metacharacters (which is the case here) or was deliberately constructed as a regex. If you want to treat the search string verbatim even if it contains regex metacharacters, more work is needed - see the bottom section.

Caveat re multi-line replacement strings:

  • Multi-line string literals using the enclosing script file's newline format implies that your replacement string too will use whatever newline format the script was saved with - which may differ from that of your input files.

  • You can control the replacement string's newline format by replacing its literal newlines with the newlines of interest; e.g.:

$newCode =  @"
        ANYTHING NEW

     </LinearLayout>



    <TextView
        android:layout_weight="1"
        android:layout_width="wrap_content"
        android:layout_height="0dp"/>


</LinearLayout>
"@ -split '\r?\n' -join "`n"
  • The above splits the multi-line strings into lines with -split '\r?\n' and re-joins (with -join) them with "`n", i.e. LF-only newlines.

  • Use "`r`n" for CRLF newlines, or [Environment]::NewLine for platform-native newlines.

  • To match the newline format of a given input file, read it with Get-Content -Raw and then use -match '\r\n' to look for the presence of at least one CRLF newline, along the lines of:

     $fileContent = Get-Content $name -Raw
     $newline = if ($fileContent -match '\r\n') { "`r`n" } else { "`n" }
     $fileContent -replace $oldCode ($newCode -split '\r?\n' -join $newline)
    

If the search string is to (always) be treated verbatim (apart from its newline format):

  • [regex]::Escape() is normally used to escape an arbitrary string that is to be treated verbatim by the .NET regex engine.

  • The challenge is that it escapes literal CR as \r and literal LF as \n, whereas the goal here is to represent all newline as \r?\n.

  • Robustly replacing all verbatim \r\n and stand-alone \n sequences in the result from [regex]::Escape() after the fact is non-trivial, notably if false positives must be ruled out; the solution below works around this challenge by first splitting the multi-line string into lines with -split '\r?\n', then escaping the lines individually, and then joining the escaped lines with literal \r?\n

$oldCode =  (@"

       </LinearLayout>



    <TextView
        android:layout_weight="1"
        android:layout_width="wrap_content"
        android:layout_height="0dp"/>


</LinearLayout>
"@ -split '\r?\n').ForEach({ [regex]::Escape($_) }) -join '\r?\n'
  • Related