" 2026 Apr 14 by Vim Project: Detect more path traversal attacks on Windows
" 2026 Apr 15 by Vim Project: Detect more path traversal attacks on Windows
" 2026 Jun 20 by Vim Project: Fix wrong escaping for the powershell calls
+" 2026 Jul 25 by Vim Project: Improved Compatibility for powershell 5 and pwsh 7
" License: Vim License (see vim's :help license)
" Copyright: Copyright (C) 2005-2019 Charles E. Campbell {{{1
" Permission is hereby granted to use and distribute this code,
if !exists("g:zip_extractcmd")
let g:zip_extractcmd= g:zip_unzipcmd
endif
+if !exists("g:zip_pwsh")
+ let g:zip_pwsh=''
+endif
" ---------------------------------------------------------------------
" required early
call s:Mess('WarningMsg', "***warning*** this version of zip needs vim 9.1 or later")
finish
endif
-" sanity checks
-if !executable(g:zip_unzipcmd) && &shell !~ 'pwsh'
- call s:Mess('Error', "***error*** (zip#Browse) unzip not available on your system")
- finish
+
+" ---------------------------------------------------------------------
+" Compatibility checks
+" PowerShell: {{{2
+" Existence of powershell always means a Windows OS. By default the
+" `zip`/`unzip` is not installed on Windows, but the runtime of
+" powershell support relative functionalities about zip compression
+" and extraction.
+" In the following implementations we'll use some powershell scripts
+" to manipulate zip files. All these scripts should available to both
+" Windows PowerShell 5 and pwsh 7+.
+
+let s:ps = ''
+" order: `g:zip_pwsh` > `&shell`, with executable check
+if &shell =~# 'powershell' || g:zip_pwsh =~# 'powershell'
+ if executable('powershell')
+ let s:ps = 'powershell'
+ endif
endif
-if !dist#vim#IsSafeExecutable('zip', g:zip_unzipcmd) && &shell !~ 'pwsh'
- call s:Mess('Error', "Warning: NOT executing " .. g:zip_unzipcmd .. " from current directory!")
- finish
+if (empty(g:zip_pwsh) && &shell =~# 'pwsh') || g:zip_pwsh =~# 'pwsh'
+ if executable('pwsh')
+ let s:ps = 'pwsh'
+ endif
endif
+fun! s:isPS()
+ return !empty(s:ps)
+endfun
+
+" ---------------------------------------------------------------------
+" sanity checks
+" s:SafeExecutable: {{{2
+fun! s:SafeExecutable(exe)
+ if !executable(a:exe) && !s:isPS()
+ call s:Mess('Error', "***error*** (zip) '".a:exe."' not available on your system")
+ return v:false
+ endif
+ if !dist#vim#IsSafeExecutable('zip', a:exe) && !s:isPS()
+ call s:Mess('Error', "Warning: NOT executing " .. a:exe .. " from current directory!")
+ return v:false
+ endif
+ return v:true
+endfun
+" guarantee default command is exist and not be injected by environment
+" every default command should be checked
+if !s:SafeExecutable(g:zip_zipcmd) | finish | endif
+if !s:SafeExecutable(g:zip_unzipcmd) | finish | endif
+if !s:SafeExecutable(g:zip_extractcmd) | finish | endif
+
" ----------------
" PowerShell: {{{1
" ----------------
function! s:TryExecGnuFallBackToPs(executable, gnu_func_call, ...)
" Check that a gnu executable is available, run the gnu_func_call if so. If
" the gnu executable is not available or if gnu_func_call fails, try
- " ps_func_call if &shell =~ 'pwsh'. If all attempts fail, print errors.
- " a:executable - one of (g:zip_zipcmd, g:zip_unzipcmd, g:zip_extractcmd)
+ " ps_func_call if `s:isPS()`. If all attempts fail, print errors.
+ " a:executable - (string) name of the executable program
" a:gnu_func_call - (string) a gnu function call to execute
" a:1 - (optional string) a PowerShell function call to execute.
let failures = []
else
call add(failures, a:executable.' not available on your system')
endif
- if &shell =~ 'pwsh' && a:0 == 1
+ if s:isPS() && a:0 == 1
try
exe a:1
return
catch
- call add(failures, 'Fallback to PowerShell attempted but failed')
+ call add(failures, 'Fallback to PowerShell attempted but failed: '.a:1)
endtry
endif
for msg in failures
" Browse the contents of a zip file using PowerShell's
" Equivalent `unzip -Z1 -- zipfile`
let cmds = [
+ \ 'Add-Type -AssemblyName System.IO.Compression.FileSystem;',
\ '$zip = [System.IO.Compression.ZipFile]::OpenRead(' . s:PSEscape(a:zipfile) . ');',
\ '$zip.Entries | ForEach-Object { $_.FullName };',
\ '$zip.Dispose()'
\ ]
- return 'pwsh -NoProfile -Command ' . s:Escape(join(cmds, ' '), 1)
+ return s:ps . ' -NoProfile -Command ' . s:Escape(join(cmds, ' '), 1)
endfunction
function! s:ZipReadPS(zipfile, fname, tempfile)
" Read a filename within a zipped file to a temporary file.
" Equivalent to `unzip -p -- zipfile fname > tempfile`
- if &shell =~ 'pwsh'
+ if s:isPS()
call s:Mess('WarningMsg', "***warning*** PowerShell can display, but cannot update, files in archive subfolders")
endif
let cmds = [
+ \ 'Add-Type -AssemblyName System.IO.Compression.FileSystem;',
\ '$zip = [System.IO.Compression.ZipFile]::OpenRead(' . s:PSEscape(a:zipfile) . ');',
\ '$fileEntry = $zip.Entries | Where-Object { $_.FullName -eq ' . s:PSEscape(a:fname) . ' };',
\ '$stream = $fileEntry.Open();',
\ '$stream.Close();',
\ '$zip.Dispose()'
\ ]
- return 'pwsh -NoProfile -Command ' . s:Escape(join(cmds, ' '))
+ return s:ps . ' -NoProfile -Command ' . s:Escape(join(cmds, ' '))
endfunction
function! s:ZipUpdatePS(zipfile, fname)
" Update a filename within a zipped file
" Equivalent to `zip -u zipfile fname`
- if &shell =~ 'pwsh' && a:fname =~ '/'
+ if s:isPS() && a:fname =~ '/'
call s:Mess('Error', "***error*** PowerShell cannot update files in archive subfolders")
return ':'
endif
return ':'
endif
let cmds = [
+ \ 'Add-Type -AssemblyName System.IO.Compression.FileSystem;',
\ '$zip = [System.IO.Compression.ZipFile]::OpenRead(' . s:PSEscape(a:zipfile) . ');',
\ '$fileEntry = $zip.Entries | Where-Object { $_.FullName -eq ' . s:PSEscape(a:fname) . ' };',
\ '$stream = $fileEntry.Open();',
\ '$stream.Close();',
\ '$zip.Dispose()'
\ ]
- return 'pwsh -NoProfile -Command ' . s:Escape(join(cmds, ' '))
+ return s:ps . ' -NoProfile -Command ' . s:Escape(join(cmds, ' '))
endfunction
function! s:ZipDeleteFilePS(zipfile, fname)
\ 'if ($entry) { $entry.Delete(); $zip.Dispose() }',
\ 'else { $zip.Dispose() }'
\ ]
- return 'pwsh -NoProfile -Command ' . s:Escape(join(cmds, ' '))
+ return s:ps . ' -NoProfile -Command ' . s:Escape(join(cmds, ' '))
endfunction
" ----------------
defer s:RestoreOpts(dict)
" sanity checks
- if !executable(g:zip_unzipcmd) && &shell !~ 'pwsh'
+ if !executable(g:zip_unzipcmd) && !s:isPS()
call s:Mess('Error', "***error*** (zip#Browse) unzip not available on your system")
return
endif
endif
let fname = fname->substitute('[', '[[]', 'g')->escape('?*\\')
" sanity check
- if !executable(substitute(g:zip_unzipcmd,'\s\+.*$','','')) && &shell !~ 'pwsh'
+ if !executable(substitute(g:zip_unzipcmd,'\s\+.*$','','')) && !s:isPS()
call s:Mess('Error', "***error*** (zip#Read) sorry, your system doesn't appear to have the ".g:zip_unzipcmd." program")
return
endif
let ps_cmd = s:ZipUpdatePS(zip, fname)
let ps_cmd = 'call system(''' . substitute(ps_cmd, "'", "''", 'g') . ''')'
call s:TryExecGnuFallBackToPs(g:zip_zipcmd, gnu_cmd, ps_cmd)
- if &shell =~ 'pwsh'
+ if s:isPS()
" Vim flashes 'creation in progress ...' from what I believe is the
" ProgressAction stream of PowerShell. Unfortunately, this cannot be
" suppressed (as of 250824) due to an open PowerShell issue.
if v:shell_error != 0
call s:Mess('Error', "***error*** ".g:zip_extractcmd." ".b:zipfile." ".fname.": failed!")
- elseif !filereadable(fname) && &shell !~ 'pwsh'
+ elseif !filereadable(fname) && !s:isPS()
call s:Mess('Error', "***error*** attempted to extract ".fname." but it doesn't appear to be present!")
else
echomsg "***note*** successfully extracted ".fname
-*pi_zip.txt* For Vim version 9.2. Last change: 2026 Jun 20
+*pi_zip.txt* For Vim version 9.2. Last change: 2026 Jul 30
+====================+
| Zip File Interface |
"0": >
let g:zip_exec=0
<
- FALLBACK TO POWERSHELL CORE~
+ FALLBACK TO POWERSHELL~
+ *g:zip_pwsh*
This plugin will first attempt to use the (more capable) GNU zip/unzip
commands. If these commands are not available or fail, and the user is
- using PowerShell Core (i.e., the 'shell' option matches "pwsh"), the
- plugin will fall back to a PowerShell Core cmdlet. The PowerShell Core
- cmdlets are limited: they cannot write or extract files within
- subdirectories of a zip archive. The advantage, however, is that no
- separate unzip binary needs to be installed.
-
+ using PowerShell, the plugin will fall back to a PowerShell cmdlet.
+ The PowerShell cmdlets are limited: they cannot write or extract files
+ within subdirectories of a zip archive. The advantage, however, is that
+ no separate unzip binary needs to be installed. The 'shell' option will
+ be checked whether to choose "pwsh" or "powershell", or set `g:zip_pwsh`.
+ If both are available, this plugin will prefer pwsh. Default value of
+ "g:zip_pwsh" is an empty string, which would not overwrite the 'shell'
+ option configuration. This functionality is not turned on by default on
+ MS-Windows, since the default 'shell' is "cmd.exe". For example: >
+ let g:zip_pwsh='powershell'
+<
PREVENTING LOADING~
If for some reason you do not wish to use vim to examine zipped files,
==============================================================================
4. History *zip-history* {{{1
unreleased:
+ Jul 25, 2026 * support both Windows PowerShell 5 and pwsh 7+
Sep 19, 2025 * support PowerShell Core
Jul 12, 2025 * drop ../ on write to prevent path traversal attacks
Mar 11, 2025 * handle filenames with leading '-' correctly