Home > Software design >  Discard the directory structure when extracting a zip archive with PowerShell
Discard the directory structure when extracting a zip archive with PowerShell

Time:12-01

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
  • Related