├── .gitignore ├── LICENSE ├── README.md ├── autoload └── suda.vim ├── doc └── suda.txt └── plugin └── suda.vim /.gitignore: -------------------------------------------------------------------------------- 1 | doc/tags 2 | *.swp 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2017 Alisue, hashnote.net 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | 21 | 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 🥪 suda.vim 2 | 3 | [![MIT License](https://img.shields.io/badge/license-MIT-blue.svg?style=flat-square)](LICENSE) 4 | [![Doc](https://img.shields.io/badge/doc-%3Ah%20suda-orange.svg?style=flat-square)](doc/suda.txt) 5 | 6 | _suda_ is a plugin to read or write files with `sudo` command. 7 | 8 | This plugin was built while `:w !sudo tee % > /dev/null` trick does not work on [neovim][]. 9 | 10 | https://github.com/neovim/neovim/issues/1716 11 | 12 | This plugin is strongly inspired by [sudo.vim][] but the interfaces was aggressively modified for modern Vim script. 13 | 14 | [sudo.vim]: https://github.com/vim-scripts/sudo.vim 15 | [neovim]: https://github.com/neovim/neovim 16 | 17 | ## Usage 18 | 19 | Use `SudaRead` to open unreadable files like: 20 | 21 | ``` 22 | " Re-open a current file with sudo 23 | :SudaRead 24 | 25 | " Open /etc/sudoers with sudo 26 | :SudaRead /etc/sudoers 27 | ``` 28 | 29 | Or `SudaWrite` to write unwritable files like: 30 | 31 | ``` 32 | " Forcedly save a current file with sudo 33 | :SudaWrite 34 | 35 | " Write contents to /etc/profile 36 | :SudaWrite /etc/profile 37 | ``` 38 | 39 | You can change the prompt string with `g:suda#prompt`. 40 | 41 | ```vim 42 | " 'Password' in french 43 | let g:suda#prompt = 'Mot de passe: ' 44 | ``` 45 | 46 | ### Smart edit 47 | 48 | When `let g:suda_smart_edit = 1` is written in your vimrc, suda automatically switch a buffer name when the target file is not readable or writable. 49 | 50 | In short, 51 | 52 | ``` 53 | $ vim /etc/hosts 54 | ``` 55 | 56 | or 57 | 58 | ``` 59 | :e /etc/shadow 60 | ``` 61 | 62 | Will open `suda:///etc/hosts` or `suda:///etc/shadow` instead of `/etc/hosts` or `/etc/shadow` because that files are not writable or not readable. 63 | 64 | ### Windows 65 | 66 | Install [mattn/sudo](https://github.com/mattn/sudo) or [gerardog/gsudo](https://github.com/gerardog/gsudo) to enable this plugin in Windows. 67 | Make sure that the following shows `1`. 68 | 69 | ```vim 70 | : echo executable('sudo') 71 | ``` 72 | 73 | ### Use sudo without a password 74 | 75 | When `let g:suda#noninteractive = 1` is written in your vimrc, suda won't ask you for a password. Use at your own risk. 76 | -------------------------------------------------------------------------------- /autoload/suda.vim: -------------------------------------------------------------------------------- 1 | " {opts} is a list of command line options 2 | " {cmd} is the argv list for the process to run 3 | " {opts} should be *sudo-specific*, while {cmd} is passed to any suda#executable 4 | " Note: {cmd} can not have any sudo flags. Put these into {opts}, as '--' is passed before {cmd} 5 | " Similarly, {opts} should *not* contain '--' 6 | " Returns a string that is safe to pass to `system` on both vim and neovim 7 | function! s:get_command(opts, cmd) 8 | if g:suda#executable ==# 'sudo' 9 | let ret = a:opts + ['--'] + a:cmd 10 | else 11 | " TODO: 12 | " Should we pass '--' before cmd when using a custom suda#executable? 13 | " Should suda#executable be split? Should we allow suda#executable to be a list instead? 14 | " This behavior is entirely undocumented 15 | let ret = a:cmd 16 | endif 17 | 18 | " TODO: Should we detect `has('neovim')` and return a list to avoid a shell? 19 | return join([g:suda#executable] + map(ret, { k, v -> shellescape(v) }), ' ') 20 | endfunction 21 | 22 | " {cmd} is a argv list for the process 23 | " {input} (a:1) is a string to pass as stdin to the command 24 | " Returns a list of the command's output, split by NLs, with NULs replaced with NLs. 25 | function! suda#systemlist(cmd, ...) abort 26 | let cmd = has('win32') || g:suda#noninteractive 27 | \ ? s:get_command([], a:cmd) 28 | \ : s:get_command(['-p', '', '-n'], a:cmd) 29 | if &verbose 30 | echomsg '[suda]' cmd 31 | endif 32 | let result = a:0 ? systemlist(cmd, a:1) : systemlist(cmd) 33 | if v:shell_error == 0 34 | return result 35 | endif 36 | let ask_pass = 1 37 | " Let's try running a command non-interactively. If it works, we have a sudo 38 | " timestamp that has not timed out yet. In this case there is no need to ask 39 | " for a password. 40 | " This only works if the timestamp_type is set to 'global' in the sudo 41 | " configuation file. It does not work with 'ppid', 'kernel' or 'tty'. 42 | " Note: for non-sudo commands, don't do this, instead *always* ask for the password 43 | if g:suda#executable ==# "sudo" 44 | let cmd = s:get_command(["-n"], ["true"]) 45 | let result = systemlist(cmd) 46 | if v:shell_error == 0 47 | let cmd = s:get_command([], a:cmd) 48 | let ask_pass = 0 49 | endif 50 | endif 51 | if ask_pass == 1 52 | try 53 | call inputsave() 54 | redraw | let password = inputsecret(g:suda#prompt) 55 | finally 56 | call inputrestore() 57 | endtry 58 | let cmd = s:get_command(['-p', '', '-S'], a:cmd) 59 | endif 60 | return systemlist(cmd, password . "\n" . (a:0 ? a:1 : '')) 61 | endfunction 62 | " {cmd} is a argv list for the process 63 | " {input} (a:1) is a string to pass as stdin to the command 64 | " Returns the command's output as a string with NULs replaced with SOH (\u0001) 65 | function! suda#system(cmd, ...) abort 66 | let output = call("suda#systemlist", [a:cmd] + a:000) 67 | " Emulate system()'s handling of output - replace NULs (represented by NL), join by NLs 68 | return join( map(l:output, { k, v -> substitute(v:val, '\n', '', 'g') }), '\n') 69 | endfunction 70 | 71 | 72 | 73 | function! suda#read(expr, ...) abort range 74 | let path = s:strip_prefix(expand(a:expr)) 75 | let path = fnamemodify(path, ':p') 76 | let options = extend({ 77 | \ 'cmdarg': v:cmdarg, 78 | \ 'range': '', 79 | \}, a:0 ? a:1 : {} 80 | \) 81 | 82 | if filereadable(path) 83 | " TODO: (aarondill) can we use readfile() here? 84 | return substitute(execute(printf( 85 | \ '%sread %s %s', 86 | \ options.range, 87 | \ options.cmdarg, 88 | \ fnameescape(path), 89 | \)), '^\r\?\n', '', '') 90 | endif 91 | 92 | let tempfile = tempname() 93 | try 94 | " NOTE: use systemlist to avoid changing newlines. Get the results of the 95 | " command (as a list) to avoid having to spawn a shell to do a redirection 96 | let resultlist = suda#systemlist(['cat', fnamemodify(path, ':p')]) 97 | if v:shell_error 98 | throw resultlist 99 | else 100 | " write with 'b' to ensure contents are the same 101 | call writefile(resultlist, tempfile, 'b') 102 | let echo_message = execute(printf( 103 | \ '%sread %s %s', 104 | \ options.range, 105 | \ options.cmdarg, 106 | \ fnameescape(tempfile), 107 | \)) 108 | " Rewrite message with a correct file name 109 | let echo_message = substitute( 110 | \ echo_message, 111 | \ s:escape_patterns(tempfile), 112 | \ fnamemodify(path, ':~'), 113 | \ 'g', 114 | \) 115 | return substitute(echo_message, '^\r\?\n', '', '') 116 | endif 117 | finally 118 | silent call delete(tempfile) 119 | endtry 120 | endfunction 121 | 122 | function! suda#write(expr, ...) abort range 123 | let path = s:strip_prefix(expand(a:expr)) 124 | let path = fnamemodify(path, ':p') 125 | let options = extend({ 126 | \ 'cmdarg': v:cmdarg, 127 | \ 'cmdbang': v:cmdbang, 128 | \ 'range': '', 129 | \}, a:0 ? a:1 : {} 130 | \) 131 | let tempfile = tempname() 132 | try 133 | let path_exists = !empty(getftype(path)) 134 | " TODO: (aarondill) can we use writefile() here? 135 | let echo_message = execute(printf( 136 | \ '%swrite%s %s %s', 137 | \ options.range, 138 | \ options.cmdbang ? '!' : '', 139 | \ options.cmdarg, 140 | \ fnameescape(tempfile), 141 | \)) 142 | if has('win32') 143 | " In MS Windows, tee.exe has been placed at $VIMRUNTIME and $VIMRUNTIME 144 | " is added to $PATH in Vim process so `executable('tee')` returns 1. 145 | " However, sudo.exe executes a command in a new environment. The 146 | " directory $VIMRUNTIME is not added here, so `tee` is not found. 147 | " Using a full path for tee command to avoid this problem. 148 | let tee_cmd = exepath('tee') 149 | let result = suda#system( 150 | \ [tee_cmd, path], 151 | \ join(readfile(tempfile, 'b'), "\n") ) 152 | else 153 | " `bs=1048576` is equivalent to `bs=1M` for GNU dd or `bs=1m` for BSD dd 154 | " Both `bs=1M` and `bs=1m` are non-POSIX 155 | let result = suda#system(['dd', 'if='.tempfile, 'of='.path, 'bs=1048576']) 156 | endif 157 | if v:shell_error 158 | throw result 159 | endif 160 | " Rewrite message with a correct file name 161 | let echo_message = substitute( 162 | \ echo_message, 163 | \ s:escape_patterns(tempfile), 164 | \ fnamemodify(path, ':~'), 165 | \ 'g', 166 | \) 167 | if path_exists 168 | let echo_message = substitute(echo_message, '\[New\] ', '', 'g') 169 | endif 170 | return substitute(echo_message, '^\r\?\n', '', '') 171 | finally 172 | silent call delete(tempfile) 173 | endtry 174 | " Warn that this file has changed 175 | checktime 176 | endfunction 177 | 178 | function! suda#BufReadCmd() abort 179 | doautocmd BufReadPre 180 | let ul = &undolevels 181 | set undolevels=-1 182 | try 183 | setlocal noswapfile noundofile 184 | let echo_message = suda#read('', { 185 | \ 'range': '1', 186 | \}) 187 | silent 0delete _ 188 | setlocal buftype=acwrite 189 | setlocal nomodified 190 | filetype detect 191 | redraw | echo echo_message 192 | catch 193 | call s:echomsg_exception() 194 | finally 195 | let &undolevels = ul 196 | doautocmd BufReadPost 197 | endtry 198 | endfunction 199 | 200 | function! suda#FileReadCmd() abort 201 | doautocmd FileReadPre 202 | try 203 | " XXX 204 | " A '[ mark indicates the {range} of the command. 205 | " However, the mark becomes 1 even user execute ':0read'. 206 | " So check the last command to find if the {range} was 0 or not. 207 | let range = histget('cmd', -1) =~# '^0r\%[ead]\>' ? '0' : '''[' 208 | redraw | echo suda#read('', { 209 | \ 'range': range, 210 | \}) 211 | catch 212 | call s:echomsg_exception() 213 | finally 214 | doautocmd FileReadPost 215 | endtry 216 | endfunction 217 | 218 | function! suda#BufWriteCmd() abort 219 | doautocmd BufWritePre 220 | try 221 | let echo_message = suda#write('', { 222 | \ 'range': '''[,'']', 223 | \}) 224 | let lhs = expand('%:p') 225 | let rhs = expand('') 226 | if lhs ==# rhs || substitute(rhs, '^suda://', '', '') ==# lhs 227 | setlocal nomodified 228 | endif 229 | redraw | echo echo_message 230 | catch 231 | call s:echomsg_exception() 232 | finally 233 | doautocmd BufWritePost 234 | endtry 235 | endfunction 236 | 237 | function! suda#FileWriteCmd() abort 238 | doautocmd FileWritePre 239 | try 240 | redraw | echo suda#write('', { 241 | \ 'range': '''[,'']', 242 | \}) 243 | catch 244 | call s:echomsg_exception() 245 | finally 246 | doautocmd FileWritePost 247 | endtry 248 | endfunction 249 | 250 | function! suda#BufEnter() abort 251 | if exists('b:suda_smart_edit_checked') 252 | return 253 | endif 254 | let b:suda_smart_edit_checked = 1 255 | let bufname = expand('') 256 | if !empty(&buftype) 257 | \ || empty(bufname) 258 | \ || match(bufname, '^[a-z]\+://*') isnot# -1 259 | \ || isdirectory(bufname) 260 | " Non file buffer 261 | return 262 | elseif filereadable(bufname) && filewritable(bufname) 263 | " File is readable and writeable 264 | return 265 | elseif empty(getftype(bufname)) 266 | " if file doesn't exist, we search for a all directories up it's path to 267 | " see if each one of them is writable, if not, we `return` 268 | let parent = fnamemodify(bufname, ':p') 269 | while parent !=# fnamemodify(parent, ':h') 270 | let parent = fnamemodify(parent, ':h') 271 | if filewritable(parent) is# 2 272 | return 273 | elseif !filereadable(parent) && isdirectory(parent) 274 | break 275 | endif 276 | endwhile 277 | endif 278 | let bufnr = str2nr(expand('')) 279 | execute printf( 280 | \ 'keepalt keepjumps edit suda://%s', 281 | \ fnameescape(fnamemodify(bufname, ':p')), 282 | \) 283 | execute printf('silent! %dbwipeout', bufnr) 284 | endfunction 285 | 286 | function! s:escape_patterns(expr) abort 287 | return escape(a:expr, '^$~.*[]\') 288 | endfunction 289 | 290 | function! s:strip_prefix(expr) abort 291 | return substitute(a:expr, '\v^(suda://)+', '', '') 292 | endfunction 293 | 294 | function! s:echomsg_exception() abort 295 | redraw 296 | echohl ErrorMsg 297 | for line in split(v:exception, '\n') 298 | echomsg printf('[suda] %s', line) 299 | endfor 300 | echohl None 301 | endfunction 302 | 303 | " Pseudo autocmd to suppress 'No such autocmd' message 304 | augroup suda_internal 305 | autocmd! 306 | autocmd BufReadPre,BufReadPost suda://* : 307 | autocmd FileReadPre,FileReadPost suda://* : 308 | autocmd BufWritePre,BufWritePost suda://* : 309 | autocmd FileWritePre,FileWritePost suda://* : 310 | augroup END 311 | 312 | " Configure 313 | " Use suda#noninteractive if defined, else suda#nopass for backwards compatability, default to 0 314 | let g:suda#noninteractive = get(g:, 'suda#noninteractive', get(g:, 'suda#nopass', 0)) 315 | let g:suda#prompt = get(g:, 'suda#prompt', 'Password: ') 316 | let g:suda#executable = get(g:, 'suda#executable', 'sudo') 317 | -------------------------------------------------------------------------------- /doc/suda.txt: -------------------------------------------------------------------------------- 1 | *suda.txt* Use sudo to read/write files in Vim/Neovim 2 | 3 | Author: Alisue 4 | License: MIT license 5 | 6 | ============================================================================= 7 | CONTENTS *suda-contents* 8 | 9 | Introduction |suda-introduction| 10 | Usage |suda-usage| 11 | Smart-edit |suda-usage-smart-edit| 12 | Windows |suda-usage-windows| 13 | Interface |suda-interface| 14 | Command |suda-interface-command| 15 | Function |suda-interface-function| 16 | Variable |suda-interface-variable| 17 | 18 | 19 | ============================================================================= 20 | INTRODUCTION *suda-introduction* 21 | 22 | *suda* is a plugin to read or write files with "sudo" command. 23 | 24 | This plugin was built while ":w !sudo tee % > /dev/null" trick does not work 25 | on Neovim. 26 | 27 | https://github.com/neovim/neovim/issues/1716 28 | 29 | This plugin is strongly inspired by sudo.vim but the interfaces was 30 | aggressively modified for modern Vim script. 31 | 32 | For Windows users, install mattn/sudo command to enable this plugin. 33 | 34 | https://github.com/mattn/sudo 35 | 36 | Make sure that the following shows 1. 37 | > 38 | : echo executable('sudo') 39 | < 40 | |suda| will ask for a password each time sudo is used for reading or writing. 41 | However, you can set global timestamps in your sudoers configuration, if you 42 | have sudo version 1.8.21 or higher. This will enable |suda| to reuse an 43 | existing sudo authentication token. In this case, it will not ask for a 44 | password if not needed. To enable, configure sudo with this option: 45 | > 46 | Defaults timestamp_type = global 47 | < 48 | The other types 'ppid', 'kernel' or 'tty' will not allow |suda| to use sudo 49 | credential caching. Please make sure this is in line with your security 50 | requirements. 51 | 52 | ============================================================================= 53 | USAGE *suda-usage* 54 | 55 | Use |:SudaRead| to open unreadable files like: 56 | > 57 | " Re-open a current file with sudo 58 | :SudaRead 59 | 60 | " Open /etc/sudoers with sudo 61 | :SudaRead /etc/sudoers 62 | < 63 | Or use |:SudaWrite| to write unwritable files like: 64 | > 65 | " Forcedly save a current file with sudo 66 | :SudaWrite 67 | 68 | " Write contents to /etc/profile with sudo 69 | :SudaWrite /etc/profile 70 | < 71 | Or directly use "suda://" prefix on |read|, |edit|, |write|, or |saveas| like: 72 | > 73 | " Open a current file with sudo 74 | :e suda://% 75 | 76 | " Save a current file with sudo 77 | :w suda://% 78 | 79 | " Edit /etc/sudoers 80 | :e suda:///etc/sudoers 81 | 82 | " Read /etc/sudoers (insert content under the cursor) 83 | :r suda:///etc/sudoers 84 | 85 | " Read /etc/sudoers at the end 86 | :$r suda:///etc/sudoers 87 | 88 | " Write contents to /etc/profile 89 | :w suda:///etc/profile 90 | 91 | " Save contents to /etc/profile 92 | :saveas suda:///etc/profile 93 | < 94 | You can change the prompt string by |g:suda#prompt| like: 95 | > 96 | " 'Password' in french 97 | let g:suda#prompt = 'Mot de passe: ' 98 | < 99 | ----------------------------------------------------------------------------- 100 | SMART-EDIT *suda-usage-smart-edit* 101 | 102 | When |g:suda_smart_edit| is true, suda automatically switch a buffer name when 103 | the target file is not readable or writable. 104 | 105 | In short, 106 | > 107 | $ vim /etc/hosts 108 | < 109 | or 110 | > 111 | :e /etc/shadow 112 | < 113 | Will open "suda:///etc/hosts" or "suda:///etc/shadow" instead of "/etc/hosts" 114 | or "/etc/shadow" because that files are not writable or not readable. 115 | 116 | ----------------------------------------------------------------------------- 117 | WINDOWS *suda-usage-windows* 118 | 119 | Install https://github.com/mattn/sudo to enable this plugin in Windows. 120 | Make sure that the following shows 1. 121 | > 122 | : echo executable('sudo') 123 | < 124 | 125 | ============================================================================= 126 | INTERFACE *suda-interface* 127 | 128 | ----------------------------------------------------------------------------- 129 | COMMAND *suda-interface-command* 130 | 131 | *:SudaRead* 132 | :SudaRead [{path}] 133 | Edit the {path} or re-open the current buffer with sudo. 134 | It is equivalent to ":e suda://{path}" 135 | 136 | *:SudaWrite* 137 | :SudaWrite [{path}] 138 | Save content of the current buffer or save it to the {path} with sudo. 139 | It is equivalent to ":w suda://{path}" 140 | 141 | ----------------------------------------------------------------------------- 142 | FUNCTION *suda-interface-function* 143 | 144 | *suda#system()* 145 | suda#system({cmd} [, {input}]) 146 | Like |system()| but execute {cmd} with "sudo". 147 | 148 | *suda#read()* 149 | suda#read({expr} [, {options}]) 150 | Insert contents of {expr} below the cursor. 151 | The following attributes are available on {options} 152 | 153 | "cmdarg" |v:cmdarg| passed to |read| command 154 | "range" {range} passed to |read| command 155 | 156 | A tempfile and |read| command is used to load the content of the 157 | {expr} internally. 158 | 159 | *suda#write()* 160 | suda#write({expr} [, {options}]) 161 | Write contents of current buffer to {expr}. 162 | The following attributes are available on {options} 163 | 164 | "cmdarg" |v:cmdarg| passed to |write| command 165 | "cmdbang" |v:cmdbang|| passed to |write| command 166 | "range" {range} passed to |write| command 167 | 168 | A tempfile and |write| command is used to dump the content of the 169 | buffer internally. 170 | 171 | ----------------------------------------------------------------------------- 172 | VARIABLE *suda-interface-variable* 173 | 174 | *g:suda#executable* 175 | A executable of "sudo" command. 176 | Default: "sudo" 177 | 178 | *g:suda#noninteractive* 179 | If set, suda will not prompt you for a password before saving a file. 180 | It is suppossed to support a setup with passwordless sudo or doas. 181 | Use with care. 182 | Default: 0 183 | 184 | *g:suda#prompt* 185 | A prompt string used to ask password. 186 | Default: "Password: " 187 | 188 | *g:suda_smart_edit* 189 | If set, an |autocmd| is created that performs a heuristic check on 190 | every buffer and decides whether to replace it with a suda buffer. 191 | The check is done only once for every buffer and it is designed to be 192 | optimized as possible so you shouldn't feel any slowdown when openning 193 | buffers. 194 | Default: 0 195 | 196 | 197 | ============================================================================= 198 | vim:tw=78:fo=tcq2mM:ts=8:ft=help:norl 199 | -------------------------------------------------------------------------------- /plugin/suda.vim: -------------------------------------------------------------------------------- 1 | if exists('g:loaded_suda') 2 | finish 3 | endif 4 | let g:loaded_suda = 1 5 | 6 | if get(g:, 'suda_smart_edit') 7 | augroup suda_smart_edit 8 | autocmd! 9 | autocmd BufEnter * nested call suda#BufEnter() 10 | augroup end 11 | endif 12 | 13 | augroup suda_plugin 14 | autocmd! 15 | autocmd BufReadCmd suda://* call suda#BufReadCmd() 16 | autocmd FileReadCmd suda://* call suda#FileReadCmd() 17 | autocmd BufWriteCmd suda://* call suda#BufWriteCmd() 18 | autocmd FileWriteCmd suda://* call suda#FileWriteCmd() 19 | augroup END 20 | 21 | function! s:read(args) abort 22 | let args = empty(a:args) ? expand('%:p') : a:args 23 | execute printf('edit suda://%s', fnameescape(args)) 24 | endfunction 25 | command! -nargs=? -complete=file SudaRead call s:read() 26 | 27 | function! s:write(args) abort 28 | let args = empty(a:args) ? expand('%:p') : a:args 29 | execute printf('write suda://%s', fnameescape(args)) 30 | endfunction 31 | command! -nargs=? -complete=file SudaWrite call s:write() 32 | --------------------------------------------------------------------------------