Wes Ellis./ a personal notebook
Technology. Stories. Side projects.
A few things worth writing down.
← Back to Script Library

SCRIPT LIBRARY · POWERSHELL

Convert 7z Archives to ZIP So Anyone Can Open Them

Repack .7z files as plain Deflate ZIPs with UTF-8 file names, including password-protected ones, and test every result before the original goes anywhere.

AT A GLANCEConvert-7zToZip.ps1
What it does
Extracts each .7z (with a password if you give it one), repacks the contents as a standard Deflate ZIP with UTF-8 file names, tests the new ZIP, and moves it into place. The .7z stays unless you ask for it to go.
Requires
  • PowerShell 7+ or Windows PowerShell 5.1
  • 7-Zip (7z.exe)
Permissions
Read access to the .7z files and write access to the destination. No admin rights.
Runs on
Windows 10/11, Windows Server 2016+
Tested
Parse-checked and run against real plain and password-protected .7z files with 7-Zip 23.01 in PowerShell 7.4, including a wrong password, -WhatIf and -RemoveSource

Part 2 of the thread Archive conversion workshop

7z is a great format right up until you send one to someone. Then you get the email: "I can't open this." Current builds of Windows 11 can open 7z in Explorer now, but plenty of machines, phones, upload forms and older tools still only speak ZIP.

This is part two of the archive series. Part one was the general test-and-convert tool; this one is only about turning .7z into the most boring, compatible ZIP possible. That means Deflate compression (not LZMA or anything fancy that some unzippers choke on) and UTF-8 file names, so a file called Résumé.docx doesn't arrive as R+¬sum+¬.docx on a Mac.

It also handles the other thing that trips people up: encrypted .7z files. Pass -Password as a secure string and it'll open them. The ZIP it produces is not encrypted, on purpose; see the gotchas. The original script from this post also changed directories with Set-Location mid-loop, so one failed extraction left the rest of the run working in the wrong folder. That's gone.

Convert-7zToZip.ps1Download
<#
.SYNOPSIS
    Repacks .7z archives as plain ZIP files that anything can open.
.DESCRIPTION
    Extracts each .7z to a private temp folder, builds a Deflate ZIP with UTF-8 file names,
    tests the new ZIP with 7-Zip, and only then moves it into place. The .7z is kept unless
    you pass -RemoveSource. Works with password-protected .7z files if you supply the password.
    Supports -WhatIf.
.PARAMETER Path
    .7z files, or folders to search for them. Accepts pipeline input.
.PARAMETER Recurse
    Search subfolders when Path is a folder.
.PARAMETER Destination
    Folder for the new ZIP files. Defaults to the same folder as each .7z.
.PARAMETER Password
    Password for encrypted .7z files. The ZIP that comes out is NOT encrypted.
.PARAMETER CompressionLevel
    Deflate level passed to 7-Zip as -mx: 1 (fastest) to 9 (smallest). Default 5.
.PARAMETER RemoveSource
    Delete each .7z after its ZIP has been built and tested.
.PARAMETER Force
    Overwrite a ZIP that already exists at the destination.
.PARAMETER SevenZipPath
    Full path to 7z.exe. If omitted, the script checks PATH, then the usual Program Files folders.
.EXAMPLE
    .\Convert-7zToZip.ps1 -Path .\handoff -WhatIf
.EXAMPLE
    .\Convert-7zToZip.ps1 -Path .\vendor-drop.7z -Password (Read-Host -AsSecureString 'Password')
#>
[CmdletBinding(SupportsShouldProcess)]
param(
    [Parameter(ValueFromPipeline, ValueFromPipelineByPropertyName)]
    [Alias('FullName')]
    [string[]]$Path = '.',
    [switch]$Recurse,
    [string]$Destination,
    [securestring]$Password,
    [ValidateSet(1, 3, 5, 7, 9)][int]$CompressionLevel = 5,
    [switch]$RemoveSource,
    [switch]$Force,
    [string]$SevenZipPath
)

begin {
    if (-not $SevenZipPath) {
        $SevenZipPath = @(
            Get-Command -Name 7z.exe, 7z -CommandType Application -ErrorAction SilentlyContinue | Select-Object -First 1 -ExpandProperty Source
            if ($env:ProgramFiles) { Join-Path $env:ProgramFiles '7-Zip\7z.exe' }
            if (${env:ProgramFiles(x86)}) { Join-Path ${env:ProgramFiles(x86)} '7-Zip\7z.exe' }
        ) | Where-Object { $_ -and (Test-Path -LiteralPath $_) } | Select-Object -First 1
    }
    if (-not $SevenZipPath -or -not (Test-Path -LiteralPath $SevenZipPath)) {
        throw '7-Zip not found. Install it from 7-zip.org or pass -SevenZipPath.'
    }
    Write-Verbose "Using 7-Zip at $SevenZipPath"

    # Only unwrapped for the 7z.exe call itself.
    $passArg = if ($Password) { '-p' + [Net.NetworkCredential]::new('', $Password).Password }

    function Invoke-SevenZip([string[]]$Arguments) {
        $out = & $SevenZipPath @Arguments 2>&1
        if ($LASTEXITCODE -ge 2) {
            $errText = $out | Where-Object { $_ -is [System.Management.Automation.ErrorRecord] } | ForEach-Object { "$_".Trim() } | Where-Object { $_ -match '\w' -and $_ -notmatch 'RemoteException|^ERRORS:$' -and $_ -ne $Arguments[-1] }
            throw "7-Zip exit code ${LASTEXITCODE}: $(($errText | Select-Object -Unique) -join '; ')"
        }
        if ($LASTEXITCODE -eq 1) { Write-Warning "7-Zip reported warnings for $($Arguments[-1])" }
    }
}

process {
    $archives = foreach ($item in $Path) {
        if (Test-Path -LiteralPath $item -PathType Container) {
            Get-ChildItem -LiteralPath $item -Filter *.7z -File -Recurse:$Recurse
        }
        else { Get-Item -LiteralPath $item }
    }

    foreach ($archive in $archives) {
        $outDir = if ($Destination) { $Destination } else { $archive.DirectoryName }
        $target = Join-Path $outDir ($archive.BaseName + '.zip')
        $result = [ordered]@{ Name = $archive.Name; SevenZipKB = [math]::Round($archive.Length / 1KB); ZipKB = $null; Files = $null; Status = $null; Target = $target }

        if ((Test-Path -LiteralPath $target) -and -not $Force) {
            $result.Status = 'SkippedExists'
            [pscustomobject]$result
            continue
        }
        if (-not $PSCmdlet.ShouldProcess($archive.FullName, "Repack as $target")) { continue }

        $work = Join-Path ([IO.Path]::GetTempPath()) ('7z2zip-' + [guid]::NewGuid().ToString('N'))
        try {
            $files = Join-Path $work 'files'
            $null = New-Item -ItemType Directory -Path $files -Force
            $extractArgs = @('x', '-y', '-bd', "-o$files")
            if ($passArg) { $extractArgs += $passArg }
            Invoke-SevenZip ($extractArgs + $archive.FullName)

            $result.Files = @(Get-ChildItem -LiteralPath $files -File -Recurse -Force).Count
            if (-not $result.Files) { throw 'Archive contains no files.' }

            # Deflate + UTF-8 names = the ZIP flavor that Explorer, macOS and most tools agree on.
            $tempZip = Join-Path $work 'out.zip'
            Invoke-SevenZip @('a', '-tzip', '-mm=Deflate', "-mx=$CompressionLevel", '-mcu=on', '-y', '-bd', $tempZip, (Join-Path $files '*'))
            Invoke-SevenZip @('t', '-bd', $tempZip)

            if (-not (Test-Path -LiteralPath $outDir)) { $null = New-Item -ItemType Directory -Path $outDir }
            $result.ZipKB = [math]::Round((Get-Item -LiteralPath $tempZip).Length / 1KB)
            Move-Item -LiteralPath $tempZip -Destination $target -Force -WhatIf:$false
            $result.Status = 'Converted'

            if ($RemoveSource -and $PSCmdlet.ShouldProcess($archive.FullName, 'Delete original .7z')) {
                Remove-Item -LiteralPath $archive.FullName -Force
                $result.Status = 'ConvertedRemoved7z'
            }
        }
        catch {
            $result.Status = "Failed: $($_.Exception.Message)"
            Write-Warning "$($archive.Name): $($_.Exception.Message)"
        }
        finally {
            Remove-Item -LiteralPath $work -Recurse -Force -ErrorAction SilentlyContinue -WhatIf:$false
        }
        [pscustomobject]$result
    }
}

Parameters

ParameterTypeDefaultWhat it's for
-Pathstring[]..7z files, or folders to search for them. Takes pipeline input.
-Recurseswitch—Search subfolders when Path is a folder.
-Destinationstring—Where the ZIPs go. Defaults to next to each .7z.
-Passwordsecurestring—Password for encrypted .7z files. Use Read-Host -AsSecureString; don't type it into the command line.
-CompressionLevelint5Deflate level, 1 to 9. Above 5 costs a lot of time for very little gain with Deflate.
-RemoveSourceswitch—Delete each .7z once its ZIP has been built and tested. Honors -WhatIf.
-Forceswitch—Overwrite a ZIP that already exists at the destination.
-SevenZipPathstring—Full path to 7z.exe. Leave it off and the script checks PATH, then Program Files and Program Files (x86).

Run it

See what it would do in a folder you're about to send somewhere.

.\Convert-7zToZip.ps1 -Path .\handoff -WhatIf

Open an encrypted .7z and hand over a plain ZIP.

.\Convert-7zToZip.ps1 -Path .\project-files.7z -Password (Read-Host -AsSecureString 'Archive password')

Convert a whole tree into a separate folder and clear out the .7z files afterwards.

.\Convert-7zToZip.ps1 -Path D:\Exports -Recurse -Destination D:\Exports-zip -RemoveSource

Only report the ones that failed.

.\Convert-7zToZip.ps1 -Path D:\Exports -Recurse | Where-Object Status -like 'Failed*'

What you'll see

Example outputvalues are illustrative
WARNING: q3-reports.7z: 7-Zip exit code 2: ERROR: D:\Exports\q3-reports.7z; Cannot open encrypted archive. Wrong password?

Name               SevenZipKB ZipKB Files Status             Target
----               ---------- ----- ----- ------             ------
project-files.7z        18344 24109   212 Converted          D:\Exports\project-files.zip
q3-reports.7z            2210        Failed: 7-Zip exit ...  D:\Exports\q3-reports.zip
site-survey.7z          51002 55870    64 ConvertedRemoved7z D:\Exports\site-survey.zip
pc0142-logs.7z            930       SkippedExists           D:\Exports\pc0142-logs.zip

How it works

  1. Find 7-Zip. -SevenZipPath if you passed it, otherwise 7z on PATH, otherwise Program Files\7-Zip\7z.exe (and the x86 folder). No 7-Zip, no run.
  2. Extract to a private temp folder. Each .7z gets its own GUID-named folder under %TEMP%. If you supplied a password, it's unwrapped from the secure string only for that 7z.exe call.
  3. Count what came out. An archive that extracts to zero files is reported as a failure instead of producing an empty ZIP.
  4. Build the compatible ZIP. -tzip -mm=Deflate -mcu=on gives you standard Deflate compression and UTF-8 names. Then 7z t tests it.
  5. Swap it in, then maybe delete. The tested ZIP is moved to its real name. -RemoveSource deletes the .7z only after that, and only through ShouldProcess, so -WhatIf covers it. The temp folder goes away in a finally block no matter what happened.

Take it further

  • Going the other way? Part three repacks ZIPs as 7z and throws the result away if it doesn't actually save space.
  • Make it a right-click. Drop a shortcut into shell:sendto whose target is pwsh.exe -File C:\Scripts\Convert-7zToZip.ps1 -Path. Explorer tacks the file you picked onto the end, and you'll never have to explain 7z to anyone again.
  • Split big ones. Some upload forms cap file size. Adding -v2g to the a arguments makes 7-Zip write 2 GB volumes, but multi-part ZIPs are less friendly than one file, so only do it when you have to.

Things that'll trip you up

  • The ZIP will be bigger. LZMA2 in a .7z beats Deflate by a fair margin, especially on text and logs. A 20 to 40 percent jump is normal. That's the price of compatibility.
  • Encrypted .7z files without -Password. 7-Zip will stop and ask for the password. If you're watching, you can type it. In a scheduled task there's nobody to answer, so that file fails and the script moves on.
  • The output ZIP isn't password-protected. That's deliberate. ZIP's classic ZipCrypto encryption is weak, and AES-encrypted ZIPs won't open in Windows Explorer, which defeats the point of converting. If the contents need protecting in transit, use a proper file-sharing link with access controls instead.
  • The password is briefly on 7z.exe's command line. 7-Zip only accepts it as a -p argument, so while 7z.exe is running, anyone who can list processes on that machine could see it. Fine on your own workstation; think twice on a shared server.
  • -Force replaces, it doesn't merge. 7-Zip's own 'a' command adds to an existing archive, which is how you end up with stale files in a ZIP. The script builds each ZIP fresh in a temp folder and then swaps it in, so an overwrite really is a clean replacement.