How can we specify an offset position into PowerShell to replace this sketchy -replace
command.
Ansgar Wiechers' helpful answer addresses the offset question, and brianary's helpful answer shows a more PowerShell-idiomatic variant.
That said, it sounds like if you had a solution for replacing only the first occurrence of your search string, your original solution may work.
First-occurrence-only string replacement:
Unfortunately, neither PowerShell's -replace
operator nor .NET's String.Replace()
method offer limiting replacing to one occurrence (or a fixed number).
However, there is a workaround:
$hx = $hx -replace '(?s)123456(.*)', 'FFFFFF$1'
(?s)
is an inline regex option that makes regex metacharacter .
match newlines too.
(.*)
captures all remaining characters in capture group 1, and $1
in the replacement string references them, which effectively removes just the first occurrence. (See this answer for the more information about -replace
and the syntax of the replacement operand.)
General caveats:
If your search string happens to contain regex metacharacters that you want to be taken literally, \
-escape them individually or, more generally, pass the entire search term to [regex]::Escape()
.
If your replacement string happens to contain $
characters that you want to be taken literally, $
-escape them or, more generally, apply -replace '\$', '$$$$'
(sic) to it.
However, as iRon points out, while the above generically solves the replace-only-once problem, it is not a fully robust solution, because there is no guarantee that the search string will match at a byte boundary; e.g., single-byte search string 12
would match the middle 12
in 0123
, even though there is no byte 12
in the input string, composed of bytes 01
and 23
.
To address this ambiguity, the input "byte string" and the search string must be constructed differently: simply separate the digits constituting a byte each with spaces, as shown below.
Replacing byte sequences by search rather than fixed offsets:
Here's an all-PowerShell solution (PSv4+) that doesn't require third-party functionality:
Note:
As in your attempt, the entire file contents are read at once, and to-and- from string conversion is performed; PSv4+ syntax
To construct the search and replacement strings as "byte strings" with space-separated hex. representations created from byte-array input, use the same approach as for constructing the byte string from the input as shown below, e.g.:
(0x12, 0x34, 0x56, 0x1).ForEach('ToString', 'X') -join ' '
-> '12 34 56 1'
.ForEach('ToString', 'X')
is the equivalent of calling .ToString('X')
on each array element and collecting the results.
- If prefer each byte to be consistently represented as two hex digits, even for values less than
0x10
, (e.g., 01
rather than 1
), use 'X2'
, which increases memory consumption, however.
Also, you'll have to 0
-prefix single-digit byte values in the search string too, e.g.:
'12 34 56 01'
# Read the entire file content as a [byte[]] array.
# Note: Use PowerShell *Core* syntax.
# In *Windows PowerShell*, replace `-AsByteStream` with `-Encoding Byte`
# `-Raw` ensures that the file is efficiently read as [byte[]] array at once.
$byteArray = Get-Content C:\OldFile.exe -Raw -AsByteStream
# Convert the byte array to a single-line "byte string",
# where the whitespace-separated tokens are the hex. encoding of a single byte.
# If you want to guaranteed that even byte values < 0x10 are represented as
# *pairs* of hex digits, use 'X2' instead.
$byteString = $byteArray.ForEach('ToString', 'X') -join ' '
# Perform the replacement.
# Note that since the string is guaranteed to be single-line,
# inline option `(?s)` isn't needed.
# Also note how the hex-digit sequences representing bytes are also separated
# by spaces in the search and replacement strings.
$byteString = $byteString -replace '\b12 34 56\b(.*)', 'FF FF FF$1'
# Convert the byte string back to a [byte[]] array, and save it to the
# target file.
# Note how the array is passed as an *argument*, via parameter -Value,
# rather than via the pipeline, because that is much faster.
# Again, in *Windows PowerShell* use `-Encoding Byte` instead of `-AsByteStream`.
[byte[]] $newByteArray = -split $byteString -replace '^', '0x'
Set-Content "C:\NewFile.exe" -AsByteStream -Value $newByteArray