In other words, does PowerShell's Expand-Archive
have an equivalent to unzip
's -j
command-line argument? If not, are there alternatives on Windows?
I have tried Expand-Archive -Path thing.zip -DestinationPath "somepath" -Force
, which just puts the directory structure in another folder called somepath
.
CodePudding user response:
This function will do what you want, obviously handling of possible file collision is not implemented, up to you how you want to implement that. Currently, if a file already exists with the same name it will give you an error and skip it. The function is a simplified version of the one from this answer which does actually keep the folder structure.
If no argument is passed to the -DestinationPath
parameter, the zip entries will be extracted to the current location.
using namespace System.IO
using namespace System.IO.Compression
function Expand-ZipArchive {
[CmdletBinding(DefaultParameterSetName = 'Path')]
param(
[Parameter(ParameterSetName = 'Path', Mandatory, Position = 0, ValueFromPipeline, ValueFromPipelineByPropertyName)]
[string] $Path,
[Parameter(ParameterSetName = 'LiteralPath', Mandatory, ValueFromPipelineByPropertyName)]
[Alias('PSPath')]
[string] $LiteralPath,
[Parameter()]
[string] $DestinationPath
)
begin {
Add-Type -AssemblyName System.IO.Compression
$DestinationPath = $PSCmdlet.GetUnresolvedProviderPathFromPSPath($DestinationPath)
}
process {
$arguments = switch($PSCmdlet.ParameterSetName) {
Path { $Path, $false, $false }
LiteralPath { $LiteralPath, $false, $true }
}
$null = [Directory]::CreateDirectory($DestinationPath)
foreach($item in $ExecutionContext.InvokeProvider.Item.Get.Invoke($arguments)) {
try {
$fileStream = $item.Open([FileMode]::Open)
$zipArchive = [ZipArchive]::new($fileStream, [ZipArchiveMode]::Read)
foreach($entry in $zipArchive.Entries) {
try {
# if it's a folder, exclude it
if(-not $entry.Name) {
continue
}
$path = [Path]::Combine($DestinationPath, $entry.Name)
# will throw if a file with same name exists, intended
# error handling should be implemented in `catch` block
$fs = [FileStream]::new($path, [FileMode]::CreateNew)
$wrappedStream = $entry.Open()
$wrappedStream.CopyTo($fs)
}
catch {
$PSCmdlet.WriteError($_)
}
finally {
$fs, $wrappedStream | ForEach-Object Dispose
}
}
}
catch {
$PSCmdlet.WriteError($_)
}
finally {
$zipArchive, $fileStream | ForEach-Object Dispose
}
}
}
}
Expand-ZipArchive .\myZip.zip
CodePudding user response:
As a PowerShell-only way you could extract the archive to a temporary directory and then move the files to the final location, discarding directory structure.
$archiveName = 'test.zip'
$destination = 'test'
# Create temp path as a sub directory of actual destination path, so the files don't
# need to be moved (potentially) across drives.
$destinationTemp = Join-Path $destination "~$((New-Guid).ToString('n'))"
# Create temp directory
$null = New-Item $destinationTemp -ItemType Dir
# Extract to temp dir
Expand-Archive $archiveName -DestinationPath $destinationTemp
# Move files from temp dir to actual destination, discarding directory structure
Get-ChildItem $destinationTemp -File -Recurse | Move-Item -Destination $destination
# Remove temp dir
Remove-Item $destinationTemp -Recurse -Force
With PowerShell 7 , you could even move each file immediately after extraction, using the new -PassThru
switch of Expand-Archive
:
$archiveName = 'test.zip'
$destination = 'test'
# Create temp path as a sub directory of actual destination path, so the files don't
# need to be moved (potentially) across drives.
$destinationTemp = Join-Path $destination "~$((New-Guid).ToString('n'))"
# Create temp directory
$null = New-Item $destinationTemp -ItemType Dir
# Expand to temp dir and move to final destination, discarding directory structure
Expand-Archive $archiveName -DestinationPath $destinationTemp -PassThru |
Where-Object -not PSIsContainer | Move-Item -Destination $destination
# Remove temp dir
Remove-Item $destinationTemp -Recurse -Force