如何使用批处理文件压缩(/ zip)和解压缩(/ unzip)文件和文件夹,而不使用任何外部工具?

48

我知道这里有很多类似的问题,但我对答案(甚至是问题)并不完全满意。

主要目标是兼容性 - 它应适用于尽可能广泛的Windows机器(包括XPVistaServer 2003 - 这三者仍占据着Windows份额的约20%),并且生成的文件应可在Unix / Mac机器上使用(因此首选标准归档/压缩格式)。

选项有哪些:

  1. 创建一个实现某些压缩算法的批处理。显然,这是可能的——但只能使用单个文件,并使用CERTUTIL进行二进制处理(一些机器默认没有CERTUTIL,并且无法在Windows XP Home Edition上安装)。
  2. 通过WSH使用shell.application。这是我认为最好的选择。它允许压缩整个目录,并可用于每台Windows机器。
  3. Makecab——尽管其压缩不太便携,但它可用于每台Windows机器。一些外部程序(如7-Zip)能够提取.CAB内容,但当需要在Unix/Mac上使用文件时,这将不太方便。虽然压缩单个文件非常简单,但保留目录结构需要更多的努力。
  4. 使用.NET Framework——不是很好的选择。从.NET 2.0开始有GZipStream,但它只允许压缩单个文件。.NET 4.5具有Zip功能,但不支持Vista和XP。而且,.NET不会默认安装在XP和Server 2003上,但由于很可能拥有.NET 2.0到4.0,这是一个可考虑的选项。
  5. PowerShell——因为它依赖于.NET,所以具有相同的功能。它不会默认安装在XP、Server 2003和Vista上,所以我会跳过它。

3
您的 Shell.Application 方法是否有 2GB 大小限制?另外请注意,ZIP64 格式仅在 Vista 或更高版本Shell.Application 中得到支持。 - David Ruhmann
@DavidRuhmann - 我测试过的最大文件是1.5GB。很可能它的大小受到限制。我会进行测试并更新。 - npocmaka
1
我刚刚成功测试了我的Shell.Application解决方案,压缩包中包含我的XP模式虚拟硬盘目录,总共约6.2GB。它被压缩到2.44GB。然后我使用IZArc进行解压缩,所有内容都能够无误地提取出来,并且大小与原始文件相同。这是在Win7 x64上完成的。 - rojo
@rojo @DavidRuhmann - 在win8.1x64上,zip文件的限制似乎在8GB左右。 - npocmaka
1
所有当前支持的 Microsoft Windows 平台都已安装 PowerShell。Compress-ArchiveExpand-Archive - lit
6个回答

76

以下是答案:

1. 使用“纯”批处理脚本压缩/解压文件。

这要归功于Frank Westlake的 ZIP.CMDUNZIP.CMD(需要管理员权限并需要 FSUTILCERTUTIL)。对于Win2003和WinXP,需要安装2003 Admin Tool Pack,它将安装 CERTUTIL。请注意,ZIP.CMD的语法是反向的:

ZIP.CMD destination.zip source.file

同时它只能压缩单个文件。

2. 使用Shell.Application

我花了一些时间创建了一个单一的 jscript/batch 混合脚本,用于常见的文件和目录的压缩/解压 (以及其他一些功能)。这里是链接(它变得太大而不能在答案中发布)。 可以直接使用其带有.bat扩展名的文件,并且不会创建任何临时文件。 我希望帮助信息足够描述它如何使用。

以下是一些示例:

// unzip content of a zip to given folder.content of the zip will be not preserved (-keep no).Destination will be not overwritten (-force no)
call zipjs.bat unzip -source C:\myDir\myZip.zip -destination C:\MyDir -keep no -force no

// lists content of a zip file and full paths will be printed (-flat yes)
call zipjs.bat list -source C:\myZip.zip\inZipDir -flat yes

// lists content of a zip file and the content will be list as a tree (-flat no)
call zipjs.bat list -source C:\myZip.zip -flat no

// prints uncompressed size in bytes
zipjs.bat getSize -source C:\myZip.zip

// zips content of folder without the folder itself
call zipjs.bat zipDirItems -source C:\myDir\ -destination C:\MyZip.zip -keep yes -force no

// zips file or a folder (with the folder itslelf)
call zipjs.bat zipItem -source C:\myDir\myFile.txt -destination C:\MyZip.zip -keep yes -force no

// unzips only part of the zip with given path inside
call zipjs.bat unZipItem -source C:\myDir\myZip.zip\InzipDir\InzipFile -destination C:\OtherDir -keep no -force yes
call zipjs.bat unZipItem -source C:\myDir\myZip.zip\InzipDir -destination C:\OtherDir 

// adds content to a zip file
call zipjs.bat addToZip -source C:\some_file -destination C:\myDir\myZip.zip\InzipDir -keep no
call zipjs.bat addToZip -source  C:\some_file -destination C:\myDir\myZip.zip

在压缩过程中可能会出现一些已知问题:

  • 如果系统驱动器(通常为 C:)上没有足够的空间,脚本可能会产生各种错误,最常见的是脚本停止。这是因为 Shell.Application 活跃地使用%TEMP% 文件夹来压缩/解压缩数据。
  • 包含 Unicode 符号名称的文件夹和文件无法被 Shell.Application 对象处理。
  • Vista 及以上版本支持的生成的 zip 文件最大大小约为 8GB,XP/2003 版本支持的最大大小约为 2GB。

脚本将检测是否出现错误消息并停止执行,并告知可能的原因。目前我无法检测弹出窗口内的文本并给出失败的确切原因。

3. Makecab

压缩单个文件很容易 - makecab file.txt "file.cab"。最终可以增加MaxCabinetSize。 压缩文件夹需要为每个(子)目录和其中的文件使用DestinationDir指令(使用相对路径)。 以下是一个脚本:

;@echo off

;;;;; rem start of the batch part  ;;;;;
;
;for %%a in (/h /help -h -help) do ( 
;   if /I "%~1" equ "%%~a" if "%~2" equ "" (
;       echo compressing directory to cab file  
;       echo Usage:
;       echo(
;       echo %~nx0 "directory" "cabfile"
;       echo(
;       echo to uncompress use:
;       echo EXPAND cabfile -F:* .
;       echo(
;       echo Example:
;       echo(
;       echo %~nx0 "c:\directory\logs" "logs"
;       exit /b 0
;   )
; )
;
; if "%~2" EQU "" (
;   echo invalid arguments.For help use:
;   echo %~nx0 /h
;   exit /b 1
;)
;
; set "dir_to_cab=%~f1"
;
; set "path_to_dir=%~pn1"
; set "dir_name=%~n1" 
; set "drive_of_dir=%~d1"
; set "cab_file=%~2"
;
; if not exist %dir_to_cab%\ (
;   echo no valid directory passed
;   exit /b 1
;)

;
;break>"%tmp%\makecab.dir.ddf"
;
;setlocal enableDelayedExpansion
;for /d /r "%dir_to_cab%" %%a in (*) do (
;   
;   set "_dir=%%~pna"
;   set "destdir=%dir_name%!_dir:%path_to_dir%=!"
;   (echo(.Set DestinationDir=!destdir!>>"%tmp%\makecab.dir.ddf")
;   for %%# in ("%%a\*") do (
;       (echo("%%~f#"  /inf=no>>"%tmp%\makecab.dir.ddf")
;   )
;)
;(echo(.Set DestinationDir=!dir_name!>>"%tmp%\makecab.dir.ddf")
;   for %%# in ("%~f1\*") do (
;       
;       (echo("%%~f#"  /inf=no>>"%tmp%\makecab.dir.ddf")
;   )

;makecab /F "%~f0" /f "%tmp%\makecab.dir.ddf" /d DiskDirectory1=%cd% /d CabinetNameTemplate=%cab_file%.cab
;rem del /q /f "%tmp%\makecab.dir.ddf"
;exit /b %errorlevel%

;;
;;;; rem end of the batch part ;;;;;

;;;; directives part ;;;;;
;;
.New Cabinet
.set GenerateInf=OFF
.Set Cabinet=ON
.Set Compress=ON
.Set UniqueFiles=ON
.Set MaxDiskSize=1215751680;

.set RptFileName=nul
.set InfFileName=nul

.set MaxErrors=1
;;
;;;; end of directives part ;;;;;

示例用法:

call cabDir.bat ./myDir compressedDir.cab

进行解压缩可以使用EXPAND cabfile -F:* .。在Unix系统中,可以使用cabextract7zip进行提取。

.NET和GZipStream

我更喜欢使用Jscript.net,因为它可以与.bat文件无缝结合(没有有毒的输出和临时文件)。Jscript不允许将对象引用传递给函数,所以我发现唯一的使其工作的方法是逐字节读取/写入文件(因此我认为这不是最快的方法 - 该如何进行缓冲读取/写入?)同样只能用于单个文件。

@if (@X)==(@Y) @end /* JScript comment
@echo off
setlocal

for /f "tokens=* delims=" %%v in ('dir /b /s /a:-d  /o:-n "%SystemRoot%\Microsoft.NET\Framework\*jsc.exe"') do (
   set "jsc=%%v"
)

if not exist "%~n0.exe" (
    "%jsc%" /nologo /out:"%~n0.exe" "%~dpsfnx0"
)

 %~n0.exe %*

endlocal & exit /b %errorlevel%


*/


import System;
import System.Collections.Generic;
import System.IO;
import System.IO.Compression;


    
    function CompressFile(source,destination){
        var sourceFile=File.OpenRead(source);
        var destinationFile=File.Create(destination);
        var output = new  GZipStream(destinationFile,CompressionMode.Compress);
        Console.WriteLine("Compressing {0} to {1}.", sourceFile.Name,destinationFile.Name, false);
        var byteR = sourceFile.ReadByte();
        while(byteR !=- 1){
            output.WriteByte(byteR);
            byteR = sourceFile.ReadByte();
        }
        sourceFile.Close();
        output.Flush();
        output.Close();
        destinationFile.Close();
    }

    function UncompressFile(source,destination){
        var sourceFile=File.OpenRead(source);
        var destinationFile=File.Create(destination);
        
        var input = new GZipStream(sourceFile,
            CompressionMode.Decompress, false);
        Console.WriteLine("Decompressing {0} to {1}.", sourceFile.Name,
                destinationFile.Name);
        
        var byteR=input.ReadByte();
        while(byteR !== -1){
            destinationFile.WriteByte(byteR);
            byteR=input.ReadByte();
        }
        destinationFile.Close();
        input.Close();
        
        
    }
    
var arguments:String[] = Environment.GetCommandLineArgs();

    function printHelp(){
        Console.WriteLine("Compress and uncompress gzip files:");
        Console.WriteLine("Compress:");
        Console.WriteLine(arguments[0]+" -c source destination");
        Console.WriteLine("Uncompress:");
        Console.WriteLine(arguments[0]+" -u source destination");
        
        
    }

if (arguments.length!=4){
    Console.WriteLine("Wrong arguments");
    printHelp();
    Environment.Exit(1);
}

switch (arguments[1]){
    case "-c":
    
        CompressFile(arguments[2],arguments[3]);
        break;
    case "-u":
        UncompressFile(arguments[2],arguments[3]);
        break;
    default:
        Console.WriteLine("Wrong arguments");
        printHelp();
        Environment.Exit(1);
}

示例用法:

//zip
call netzip.bat -c my.file my.zip
//unzip
call netzip.bat -u my.zip my.file

5. TAR (仅适用于最新的Windows版本)

使用最新版本的Windows 10,现在我们有了TAR命令,尽管它不是最兼容的选项:

//compress directory
tar -cvf archive.tar c:\my_dir
//extract to dir
tar -xvf archive.tar.gz -C c:\data
//compres to zip format
tar -caf archive.zip c:\my_dir

2
这确实是一个很好的解决方案集合。我最喜欢第二个解决方案,使用 zipjs.bat。但是我对 zipjs.bat 有一个增强请求。在 ZIP 文件的 unzip 中,它会在 %TEMP% 中留下用于从 ZIP 文件中提取每个文件的临时子目录和文件。如果这个混合批处理文件自己删除它们,将会很好,可以参考我的答案:如何编写一个批处理文件,可以将文件解压缩到具有相同名称的新文件夹中? - Mofi
@Mofi - 我会检查这个问题。我不会在临时文件夹中创建zip文件,而是使用shell.application的方式。我还有两个关于zipjs.bat的请求,希望很快能有时间更新它。 - npocmaka
1
zipjs.bat 对我非常有效,感谢您提供这个脚本!只有选项-keep no似乎不可靠,有时源文件夹及其部分内容未被删除。我会在之后自己执行删除(RD)。 - johey
3
截至当前日期,ZIP.CMD和UNZIP.cmd的链接均返回404错误。我会尽力让翻译更加通俗易懂,但不会改变原意,也不会提供解释或其他内容。 - cybernard
1
ZIP.CMD和UNZIP.cmd的链接已经失效,但它们已经被分叉并可以在https://github.com/Klozz/CMD-scripts/blob/master/zip.cmd和https://github.com/Klozz/CMD-scripts/blob/master/unzip.cmd找到。 - SomethingDark
显示剩余7条评论

7

绝妙的解决方案!

makecab解决方案存在一些问题,因此这里提供了一个修复版本,解决了在使用带有空格的目录时出现的问题。

;@echo off

;;;;; rem start of the batch part  ;;;;;
;
;for %%a in (/h /help -h -help) do ( 
;   if /I "%~1" equ "%%~a" if "%~2" equ "" (
;       echo compressing directory to cab file  
;       echo Usage:
;       echo(
;       echo %~nx0 "directory" "cabfile"
;       echo(
;       echo to uncompress use:
;       echo EXPAND cabfile -F:* .
;       echo(
;       echo Example:
;       echo(
;       echo %~nx0 "c:\directory\logs" "logs"
;       exit /b 0
;   )
; )
;
; if "%~2" EQU "" (
;   echo invalid arguments.For help use:
;   echo %~nx0 /h
;   exit /b 1
;)
;
; set "dir_to_cab=%~f1"
;
; set "path_to_dir=%~pn1"
; set "dir_name=%~n1" 
; set "drive_of_dir=%~d1"
; set "cab_file=%~2"
; 
; if not exist "%dir_to_cab%\" (
;   echo no valid directory passed
;   exit /b 1
;)

;
;break>"%tmp%\makecab.dir.ddf"
;
;setlocal enableDelayedExpansion
;for /d /r "%dir_to_cab%" %%a in (*) do (
;   
;   set "_dir=%%~pna"
;   set "destdir=%dir_name%!_dir:%path_to_dir%=!"
;   (echo(.Set DestinationDir=!destdir!>>"%tmp%\makecab.dir.ddf")
;   for %%# in ("%%a\*") do (
;       (echo("%%~f#"  /inf=no>>"%tmp%\makecab.dir.ddf")
;   )
;)
;(echo(.Set DestinationDir=!dir_name!>>"%tmp%\makecab.dir.ddf")
;   for %%# in ("%~f1\*") do (
;       
;       (echo("%%~f#"  /inf=no>>"%tmp%\makecab.dir.ddf")
;   )

;makecab /F "%~f0" /f "%tmp%\makecab.dir.ddf" /d DiskDirectory1="%cd%" /d CabinetNameTemplate=%cab_file%.cab
;rem del /q /f "%tmp%\makecab.dir.ddf"
;exit /b %errorlevel%

;;
;;;; rem end of the batch part ;;;;;

;;;; directives part ;;;;;
;;
.New Cabinet
.set GenerateInf=OFF
.Set Cabinet=ON
.Set Compress=ON
.Set UniqueFiles=ON
.Set MaxDiskSize=1215751680;

.set RptFileName=nul
.set InfFileName=nul

.set MaxErrors=1
;;
;;;; end of directives part ;;;;;

4

使用Compress-ArchiveExpand-Archive的一些使用示例:

//zipping folder or file:
powershell "Compress-Archive -Path """C:\some_folder""" -DestinationPath """zippedFolder.zip""""

//unzipping folder:
powershell "Expand-Archive -Path """Draftv2.Zip""" -DestinationPath """C:\Reference""""

在当前支持的 Windows 版本中,这些命令行工具默认已安装。

使用 PowerShell 的另一种方法:

//zip directory
powershell "Add-Type -Assembly """System.IO.Compression.FileSystem""" ;[System.IO.Compression.ZipFile]::CreateFromDirectory("""C:\some_dir""", """some.zip""");"

//unzip directory
powershell "Add-Type -Assembly "System.IO.Compression.FileSystem" ;[System.IO.Compression.ZipFile]::ExtractToDirectory("""yourfile.zip""", """c:\your\destination""");"

我想要压缩一个文件夹/目录。这里有两个命令可以实现这个目的,但只有第二个命令对我有效。可能问题在于路径中包含了括号。 - Ray Woodcock

3

作为@IIde的回答,我改进了@npocmaka发布的makecab批处理脚本,并对其进行了一些进一步的改进。具体地,我添加了一个[-w / -wrapper]选项作为第三个参数,并更改了其默认行为。

原始脚本的工作方式是,将源目录包含在生成的cab文件中作为一个外部的"wrapper" / "container"。相反,我将收集到的源目录的递归内容从存档中的根目录开始放置。但是,如果您想保留嵌套,则可以将-w作为最后一个参数传递给我的版本。

此外,我将推荐的解压缩实用程序从EXPAND更改为EXTRAC32。前者在未将"wrapper"目录压缩到存档中时实现起来比较棘手,而后者可以轻松解压缩所有内容。(请注意,例如7-zip这样的GUI实用程序对于此目的甚至更容易!)

Dir2Cab.bat

;@echo off

;;;;; rem start of the batch part  ;;;;;
;
;for %%a in (/h /help -h -help) do ( 
;   if /I "%~1" equ "%%~a" if "%~2" equ "" (
;       echo compressing directory to cab file  
;       echo Usage:
;       echo(
;       echo %~nx0 "directory" "cabfile" [-w/-wrapper]
;       echo(
;       echo to uncompress use:
;       echo EXTRAC32 cabfile.cab /E /L .
;       echo(
;       echo Example:
;       echo(
;       echo %~nx0 "c:\directory\logs" "logs" 
;       exit /b 0
;   )
; )
;
; if "%~2" equ "" (
;   echo invalid arguments.For help use:
;   echo %~nx0 -h
;   exit /b 1
;)
;
; set "dir_to_cab=%~f1"
;
; set "path_to_dir=%~pn1"
; set "dir_name=%~n1" 
; set "drive_of_dir=%~d1"
; set "cab_file=%~2"
; 
; if not exist "%dir_to_cab%\" (
;   echo no valid directory passed
;   exit /b 1
;)

; :: Toggle the wrapper directory option
;set "wrapperdir="
;for %%a in (-w -wrapper) do ( 
;   if /I "%~3" equ "%%~a" set "wrapperdir=%dir_name%"
;)

;
;break>"%tmp%\makecab.dir.ddf"
;
;setlocal enableDelayedExpansion

;:: Generate a DDF file via a recursive directory listing
;for /d /r "%dir_to_cab%" %%a in (*) do (
;   
;   set "_dir=%%~pna"
;   set "destdir=%wrapperdir%!_dir:%path_to_dir%=!"
;   (echo(.Set DestinationDir=!destdir!>>"%tmp%\makecab.dir.ddf")
;   for %%# in ("%%a\*") do (
;       (echo("%%~f#"  /inf=no>>"%tmp%\makecab.dir.ddf")
;   )
;)
;(echo(.Set DestinationDir=!wrapperdir!>>"%tmp%\makecab.dir.ddf")
;   for %%# in ("%~f1\*") do (
;       (echo("%%~f#"  /inf=no>>"%tmp%\makecab.dir.ddf")
;   )

;:: Run makecab against the DDF file 
;makecab /F "%~f0" /f "%tmp%\makecab.dir.ddf" /d DiskDirectory1="%cd%" /d CabinetNameTemplate=%cab_file%.cab

;:: You might comment out this DDF file deletion for debugging purposes...
;del /q /f "%tmp%\makecab.dir.ddf"

;exit /b %errorlevel%

;;
;;;; rem end of the batch part ;;;;;

;;;; directives part ;;;;;
;;
.New Cabinet
.set GenerateInf=OFF
.Set Cabinet=ON
.Set Compress=ON
.Set UniqueFiles=ON
.Set MaxDiskSize=1215751680;

.set RptFileName=nul
.set InfFileName=nul

.set MaxErrors=1
;;
;;;; end of directives part ;;;;;

2

CAB.bat [输入] 文件夹或文件:打包 | .cab或.??_:解包 | 无:打包一个files子文件夹
还会在右键“发送到”菜单中添加一个CAB条目,方便处理
由于此脚本可以无缝执行两个任务,因此应该优先使用它,而不是丑陋的makecab - 如果你无论如何都要写入临时文件,为什么要使用混合脚本呢?

@echo off &echo. &set "ext=%~x1" &title CAB [%1] &rem input file or folder / 'files' folder / unpacks .cab .??_
if "_%1"=="_" if not exist "%~dp0files" echo CAB: No input and no 'files' directory to pack &goto :Exit "do nothing"
if "_%1"=="_" if exist "%~dp0files" call :CabDir "%~dp0files" &goto :Exit "input = none, use 'files' directory -pack" 
for /f "tokens=1 delims=r-" %%I in ("%~a1") do if "_%%I"=="_d" call :CabDir "%~f1" &goto :Exit "input = dir -pack"
if not "_%~x1"=="_.cab" if not "_%ext:~-1%"=="__" call :CabFile "%~f1" &goto :Exit "input = file -pack"
call :CabExtract "%~f1" &goto :Exit "input = .cab or .??_ -unpack" 
:Exit AveYo: script will add a CAB entry to right-click -- SendTo menu
if not exist "%APPDATA%\Microsoft\Windows\SendTo\CAB.bat" copy /y "%~f0" "%APPDATA%\Microsoft\Windows\SendTo\CAB.bat" >nul 2>nul
ping -n 6 localhost >nul &title cmd.exe &exit /b
:CabExtract %1:[.cab or .xx_]
echo %1 &pushd "%~dp1" &mkdir "%~n1" >nul 2>nul &expand -R "%~1" -F:* "%~n1" &popd &goto :eof
:CabFile %1:[filename]
echo %1 &pushd "%~dp1" &makecab /D CompressionType=LZX /D CompressionLevel=7 /D CompressionMemory=21 "%~nx1" "%~n1.cab" &goto :eof   
:CabDir %1:[directory]
dir /a:-D/b/s "%~1"
set "ddf="%temp%\ddf""
echo/.New Cabinet>%ddf%
echo/.set Cabinet=ON>>%ddf%
echo/.set CabinetFileCountThreshold=0;>>%ddf%
echo/.set Compress=ON>>%ddf%
echo/.set CompressionType=LZX>>%ddf%
echo/.set CompressionLevel=7;>>%ddf%
echo/.set CompressionMemory=21;>>%ddf%
echo/.set FolderFileCountThreshold=0;>>%ddf%
echo/.set FolderSizeThreshold=0;>>%ddf%
echo/.set GenerateInf=OFF>>%ddf%
echo/.set InfFileName=nul>>%ddf%
echo/.set MaxCabinetSize=0;>>%ddf%
echo/.set MaxDiskFileCount=0;>>%ddf%
echo/.set MaxDiskSize=0;>>%ddf%
echo/.set MaxErrors=1;>>%ddf%
echo/.set RptFileName=nul>>%ddf%
echo/.set UniqueFiles=ON>>%ddf%
setlocal enabledelayedexpansion
pushd "%~dp1"
for /f "tokens=* delims=" %%D in ('dir /a:-D/b/s "%~1"') do (
 set "DestinationDir=%%~dpD" &set "DestinationDir=!DestinationDir:%~1=!" &set "DestinationDir=!DestinationDir:~0,-1!"
 echo/.Set DestinationDir=!DestinationDir!;>>%ddf%
 echo/"%%~fD"  /inf=no;>>%ddf%
)
makecab /F %ddf% /D DiskDirectory1="" /D CabinetNameTemplate=%~nx1.cab &endlocal &popd &del /q /f %ddf% &goto :eof

1

使用makeself, certutilbusybox创建的工作原型:

https://github.com/hemnstill/makeself/releases/tag/release-2.4.5-cmd

优点:
- 单个`.bat`文件可在Windows 2016+和许多Linux上运行。 - 令人印象深刻的命令行参数,潜在支持不同的存档格式。 - 没有外部依赖,只使用`certutil.exe`来提取`busybox.exe`。
缺点:
- 负载大小约为900kb(可以通过upx稍微减小)。 - virustotal结果:9/56(当使用certutil.exe来提取负载时,防病毒软件不喜欢)。 - `busybox.exe`不支持makeself cli的所有功能。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接