A generalized solution supporting multiple files, building on Ansgar Wiechers' great, memory-efficient System.IO.StreamReader
solution:
PowerShell's ability to invoke members (properties, methods) directly on a collection and have them automatically invoked on all items in the collection (member-access enumeration, v3+) allows for easy generalization:
# The input file paths.
$files = 'file1', 'file2', 'file3'
# Create stream-reader objects for all input files.
# Note: Convert-Path converts the $files elements to *full paths*, which is
# necessary, because .NET's current dir. usually differs from PowerShell's.
$readers = [IO.StreamReader[]] (Convert-Path -LiteralPath $files)
# Keep reading while at least 1 file still has more lines.
while ($readers.EndOfStream -contains $false) {
# Read the next line from each stream (file).
# Streams that are already at EOF fortunately just return "".
$lines = $readers.ReadLine()
# Output the lines separated with tabs.
$lines -join "`t"
}
# Close the stream readers.
$readers.Close()
Get-MergedLines
(source code below; invoke with -?
for help) wraps the functionality in a function that:
accepts a variable number of filenames - both as an argument and via the pipeline
uses a configurable separator to join the lines (defaults to a tab)
allows trimming trailing separator instances
function Get-MergedLines() {
<#
.SYNOPSIS
Merges lines from 2 or more files with a specifiable separator (default is tab).
.EXAMPLE
Get-MergedLines file1, file2 '<->'
.EXAMPLE
Get-ChildItem file? | Get-MergedLines
#>
param(
[Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName)]
[Alias('PSPath')]
[string[]] $Path,
[string] $Separator = "`t",
[switch] $TrimTrailingSeparators
)
begin { $allPaths = @() }
# Note: += to "grow" arrays is generally best avoided, given
# that a new array must be created every time; for *small*
# arrays, however, this method is convenient, without noticeably
# impacting performance.
process { $allPaths += $Path }
end {
# Resolve all paths to full paths, which may include wildcard resolution.
# Note: By using full paths, we needn't worry about .NET's current dir.
# potentially being different.
$fullPaths = (Resolve-Path $allPaths).ProviderPath
# Create stream-reader objects for all input files.
$readers = [System.IO.StreamReader[]] $fullPaths
# Keep reading while at least 1 file still has more lines.
while ($readers.EndOfStream -contains $false) {
# Read the next line from each stream (file).
# Streams that are already at EOF fortunately just return "".
$lines = $readers.ReadLine()
# Join the lines.
$mergedLine = $lines -join $Separator
# Trim (remove) trailing separators, if requested.
if ($TrimTrailingSeparators) {
$mergedLine = $mergedLine -replace ('^(.*?)(?:' + [regex]::Escape($Separator) + ')+$'), '$1'
}
# Output the merged line.
$mergedLine
}
# Close the stream readers.
$readers.Close()
}
}
$null
; in the context of string concatenation, this is equivalent to the empty string, so oncefile2
has run out of lines, you'll simply get nothing after the"`t"
. – Rubefaction