Xir
Xir

Reputation: 13

Replacing a String In Multiple Files With Their Filename

So I have a whole large folder of text files where I'd like to change the string unamed in each one of them to the name of the file itself, extension excluded.

So far with my scavengous skills I have come up with the following, but it doesn't give what I'm looking for as it changes the name to every single collected file

$path = "*.txt"
$filename = Get-Item $path | Select-Object -ExpandProperty BaseName
$Change = Get-Content $path
$Change | ForEach-Object {$_ -Replace "unamed", $filename} | Set-Content $path

So basically something like this

The name of this file is unamed.

is becoming

The name of this file is File1Name File2Name File3Name.

There is also a side issue I'm experiencing where the results of the all the files merges into one file (so contents of file 1 will now also contain contents of all the other files too). How can I get the results I seek oh wise ones?

If there is a way to settle this with regular command line instead of powershell then I'll settle for that too (I found this but had issue with replace_string being invalid command? Batch script that replaces static string in file with filename)

Upvotes: 1

Views: 916

Answers (2)

mklement0
mklement0

Reputation: 440471

Esperanto's answer works well and is PSv2-compatible; here's a much faster - but more memory-intensive - PSv3+ solution:

Get-Content -Raw *.txt | ForEach-Object {
  $_ -replace 'unamed', [IO.Path]::GetFileNameWithoutExtension($_.PSPath) |
    Set-Content $_.PSPath
}
  • Unless you have special file-matching needs such as looking for files recursively or needing to exclude files or needing to match by file attributes, you don't need Get-Item / Get-ChildItem to enumerate files of interest - you can pass a wildcard pattern such as *.txt directly to Get-Content's (implied) -Path parameter.

  • Get-Content -Raw (PSv3+) reads the the entire content of each file matching *.txt into memory as a whole, as a single string.

    • Note: Because each file is read as a whole before it is output to the pipeline, this solution may not work with very large files, depending on available memory.
  • Get-Content decorates the strings it outputs with information about the where the string came from, so that the .PSPath property on each string returned contains the full filename of the file the string was read from.

  • [IO.Path]::GetFileNameWithoutExtension($_.PSPath) extracts the filename root from the path (the mere filename without extension)

    • The direct use of .NET is necessary, unfortunately, the Split-Path cmdlet doesn't allow you to do this in Windows PowerShell, but in PowerShell Core you could use Split-Path -LeafBase instead.

Caveat: Set-Content uses a default encoding (Windows PowerShell: "ANSI"; PowerShell Core: UTF-8 without BOM), which may or may not be the same as the input encoding; use the -Encoding parameter as needed.


As for what you tried:

$filename = Get-Item $path | Select-Object -ExpandProperty BaseName

$filename now contains an array of filename roots (base names).

$Change = Get-Content $path

$Change now contains a single array whose elements are the lines from all input files.

$Change | ForEach-Object {$_ -Replace "unamed", $filename} | Set-Content $path

  • Because $Change is a single, flat array, you've lost the partitioning of the array elements into the files or origin, and Set-Content sends all lines to the target file(s).

  • Because $filename is an array whereas the 2nd RHS operand of -replace only supports a scalar (single string), $_ -replace "unamed", $filename causes $filename to be converted to a single string, which is a space-separated concatenation of the array elements (filename roots).

  • Set-Content $Path so happens to write the flat (modified) array of lines to multiple files, namely those existing files that match the wildcard expression in $Path, *.txt:

    • As stated, the input Set-Content receives is a concatenation of the input files' lines, so all output files receive the concatenated (modified) content from all input files (all files whose names match wildcard pattern *.txt)

    • As an aside: That Set-Content *.txt allows writing the same content to multiple preexisting output files is a surprising and possibly destructive feature and should arguably not be supported - see this GitHub discussion.

Upvotes: 0

Esperento57
Esperento57

Reputation: 17492

Something like this?

Get-ChildItem -Filter "*.txt" | ForEach-Object {
  $Path=$_.FullName; (Get-Content $Path) -replace 'unamed', $_.BaseName | Set-Content $Path 
}

Upvotes: 2

Related Questions