├── .github └── FUNDING.yml ├── .gitignore ├── README.markdown ├── doc └── eunuch.txt └── plugin └── eunuch.vim /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: tpope 2 | custom: ["https://www.paypal.me/vimpope"] 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /doc/tags 2 | -------------------------------------------------------------------------------- /README.markdown: -------------------------------------------------------------------------------- 1 | # eunuch.vim 2 | 3 | Vim sugar for the UNIX shell commands that need it the most. Features 4 | include: 5 | 6 | * `:Remove`: Delete a file on disk without `E211: File no longer available`. 7 | * `:Delete`: Delete a file on disk and the buffer too. 8 | * `:Move`: Rename a buffer and the file on disk simultaneously. See also 9 | `:Rename`, `:Copy`, and `:Duplicate`. 10 | * `:Chmod`: Change the permissions of the current file. 11 | * `:Mkdir`: Create a directory, defaulting to the parent of the current file. 12 | * `:Cfind`: Run `find` and load the results into the quickfix list. 13 | * `:Clocate`: Run `locate` and load the results into the quickfix list. 14 | * `:Lfind`/`:Llocate`: Like above, but use the location list. 15 | * `:Wall`: Write every open window. Handy for kicking off tools like 16 | [guard][]. 17 | * `:SudoWrite`: Write a privileged file with `sudo`. 18 | * `:SudoEdit`: Edit a privileged file with `sudo`. 19 | * Typing a shebang line causes the file type to be re-detected. Additionally 20 | the file will be automatically made executable (`chmod +x`) after the next 21 | write. 22 | 23 | [guard]: https://github.com/guard/guard 24 | 25 | ## Installation 26 | 27 | Install using your favorite package manager, or use Vim's built-in package 28 | support: 29 | 30 | mkdir -p ~/.vim/pack/tpope/start 31 | cd ~/.vim/pack/tpope/start 32 | git clone https://tpope.io/vim/eunuch.git 33 | vim -u NONE -c "helptags eunuch/doc" -c q 34 | 35 | ## Contributing 36 | 37 | See the contribution guidelines for 38 | [pathogen.vim](https://github.com/tpope/vim-pathogen#readme). 39 | 40 | ## Self-Promotion 41 | 42 | Like eunuch.vim? Follow the repository on 43 | [GitHub](https://github.com/tpope/vim-eunuch) and vote for it on 44 | [vim.org](http://www.vim.org/scripts/script.php?script_id=4300). And if 45 | you're feeling especially charitable, follow [tpope](http://tpo.pe/) on 46 | [Twitter](http://twitter.com/tpope) and 47 | [GitHub](https://github.com/tpope). 48 | 49 | ## License 50 | 51 | Copyright (c) Tim Pope. Distributed under the same terms as Vim itself. 52 | See `:help license`. 53 | -------------------------------------------------------------------------------- /doc/eunuch.txt: -------------------------------------------------------------------------------- 1 | *eunuch.txt* File manipulation 2 | Author: Tim Pope 3 | License: Same terms as Vim itself (see |license|) 4 | 5 | This plugin is only available if 'compatible' is not set. 6 | 7 | INTRODUCTION *eunuch* 8 | 9 | Vim sugar for the UNIX shell commands that need it the most. Delete or rename 10 | a buffer and the underlying file at the same time. Load a `find` or a 11 | `locate` into the quickfix list. And so on. 12 | 13 | COMMANDS *eunuch-commands* 14 | 15 | *eunuch-:Remove* *eunuch-:Unlink* 16 | :Remove[!] Delete the file from disk and reload the buffer. If 17 | :Unlink[!] you change your mind, the contents of the buffer can 18 | be restored with |u| (see 'undoreload'). 19 | 20 | *eunuch-:Delete* 21 | :Delete[!] Delete the file from disk and |:bdelete| the buffer. 22 | This cannot be undone, and thus a `!` is required to 23 | delete non-empty files. 24 | 25 | *eunuch-:Copy* 26 | :Copy[!] {file} Small wrapper around |:saveas|. Parent directories 27 | are automatically created. If the argument itself is 28 | a directory, a file with the same basename will be 29 | created inside that directory. 30 | 31 | *eunuch-:Duplicate* 32 | :Duplicate[!] {file} Like |:Copy|, but the argument is taken as relative to 33 | the current file's parent directory. 34 | 35 | *eunuch-:Move* 36 | :Move[!] {file} Like |:Copy|, but delete the old file and |:bwipe| the 37 | old buffer afterwards. 38 | 39 | *eunuch-:Rename* 40 | :Rename[!] {file} Like |:Move|, but the argument is taken as relative to 41 | the current file's parent directory. 42 | 43 | *eunuch-:Chmod* 44 | :Chmod {mode} Change the permissions of the current file. 45 | 46 | *eunuch-:Mkdir* 47 | :Mkdir {dir} Create directory {dir} and all parent directories, 48 | like `mkdir -p`. 49 | 50 | :Mkdir With no argument, create the containing directory for 51 | the current file. 52 | 53 | *eunuch-:Cfind* 54 | :Cfind[!] {args} Run `find` and load the results into the quickfix 55 | list. Jump to the first result unless ! is given. 56 | 57 | *eunuch-:Lfind* 58 | :Lfind[!] {args} Run `find` and load the results into the location 59 | list. Jump to the first result unless ! is given. 60 | 61 | *eunuch-:Clocate* 62 | :Clocate[!] {args} Run `locate` and load the results into the quickfix 63 | list. Jump to the first result unless ! is given. 64 | 65 | *eunuch-:Llocate* 66 | :Llocate[!] {args} Run `locate` and load the results into the location 67 | list. Jump to the first result unless ! is given. 68 | 69 | *eunuch-:SudoEdit* 70 | :SudoEdit [file] Edit a file using sudo. This overrides any read 71 | permission issues, plus allows you to write the file 72 | with :w!. 73 | 74 | *eunuch-:SudoWrite* 75 | :SudoWrite Use sudo to write the file to disk. Handy when you 76 | forgot to use sudo to invoke Vim. This uses :SudoEdit 77 | internally, so after the first invocation you can 78 | subsequently use :w!. 79 | 80 | Both sudo commands are implemented using `sudo -e`, 81 | also known as sudoedit. This has the advantage of 82 | respecting sudoedit permissions in /etc/sudoers, and 83 | the constraint of not allowing edits to symlinks or 84 | files in writable directories, both of which can be 85 | abused in some circumstances to write to files that 86 | were not intended. These restrictions can be lifted 87 | with the sudoedit_follow and sudoedit_checkdir sudo 88 | options, respectively. 89 | 90 | *eunuch-:Wall* *eunuch-:W* 91 | :Wall Like |:wall|, but for windows rather than buffers. 92 | :W It also writes files that haven't changed, which is 93 | useful for kicking off build and test suites (such as 94 | with watchr or guard). Furthermore, it handily 95 | doubles as a safe fallback for people who, like me, 96 | accidentally type :W instead of :w a lot. 97 | 98 | PASSIVE BEHAVIORS *eunuch-passive* 99 | 100 | If you type a line at the beginning of a file that starts with #! and press 101 | , The current file type will be re-detected. This is implemented using a 102 | map. If you already have a map, Eunuch will attempt to combine with 103 | it. For best results, use an map. 104 | 105 | Additionally, if the shebang line lacks a path (e.g., `#!bash`), it will be 106 | normalized by adding `/usr/bin/env` (e.g., `#!/usr/bin/env bash`). If it 107 | lacks a command entirely (just `#!`), Eunuch will invert the process and pick 108 | a command appropriate for the current file type. For example, if the file 109 | type is "python", the shebang will become `#!/usr/bin/env python3` . 110 | 111 | Finally, adding a shebang line to a new or existing file will cause `chmod +x` 112 | to be invoked on the file on the next write. 113 | 114 | *g:eunuch_interpreters* 115 | You can customize the generated shebang with g:eunuch_interpreters, a 116 | dictionary that maps between file types and shell commands: 117 | > 118 | let g:eunuch_interpreters = { 119 | \ 'lua': '/usr/bin/lua5.1'} 120 | < 121 | This example is a joke. Do not use Lua. 122 | 123 | ABOUT *eunuch-about* 124 | 125 | Grab the latest version or report a bug on GitHub: 126 | 127 | http://github.com/tpope/vim-eunuch 128 | 129 | vim:tw=78:et:ft=help:norl: 130 | -------------------------------------------------------------------------------- /plugin/eunuch.vim: -------------------------------------------------------------------------------- 1 | " eunuch.vim - Helpers for UNIX 2 | " Maintainer: Tim Pope 3 | " Version: 1.3 4 | 5 | if exists('g:loaded_eunuch') || &cp || v:version < 704 6 | finish 7 | endif 8 | let g:loaded_eunuch = 1 9 | 10 | let s:slash_pat = exists('+shellslash') ? '[\/]' : '/' 11 | 12 | function! s:separator() abort 13 | return !exists('+shellslash') || &shellslash ? '/' : '\' 14 | endfunction 15 | 16 | function! s:ffn(fn, path) abort 17 | return get(get(g:, 'io_' . matchstr(a:path, '^\a\a\+\ze:'), {}), a:fn, a:fn) 18 | endfunction 19 | 20 | function! s:fcall(fn, path, ...) abort 21 | return call(s:ffn(a:fn, a:path), [a:path] + a:000) 22 | endfunction 23 | 24 | function! s:AbortOnError(cmd) abort 25 | try 26 | exe a:cmd 27 | catch '^Vim(\w\+):E\d' 28 | return 'return ' . string('echoerr ' . string(matchstr(v:exception, ':\zsE\d.*'))) 29 | endtry 30 | return '' 31 | endfunction 32 | 33 | function! s:MinusOne(...) abort 34 | return -1 35 | endfunction 36 | 37 | function! EunuchRename(src, dst) abort 38 | if a:src !~# '^\a\a\+:' && a:dst !~# '^\a\a\+:' 39 | return rename(a:src, a:dst) 40 | endif 41 | try 42 | let fn = s:ffn('writefile', a:dst) 43 | let copy = call(fn, [s:fcall('readfile', a:src, 'b'), a:dst]) 44 | if copy == 0 45 | let delete = s:fcall('delete', a:src) 46 | if delete == 0 47 | return 0 48 | else 49 | call s:fcall('delete', a:dst) 50 | return -1 51 | endif 52 | endif 53 | catch 54 | return -1 55 | endtry 56 | endfunction 57 | 58 | function! s:MkdirCallable(name) abort 59 | let ns = matchstr(a:name, '^\a\a\+\ze:') 60 | if !s:fcall('isdirectory', a:name) && s:fcall('filewritable', a:name) !=# 2 61 | if exists('g:io_' . ns . '.mkdir') 62 | return [g:io_{ns}.mkdir, [a:name, 'p']] 63 | elseif empty(ns) 64 | return ['mkdir', [a:name, 'p']] 65 | endif 66 | endif 67 | return ['s:MinusOne', []] 68 | endfunction 69 | 70 | function! s:Delete(path) abort 71 | if has('patch-7.4.1107') && isdirectory(a:path) 72 | return delete(a:path, 'd') 73 | else 74 | return s:fcall('delete', a:path) 75 | endif 76 | endfunction 77 | 78 | command! -bar -bang -nargs=? -complete=dir Mkdir 79 | \ let s:dst = empty() ? expand('%:h') : | 80 | \ if call('call', s:MkdirCallable(s:dst)) == -1 | 81 | \ echohl WarningMsg | 82 | \ echo "Directory already exists: " . s:dst | 83 | \ echohl NONE | 84 | \ elseif empty() | 85 | \ silent keepalt execute 'file' fnameescape(@%) | 86 | \ endif | 87 | \ unlet s:dst 88 | 89 | function! s:DeleteError(file) abort 90 | if empty(s:fcall('getftype', a:file)) 91 | return 'Could not find "' . a:file . '" on disk' 92 | else 93 | return 'Failed to delete "' . a:file . '"' 94 | endif 95 | endfunction 96 | 97 | command! -bar -bang Unlink 98 | \ if 1 && &undoreload >= 0 && line('$') >= &undoreload | 99 | \ echoerr "Buffer too big for 'undoreload' (add ! to override)" | 100 | \ elseif s:Delete(@%) | 101 | \ echoerr s:DeleteError(@%) | 102 | \ else | 103 | \ edit! | 104 | \ silent exe 'doautocmd User FileUnlinkPost' | 105 | \ endif 106 | 107 | command! -bar -bang Remove Unlink 108 | 109 | command! -bar -bang Delete 110 | \ if 1 && !(line('$') == 1 && empty(getline(1)) || s:fcall('getftype', @%) !=# 'file') | 111 | \ echoerr "File not empty (add ! to override)" | 112 | \ else | 113 | \ let s:file = expand('%:p') | 114 | \ execute 'bdelete' | 115 | \ if !bufloaded(s:file) && s:Delete(s:file) | 116 | \ echoerr s:DeleteError(s:sfile) | 117 | \ endif | 118 | \ unlet s:file | 119 | \ endif 120 | 121 | function! s:FileDest(q_args) abort 122 | let file = a:q_args 123 | if file =~# s:slash_pat . '$' 124 | let file .= expand('%:t') 125 | elseif s:fcall('isdirectory', file) 126 | let file .= s:separator() . expand('%:t') 127 | endif 128 | return substitute(file, '^\.' . s:slash_pat, '', '') 129 | endfunction 130 | 131 | command! -bar -nargs=1 -bang -complete=file Copy 132 | \ let s:dst = s:FileDest() | 133 | \ call call('call', s:MkdirCallable(fnamemodify(s:dst, ':h'))) | 134 | \ let s:dst = s:fcall('simplify', s:dst) | 135 | \ exe expand('') 'saveas' fnameescape(remove(s:, 'dst')) | 136 | \ filetype detect 137 | 138 | function! s:Move(bang, arg) abort 139 | let dst = s:FileDest(a:arg) 140 | exe s:AbortOnError('call call("call", s:MkdirCallable(' . string(fnamemodify(dst, ':h')) . '))') 141 | let dst = s:fcall('simplify', dst) 142 | if !a:bang && s:fcall('filereadable', dst) 143 | let confirm = &confirm 144 | try 145 | if confirm | set noconfirm | endif 146 | exe s:AbortOnError('keepalt saveas ' . fnameescape(dst)) 147 | finally 148 | if confirm | set confirm | endif 149 | endtry 150 | endif 151 | if s:fcall('filereadable', @%) && EunuchRename(@%, dst) 152 | return 'echoerr ' . string('Failed to rename "'.@%.'" to "'.dst.'"') 153 | else 154 | let last_bufnr = bufnr('$') 155 | exe s:AbortOnError('silent keepalt file ' . fnameescape(dst)) 156 | if bufnr('$') != last_bufnr 157 | exe bufnr('$') . 'bwipe' 158 | endif 159 | setlocal modified 160 | return 'write!|filetype detect' 161 | endif 162 | endfunction 163 | 164 | command! -bar -nargs=1 -bang -complete=file Move exe s:Move(0, ) 165 | 166 | " ~/f, $VAR/f, %:h/f, #1:h/f, /f, C:/f, url://f 167 | let s:absolute_pat = '^[~$#%]\|^' . s:slash_pat . '\|^\a\+:' 168 | 169 | function! s:RenameComplete(A, L, P) abort 170 | let sep = s:separator() 171 | if a:A =~# s:absolute_pat 172 | let prefix = '' 173 | else 174 | let prefix = expand('%:h') . sep 175 | endif 176 | let files = split(glob(prefix.a:A.'*'), "\n") 177 | call map(files, 'fnameescape(strpart(v:val, len(prefix))) . (isdirectory(v:val) ? sep : "")') 178 | return files 179 | endfunction 180 | 181 | function! s:RenameArg(arg) abort 182 | if a:arg =~# s:absolute_pat 183 | return a:arg 184 | else 185 | return '%:h/' . a:arg 186 | endif 187 | endfunction 188 | 189 | command! -bar -nargs=1 -bang -complete=customlist,s:RenameComplete Duplicate 190 | \ exe 'Copy' escape(s:RenameArg(), '"|') 191 | 192 | command! -bar -nargs=1 -bang -complete=customlist,s:RenameComplete Rename 193 | \ exe 'Move' escape(s:RenameArg(), '"|') 194 | 195 | let s:permlookup = ['---','--x','-w-','-wx','r--','r-x','rw-','rwx'] 196 | function! s:Chmod(bang, perm, ...) abort 197 | let autocmd = 'silent doautocmd User FileChmodPost' 198 | let file = a:0 ? expand(join(a:000, ' ')) : @% 199 | if !a:bang && exists('*setfperm') 200 | let perm = '' 201 | if a:perm =~# '^\0*[0-7]\{3\}$' 202 | let perm = substitute(a:perm[-3:-1], '.', '\=s:permlookup[submatch(0)]', 'g') 203 | elseif a:perm ==# '+x' 204 | let perm = substitute(s:fcall('getfperm', file), '\(..\).', '\1x', 'g') 205 | elseif a:perm ==# '-x' 206 | let perm = substitute(s:fcall('getfperm', file), '\(..\).', '\1-', 'g') 207 | endif 208 | if len(perm) && file =~# '^\a\a\+:' && !s:fcall('setfperm', file, perm) 209 | return autocmd 210 | endif 211 | endif 212 | if !executable('chmod') 213 | return 'echoerr "No chmod command in path"' 214 | endif 215 | let out = get(split(system('chmod '.(a:bang ? '-R ' : '').a:perm.' '.shellescape(file)), "\n"), 0, '') 216 | return len(out) ? 'echoerr ' . string(out) : autocmd 217 | endfunction 218 | 219 | command! -bar -bang -nargs=+ Chmod 220 | \ exe s:Chmod(0, ) 221 | 222 | function! s:FindPath() abort 223 | if !has('win32') 224 | return 'find' 225 | elseif !exists('s:find_path') 226 | let s:find_path = 'find' 227 | for p in split($PATH, ';') 228 | let prg_path = p ..'/find' 229 | if p !~? '\' && executable(prg_path) 230 | let s:find_path = prg_path 231 | break 232 | endif 233 | endfor 234 | endif 235 | return s:find_path 236 | endf 237 | 238 | command! -bang -complete=file -nargs=+ Cfind exe s:Grep(, , s:FindPath(), '') 239 | command! -bang -complete=file -nargs=+ Clocate exe s:Grep(, , 'locate', '') 240 | command! -bang -complete=file -nargs=+ Lfind exe s:Grep(, , s:FindPath(), 'l') 241 | command! -bang -complete=file -nargs=+ Llocate exe s:Grep(, , 'locate', 'l') 242 | function! s:Grep(bang, args, prg, type) abort 243 | let grepprg = &l:grepprg 244 | let grepformat = &l:grepformat 245 | let shellpipe = &shellpipe 246 | try 247 | let &l:grepprg = a:prg 248 | setlocal grepformat=%f 249 | if &shellpipe ==# '2>&1| tee' || &shellpipe ==# '|& tee' 250 | let &shellpipe = "| tee" 251 | endif 252 | execute a:type.'grep! '.a:args 253 | if empty(a:bang) && !empty(getqflist()) 254 | return 'cfirst' 255 | else 256 | return '' 257 | endif 258 | finally 259 | let &l:grepprg = grepprg 260 | let &l:grepformat = grepformat 261 | let &shellpipe = shellpipe 262 | endtry 263 | endfunction 264 | 265 | function! s:SilentSudoCmd(editor) abort 266 | let cmd = 'env SUDO_EDITOR=' . a:editor . ' VISUAL=' . a:editor . ' sudo -e' 267 | let local_nvim = has('nvim') && len($DISPLAY . $SECURITYSESSIONID . $TERM_PROGRAM) 268 | if !local_nvim && (!has('gui_running') || &guioptions =~# '!') 269 | redraw 270 | echo 271 | return ['silent', cmd] 272 | elseif !empty($SUDO_ASKPASS) || 273 | \ filereadable('/etc/sudo.conf') && 274 | \ len(filter(readfile('/etc/sudo.conf', '', 50), 'v:val =~# "^Path askpass "')) 275 | return ['silent', cmd . ' -A'] 276 | else 277 | return [local_nvim ? 'silent' : '', cmd] 278 | endif 279 | endfunction 280 | 281 | augroup eunuch_sudo 282 | augroup END 283 | 284 | function! s:SudoSetup(file, resolve_symlink) abort 285 | let file = a:file 286 | if a:resolve_symlink && getftype(file) ==# 'link' 287 | let file = resolve(file) 288 | if file !=# a:file 289 | silent keepalt exe 'file' fnameescape(file) 290 | endif 291 | endif 292 | let file = substitute(file, s:slash_pat, '/', 'g') 293 | if file !~# '^\a\+:\|^/' 294 | let file = substitute(getcwd(), s:slash_pat, '/', 'g') . '/' . file 295 | endif 296 | if !filereadable(file) && !exists('#eunuch_sudo#BufReadCmd#'.fnameescape(file)) 297 | execute 'autocmd eunuch_sudo BufReadCmd ' fnameescape(file) 'exe s:SudoReadCmd()' 298 | endif 299 | if !filewritable(file) && !exists('#eunuch_sudo#BufWriteCmd#'.fnameescape(file)) 300 | execute 'autocmd eunuch_sudo BufReadPost' fnameescape(file) 'set noreadonly' 301 | execute 'autocmd eunuch_sudo BufWriteCmd' fnameescape(file) 'exe s:SudoWriteCmd()' 302 | endif 303 | endfunction 304 | 305 | let s:error_file = tempname() 306 | 307 | function! s:SudoError() abort 308 | let error = join(readfile(s:error_file), " | ") 309 | if error =~# '^sudo' || v:shell_error 310 | return len(error) ? error : 'Error invoking sudo' 311 | else 312 | return error 313 | endif 314 | endfunction 315 | 316 | function! s:SudoReadCmd() abort 317 | if &shellpipe =~ '|&' 318 | return 'echoerr ' . string('eunuch.vim: no sudo read support for csh') 319 | endif 320 | silent %delete_ 321 | silent doautocmd BufReadPre 322 | let [silent, cmd] = s:SilentSudoCmd('cat') 323 | execute silent 'read !' . cmd . ' "%" 2> ' . s:error_file 324 | let exit_status = v:shell_error 325 | silent 1delete_ 326 | setlocal nomodified 327 | if exit_status 328 | return 'echoerr ' . string(s:SudoError()) 329 | else 330 | return 'silent doautocmd BufReadPost' 331 | endif 332 | endfunction 333 | 334 | function! s:SudoWriteCmd() abort 335 | silent doautocmd BufWritePre 336 | let [silent, cmd] = s:SilentSudoCmd(shellescape('sh -c cat>"$0"')) 337 | execute silent 'write !' . cmd . ' "%" 2> ' . s:error_file 338 | let error = s:SudoError() 339 | if !empty(error) 340 | return 'echoerr ' . string(error) 341 | else 342 | setlocal nomodified 343 | return 'silent doautocmd BufWritePost' 344 | endif 345 | endfunction 346 | 347 | command! -bar -bang -complete=file -nargs=? SudoEdit 348 | \ let s:arg = resolve() | 349 | \ call s:SudoSetup(fnamemodify(empty(s:arg) ? @% : s:arg, ':p'), empty(s:arg) && 0) | 350 | \ if !&modified || !empty(s:arg) || 0 | 351 | \ exe 'edit' fnameescape(s:arg) | 352 | \ endif | 353 | \ if empty() || expand('%:p') ==# fnamemodify(s:arg, ':p') | 354 | \ set noreadonly | 355 | \ endif | 356 | \ unlet s:arg 357 | 358 | if exists(':SudoWrite') != 2 359 | command! -bar -bang SudoWrite 360 | \ call s:SudoSetup(expand('%:p'), 0) | 361 | \ setlocal noreadonly | 362 | \ write! 363 | endif 364 | 365 | command! -bar Wall call s:Wall() 366 | if exists(':W') !=# 2 367 | command! -bar W Wall 368 | endif 369 | function! s:Wall() abort 370 | let tab = tabpagenr() 371 | let win = winnr() 372 | let seen = {} 373 | if !&readonly && &buftype =~# '^\%(acwrite\)\=$' && expand('%') !=# '' 374 | let seen[bufnr('')] = 1 375 | write 376 | endif 377 | tabdo windo if !&readonly && &buftype =~# '^\%(acwrite\)\=$' && expand('%') !=# '' && !has_key(seen, bufnr('')) | silent write | let seen[bufnr('')] = 1 | endif 378 | execute 'tabnext '.tab 379 | execute win.'wincmd w' 380 | endfunction 381 | 382 | " Adapted from autoload/dist/script.vim. 383 | let s:interpreters = { 384 | \ '.': '/bin/sh', 385 | \ 'sh': '/bin/sh', 386 | \ 'bash': 'bash', 387 | \ 'csh': 'csh', 388 | \ 'tcsh': 'tcsh', 389 | \ 'zsh': 'zsh', 390 | \ 'tcl': 'tclsh', 391 | \ 'expect': 'expect', 392 | \ 'gnuplot': 'gnuplot', 393 | \ 'make': 'make -f', 394 | \ 'pike': 'pike', 395 | \ 'lua': 'lua', 396 | \ 'perl': 'perl', 397 | \ 'php': 'php', 398 | \ 'python': 'python3', 399 | \ 'groovy': 'groovy', 400 | \ 'raku': 'raku', 401 | \ 'ruby': 'ruby', 402 | \ 'javascript': 'node', 403 | \ 'bc': 'bc', 404 | \ 'sed': 'sed', 405 | \ 'ocaml': 'ocaml', 406 | \ 'awk': 'awk', 407 | \ 'wml': 'wml', 408 | \ 'scheme': 'scheme', 409 | \ 'cfengine': 'cfengine', 410 | \ 'erlang': 'escript', 411 | \ 'haskell': 'haskell', 412 | \ 'scala': 'scala', 413 | \ 'clojure': 'clojure', 414 | \ 'pascal': 'instantfpc', 415 | \ 'fennel': 'fennel', 416 | \ 'routeros': 'rsc', 417 | \ 'fish': 'fish', 418 | \ 'forth': 'gforth', 419 | \ } 420 | 421 | function! s:NormalizeInterpreter(str) abort 422 | if empty(a:str) || a:str =~# '^[ /]' 423 | return a:str 424 | elseif a:str =~# '[ \''"#]' 425 | return '/usr/bin/env -S ' . a:str 426 | else 427 | return '/usr/bin/env ' . a:str 428 | endif 429 | endfunction 430 | 431 | function! s:FileTypeInterpreter() abort 432 | try 433 | let ft = get(split(&filetype, '\.'), 0, '.') 434 | let configured = get(g:, 'eunuch_interpreters', {}) 435 | if type(get(configured, ft)) == type(function('tr')) 436 | return call(configured[ft], []) 437 | elseif get(configured, ft) is# 1 || get(configured, ft) is# get(v:, 'true', 1) 438 | return ft ==# '.' ? s:interpreters['.'] : '/usr/bin/env ' . ft 439 | elseif empty(get(configured, ft, 1)) 440 | return '' 441 | elseif type(get(configured, ft)) == type('') 442 | return s:NormalizeInterpreter(get(configured, ft)) 443 | endif 444 | return s:NormalizeInterpreter(get(s:interpreters, ft, '')) 445 | endtry 446 | endfunction 447 | 448 | let s:shebang_pat = '^#!\s*[/[:alnum:]_-]' 449 | 450 | function! EunuchNewLine(...) abort 451 | if a:0 && type(a:1) == type('') 452 | return a:1 . (a:1 =~# "\r" && empty(&buftype) ? "\=EunuchNewLine()\r" : "") 453 | endif 454 | if !empty(&buftype) || getline(1) !~# '^#!$\|' . s:shebang_pat || line('.') != 2 || getline(2) !~# '^#\=$' 455 | return "" 456 | endif 457 | let b:eunuch_chmod_shebang = 1 458 | let inject = '' 459 | let detect = 0 460 | let ret = empty(getline(2)) ? "" : "\" 461 | if getline(1) ==# '#!' 462 | let inject = s:FileTypeInterpreter() 463 | let detect = !empty(inject) && empty(&filetype) 464 | else 465 | filetype detect 466 | if getline(1) =~# '^#![^ /].\{-\}[ \''"#]' 467 | let inject = '/usr/bin/env -S ' 468 | elseif getline(1) =~# '^#![^ /]' 469 | let inject = '/usr/bin/env ' 470 | endif 471 | endif 472 | if len(inject) 473 | let ret .= "\\\" . inject . "\\" 474 | endif 475 | if detect 476 | let ret .= "\\:filetype detect\r" 477 | endif 478 | return ret 479 | endfunction 480 | 481 | function! s:MapCR() abort 482 | imap