├── .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