zip.vim 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437
  1. " zip.vim: Handles browsing zipfiles
  2. " AUTOLOAD PORTION
  3. " Date: 2024 Aug 21
  4. " Version: 34
  5. " Maintainer: This runtime file is looking for a new maintainer.
  6. " Former Maintainer: Charles E Campbell
  7. " Last Change:
  8. " 2024 Jun 16 by Vim Project: handle whitespace on Windows properly (#14998)
  9. " 2024 Jul 23 by Vim Project: fix 'x' command
  10. " 2024 Jul 24 by Vim Project: use delete() function
  11. " 2024 Jul 30 by Vim Project: fix opening remote zipfile
  12. " 2024 Aug 04 by Vim Project: escape '[' in name of file to be extracted
  13. " 2024 Aug 05 by Vim Project: workaround for the FreeBSD's unzip
  14. " 2024 Aug 05 by Vim Project: clean-up and make it work with shellslash on Windows
  15. " 2024 Aug 18 by Vim Project: correctly handle special globbing chars
  16. " 2024 Aug 21 by Vim Project: simplify condition to detect MS-Windows
  17. " 2025 Mar 11 by Vim Project: handle filenames with leading '-' correctly
  18. " License: Vim License (see vim's :help license)
  19. " Copyright: Copyright (C) 2005-2019 Charles E. Campbell {{{1
  20. " Permission is hereby granted to use and distribute this code,
  21. " with or without modifications, provided that this copyright
  22. " notice is copied with it. Like anything else that's free,
  23. " zip.vim and zipPlugin.vim are provided *as is* and comes with
  24. " no warranty of any kind, either expressed or implied. By using
  25. " this plugin, you agree that in no event will the copyright
  26. " holder be liable for any damages resulting from the use
  27. " of this software.
  28. " ---------------------------------------------------------------------
  29. " Load Once: {{{1
  30. if &cp || exists("g:loaded_zip")
  31. finish
  32. endif
  33. let g:loaded_zip= "v34"
  34. let s:keepcpo= &cpo
  35. set cpo&vim
  36. let s:zipfile_escape = ' ?&;\'
  37. let s:ERROR = 2
  38. let s:WARNING = 1
  39. let s:NOTE = 0
  40. " ---------------------------------------------------------------------
  41. " Global Values: {{{1
  42. if !exists("g:zip_shq")
  43. if &shq != ""
  44. let g:zip_shq= &shq
  45. elseif has("unix")
  46. let g:zip_shq= "'"
  47. else
  48. let g:zip_shq= '"'
  49. endif
  50. endif
  51. if !exists("g:zip_zipcmd")
  52. let g:zip_zipcmd= "zip"
  53. endif
  54. if !exists("g:zip_unzipcmd")
  55. let g:zip_unzipcmd= "unzip"
  56. endif
  57. if !exists("g:zip_extractcmd")
  58. let g:zip_extractcmd= g:zip_unzipcmd
  59. endif
  60. " ---------------------------------------------------------------------
  61. " required early
  62. " s:Mess: {{{2
  63. fun! s:Mess(group, msg)
  64. redraw!
  65. exe "echohl " . a:group
  66. echomsg a:msg
  67. echohl Normal
  68. endfun
  69. if v:version < 702
  70. call s:Mess('WarningMsg', "***warning*** this version of zip needs vim 7.2 or later")
  71. finish
  72. endif
  73. " sanity checks
  74. if !executable(g:zip_unzipcmd)
  75. call s:Mess('Error', "***error*** (zip#Browse) unzip not available on your system")
  76. finish
  77. endif
  78. if !dist#vim#IsSafeExecutable('zip', g:zip_unzipcmd)
  79. call s:Mess('Error', "Warning: NOT executing " .. g:zip_unzipcmd .. " from current directory!")
  80. finish
  81. endif
  82. " ----------------
  83. " Functions: {{{1
  84. " ----------------
  85. " ---------------------------------------------------------------------
  86. " zip#Browse: {{{2
  87. fun! zip#Browse(zipfile)
  88. " sanity check: ensure that the zipfile has "PK" as its first two letters
  89. " (zip files have a leading PK as a "magic cookie")
  90. if filereadable(a:zipfile) && readblob(a:zipfile, 0, 2) != 0z50.4B
  91. exe "noswapfile noautocmd e " .. fnameescape(a:zipfile)
  92. return
  93. endif
  94. let dict = s:SetSaneOpts()
  95. defer s:RestoreOpts(dict)
  96. " sanity checks
  97. if !executable(g:zip_unzipcmd)
  98. call s:Mess('Error', "***error*** (zip#Browse) unzip not available on your system")
  99. return
  100. endif
  101. if !filereadable(a:zipfile)
  102. if a:zipfile !~# '^\a\+://'
  103. " if it's an url, don't complain, let url-handlers such as vim do its thing
  104. call s:Mess('Error', "***error*** (zip#Browse) File not readable <".a:zipfile.">")
  105. endif
  106. return
  107. endif
  108. if &ma != 1
  109. set ma
  110. endif
  111. let b:zipfile= a:zipfile
  112. setlocal noswapfile
  113. setlocal buftype=nofile
  114. setlocal bufhidden=hide
  115. setlocal nobuflisted
  116. setlocal nowrap
  117. " Oct 12, 2021: need to re-use Bram's syntax/tar.vim.
  118. " Setting the filetype to zip doesn't do anything (currently),
  119. " but it is perhaps less confusing to curious perusers who do
  120. " a :echo &ft
  121. setf zip
  122. run! syntax/tar.vim
  123. " give header
  124. call append(0, ['" zip.vim version '.g:loaded_zip,
  125. \ '" Browsing zipfile '.a:zipfile,
  126. \ '" Select a file with cursor and press ENTER'])
  127. keepj $
  128. exe $"keepj sil r! {g:zip_unzipcmd} -Z1 -- {s:Escape(a:zipfile, 1)}"
  129. if v:shell_error != 0
  130. call s:Mess('WarningMsg', "***warning*** (zip#Browse) ".fnameescape(a:zipfile)." is not a zip file")
  131. keepj sil! %d
  132. let eikeep= &ei
  133. set ei=BufReadCmd,FileReadCmd
  134. exe "keepj r ".fnameescape(a:zipfile)
  135. let &ei= eikeep
  136. keepj 1d
  137. return
  138. endif
  139. " Maps associated with zip plugin
  140. setlocal noma nomod ro
  141. noremap <silent> <buffer> <cr> :call <SID>ZipBrowseSelect()<cr>
  142. noremap <silent> <buffer> x :call zip#Extract()<cr>
  143. if &mouse != ""
  144. noremap <silent> <buffer> <leftmouse> <leftmouse>:call <SID>ZipBrowseSelect()<cr>
  145. endif
  146. endfun
  147. " ---------------------------------------------------------------------
  148. " ZipBrowseSelect: {{{2
  149. fun! s:ZipBrowseSelect()
  150. let dict = s:SetSaneOpts()
  151. defer s:RestoreOpts(dict)
  152. let fname= getline(".")
  153. if !exists("b:zipfile")
  154. return
  155. endif
  156. " sanity check
  157. if fname =~ '^"'
  158. return
  159. endif
  160. if fname =~ '/$'
  161. call s:Mess('Error', "***error*** (zip#Browse) Please specify a file, not a directory")
  162. return
  163. endif
  164. " get zipfile to the new-window
  165. let zipfile = b:zipfile
  166. let curfile = expand("%")
  167. noswapfile new
  168. if !exists("g:zip_nomax") || g:zip_nomax == 0
  169. wincmd _
  170. endif
  171. let s:zipfile_{winnr()}= curfile
  172. exe "noswapfile e ".fnameescape("zipfile://".zipfile.'::'.fname)
  173. filetype detect
  174. endfun
  175. " ---------------------------------------------------------------------
  176. " zip#Read: {{{2
  177. fun! zip#Read(fname,mode)
  178. let dict = s:SetSaneOpts()
  179. defer s:RestoreOpts(dict)
  180. if has("unix")
  181. let zipfile = substitute(a:fname,'zipfile://\(.\{-}\)::[^\\].*$','\1','')
  182. let fname = substitute(a:fname,'zipfile://.\{-}::\([^\\].*\)$','\1','')
  183. else
  184. let zipfile = substitute(a:fname,'^.\{-}zipfile://\(.\{-}\)::[^\\].*$','\1','')
  185. let fname = substitute(a:fname,'^.\{-}zipfile://.\{-}::\([^\\].*\)$','\1','')
  186. endif
  187. let fname = fname->substitute('[', '[[]', 'g')->escape('?*\\')
  188. " sanity check
  189. if !executable(substitute(g:zip_unzipcmd,'\s\+.*$','',''))
  190. call s:Mess('Error', "***error*** (zip#Read) sorry, your system doesn't appear to have the ".g:zip_unzipcmd." program")
  191. return
  192. endif
  193. " the following code does much the same thing as
  194. " exe "keepj sil! r! ".g:zip_unzipcmd." -p -- ".s:Escape(zipfile,1)." ".s:Escape(fname,1)
  195. " but allows zipfile://... entries in quickfix lists
  196. let temp = tempname()
  197. let fn = expand('%:p')
  198. exe "sil !".g:zip_unzipcmd." -p -- ".s:Escape(zipfile,1)." ".s:Escape(fname,1).' > '.temp
  199. sil exe 'keepalt file '.temp
  200. sil keepj e!
  201. sil exe 'keepalt file '.fnameescape(fn)
  202. call delete(temp)
  203. filetype detect
  204. " cleanup
  205. set nomod
  206. endfun
  207. " ---------------------------------------------------------------------
  208. " zip#Write: {{{2
  209. fun! zip#Write(fname)
  210. let dict = s:SetSaneOpts()
  211. defer s:RestoreOpts(dict)
  212. " sanity checks
  213. if !executable(substitute(g:zip_zipcmd,'\s\+.*$','',''))
  214. call s:Mess('Error', "***error*** (zip#Write) sorry, your system doesn't appear to have the ".g:zip_zipcmd." program")
  215. return
  216. endif
  217. if !exists("*mkdir")
  218. call s:Mess('Error', "***error*** (zip#Write) sorry, mkdir() doesn't work on your system")
  219. return
  220. endif
  221. let curdir= getcwd()
  222. let tmpdir= tempname()
  223. if tmpdir =~ '\.'
  224. let tmpdir= substitute(tmpdir,'\.[^.]*$','','e')
  225. endif
  226. call mkdir(tmpdir,"p")
  227. " attempt to change to the indicated directory
  228. if s:ChgDir(tmpdir,s:ERROR,"(zip#Write) cannot cd to temporary directory")
  229. return
  230. endif
  231. " place temporary files under .../_ZIPVIM_/
  232. if isdirectory("_ZIPVIM_")
  233. call delete("_ZIPVIM_", "rf")
  234. endif
  235. call mkdir("_ZIPVIM_")
  236. cd _ZIPVIM_
  237. if has("unix")
  238. let zipfile = substitute(a:fname,'zipfile://\(.\{-}\)::[^\\].*$','\1','')
  239. let fname = substitute(a:fname,'zipfile://.\{-}::\([^\\].*\)$','\1','')
  240. else
  241. let zipfile = substitute(a:fname,'^.\{-}zipfile://\(.\{-}\)::[^\\].*$','\1','')
  242. let fname = substitute(a:fname,'^.\{-}zipfile://.\{-}::\([^\\].*\)$','\1','')
  243. endif
  244. if fname =~ '/'
  245. let dirpath = substitute(fname,'/[^/]\+$','','e')
  246. if has("win32unix") && executable("cygpath")
  247. let dirpath = substitute(system("cygpath ".s:Escape(dirpath,0)),'\n','','e')
  248. endif
  249. call mkdir(dirpath,"p")
  250. endif
  251. if zipfile !~ '/'
  252. let zipfile= curdir.'/'.zipfile
  253. endif
  254. exe "w! ".fnameescape(fname)
  255. if has("win32unix") && executable("cygpath")
  256. let zipfile = substitute(system("cygpath ".s:Escape(zipfile,0)),'\n','','e')
  257. endif
  258. if (has("win32") || has("win95") || has("win64") || has("win16")) && &shell !~? 'sh$'
  259. let fname = substitute(fname, '[', '[[]', 'g')
  260. endif
  261. call system(g:zip_zipcmd." -u ".s:Escape(fnamemodify(zipfile,":p"),0)." ".s:Escape(fname,0))
  262. if v:shell_error != 0
  263. call s:Mess('Error', "***error*** (zip#Write) sorry, unable to update ".zipfile." with ".fname)
  264. elseif s:zipfile_{winnr()} =~ '^\a\+://'
  265. " support writing zipfiles across a network
  266. let netzipfile= s:zipfile_{winnr()}
  267. 1split|enew
  268. let binkeep= &binary
  269. let eikeep = &ei
  270. set binary ei=all
  271. exe "noswapfile e! ".fnameescape(zipfile)
  272. call netrw#NetWrite(netzipfile)
  273. let &ei = eikeep
  274. let &binary = binkeep
  275. q!
  276. unlet s:zipfile_{winnr()}
  277. endif
  278. " cleanup and restore current directory
  279. cd ..
  280. call delete("_ZIPVIM_", "rf")
  281. call s:ChgDir(curdir,s:WARNING,"(zip#Write) unable to return to ".curdir."!")
  282. call delete(tmpdir, "rf")
  283. setlocal nomod
  284. endfun
  285. " ---------------------------------------------------------------------
  286. " zip#Extract: extract a file from a zip archive {{{2
  287. fun! zip#Extract()
  288. let dict = s:SetSaneOpts()
  289. defer s:RestoreOpts(dict)
  290. let fname= getline(".")
  291. " sanity check
  292. if fname =~ '^"'
  293. return
  294. endif
  295. if fname =~ '/$'
  296. call s:Mess('Error', "***error*** (zip#Extract) Please specify a file, not a directory")
  297. return
  298. endif
  299. if filereadable(fname)
  300. call s:Mess('Error', "***error*** (zip#Extract) <" .. fname .."> already exists in directory, not overwriting!")
  301. return
  302. endif
  303. let target = fname->substitute('\[', '[[]', 'g')
  304. " unzip 6.0 does not support -- to denote end-of-arguments
  305. " unzip 6.1 (2010) apparently supports, it, but hasn't been released
  306. " so the workaround is to use glob '[-]' so that it won't be considered an argument
  307. " else, it would be possible to use 'unzip -o <file.zip> '-d/tmp' to extract the whole archive
  308. let target = target->substitute('^-', '[&]', '')
  309. if &shell =~ 'cmd' && has("win32")
  310. let target = target
  311. \ ->substitute('[?*]', '[&]', 'g')
  312. \ ->substitute('[\\]', '?', 'g')
  313. \ ->shellescape()
  314. " there cannot be a file name with '\' in its name, unzip replaces it by _
  315. let fname = fname->substitute('[\\?*]', '_', 'g')
  316. else
  317. let target = target->escape('*?\\')->shellescape()
  318. endif
  319. " extract the file mentioned under the cursor
  320. call system($"{g:zip_extractcmd} -o {shellescape(b:zipfile)} {target}")
  321. if v:shell_error != 0
  322. call s:Mess('Error', "***error*** ".g:zip_extractcmd." ".b:zipfile." ".fname.": failed!")
  323. elseif !filereadable(fname)
  324. call s:Mess('Error', "***error*** attempted to extract ".fname." but it doesn't appear to be present!")
  325. else
  326. echomsg "***note*** successfully extracted ".fname
  327. endif
  328. endfun
  329. " ---------------------------------------------------------------------
  330. " s:Escape: {{{2
  331. fun! s:Escape(fname,isfilt)
  332. if exists("*shellescape")
  333. if a:isfilt
  334. let qnameq= shellescape(a:fname,1)
  335. else
  336. let qnameq= shellescape(a:fname)
  337. endif
  338. else
  339. let qnameq= g:zip_shq.escape(a:fname,g:zip_shq).g:zip_shq
  340. endif
  341. return qnameq
  342. endfun
  343. " ---------------------------------------------------------------------
  344. " s:ChgDir: {{{2
  345. fun! s:ChgDir(newdir,errlvl,errmsg)
  346. try
  347. exe "cd ".fnameescape(a:newdir)
  348. catch /^Vim\%((\a\+)\)\=:E344/
  349. redraw!
  350. if a:errlvl == s:NOTE
  351. echomsg "***note*** ".a:errmsg
  352. elseif a:errlvl == s:WARNING
  353. call s:Mess("WarningMsg", "***warning*** ".a:errmsg)
  354. elseif a:errlvl == s:ERROR
  355. call s:Mess("Error", "***error*** ".a:errmsg)
  356. endif
  357. return 1
  358. endtry
  359. return 0
  360. endfun
  361. " ---------------------------------------------------------------------
  362. " s:SetSaneOpts: {{{2
  363. fun! s:SetSaneOpts()
  364. let dict = {}
  365. let dict.report = &report
  366. let dict.shellslash = &shellslash
  367. let &report = 10
  368. if exists('+shellslash')
  369. let &shellslash = 0
  370. endif
  371. return dict
  372. endfun
  373. " ---------------------------------------------------------------------
  374. " s:RestoreOpts: {{{2
  375. fun! s:RestoreOpts(dict)
  376. for [key, val] in items(a:dict)
  377. exe $"let &{key} = {val}"
  378. endfor
  379. endfun
  380. " ------------------------------------------------------------------------
  381. " Modelines And Restoration: {{{1
  382. let &cpo= s:keepcpo
  383. unlet s:keepcpo
  384. " vim:ts=8 fdm=marker