Skip to content

Commit 2f5fc67

Browse files
authored
refactor!: change suda#system to a list-based api (#74)
* refactor!: change suda#system to a list-based api This eliminates the need for shell-escaping, and removes possibility of accidental shell-injection. This is technically a breaking change, since suda#system is globally exposed; however this is *not* a documented public api, so the chance of breakage is minimal. Note that this *changes the behavior of g:suda#executeable! It is no longer shell-processed, but rather treated as a path/name to an executable and processed by the os in that way. Should we split suda#executable on spaces to preserve the old behavior? Should we allow suda#executable to (optionally) be a list to allow passing arguments? * refactor: implement suda#systemlist This replaces the 3 argument suda#system implementation and follows the vimscript implementation. * fix: #system passes empty list to systemlist use call() instead to ensure that it passes the right amount of args
1 parent 66727b4 commit 2f5fc67

File tree

1 file changed

+48
-35
lines changed

1 file changed

+48
-35
lines changed

autoload/suda.vim

Lines changed: 48 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,30 @@
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 '--'
16
function! s:get_command(opts, cmd)
2-
" TODO: should we pass '--' between a:opts and a:cmd?
3-
" TODO: should we change this api to use lists? system() allows either
4-
" strings or lists. We don't need a intermediate shell for anything though.
5-
" TODO: Should we move shell escaping to the responsibility of
6-
" suda#system/s:get_command to avoid forgetting it at the call site?
7-
return g:suda#executable ==# "sudo" && len(a:opts) > 0
8-
\ ? printf('%s %s %s', g:suda#executable, a:opts, a:cmd)
9-
\ : printf('%s %s', g:suda#executable, a:cmd)
7+
if g:suda#executable ==# 'sudo'
8+
return [g:suda#executable] + a:opts + ['--'] + a:cmd
9+
endif
10+
" TODO:
11+
" Should we pass '--' before cmd when using a custom suda#executable?
12+
" Should suda#executable be split? Should we allow suda#executable to be a list instead?
13+
" This behavior is entirely undocumented
14+
return [g:suda#executable] + a:cmd
1015
endfunction
1116

12-
function! suda#system(cmd, ...) abort
17+
" {cmd} is a argv list for the process
18+
" {input} (a:1) is a string to pass as stdin to the command
19+
" Returns a list of the command's output, split by NLs, with NULs replaced with NLs.
20+
function! suda#systemlist(cmd, ...) abort
1321
let cmd = has('win32') || g:suda#nopass
14-
\ ? s:get_command('', a:cmd)
15-
\ : s:get_command('-p '''' -n', a:cmd)
22+
\ ? s:get_command([], a:cmd)
23+
\ : s:get_command(['-p', '', '-n'], a:cmd)
1624
if &verbose
1725
echomsg '[suda]' cmd
1826
endif
19-
let result = a:0 ? system(cmd, a:1) : system(cmd)
27+
let result = a:0 ? systemlist(cmd, a:1) : systemlist(cmd)
2028
if v:shell_error == 0
2129
return result
2230
endif
@@ -28,24 +36,34 @@ function! suda#system(cmd, ...) abort
2836
" configuation file. It does not work with 'ppid', 'kernel' or 'tty'.
2937
" Note: for non-sudo commands, don't do this, instead *always* ask for the password
3038
if g:suda#executable ==# "sudo"
31-
let cmd = s:get_command("-n", "true")
32-
let result = system(cmd)
39+
let cmd = s:get_command(["-n"], ["true"])
40+
let result = systemlist(cmd)
3341
if v:shell_error == 0
34-
let cmd = s:get_command('', a:cmd)
42+
let cmd = s:get_command([], a:cmd)
3543
let ask_pass = 0
3644
endif
3745
endif
3846
if ask_pass == 1
3947
try
4048
call inputsave()
41-
redraw | let password = inputsecret(g:suda#prompt)
49+
redraw | let password = inputsecret(g:suda#prompt)
4250
finally
4351
call inputrestore()
4452
endtry
45-
let cmd = s:get_command('-p '''' -S', a:cmd)
53+
let cmd = s:get_command(['-p', '', '-S'], a:cmd)
4654
endif
47-
return system(cmd, password . "\n" . (a:0 ? a:1 : ''))
55+
return systemlist(cmd, password . "\n" . (a:0 ? a:1 : ''))
4856
endfunction
57+
" {cmd} is a argv list for the process
58+
" {input} (a:1) is a string to pass as stdin to the command
59+
" Returns the command's output as a string with NULs replaced with SOH (\u0001)
60+
function! suda#system(cmd, ...) abort
61+
let output = call("suda#systemlist", [a:cmd] + a:000)
62+
" Emulate system()'s handling of output - replace NULs (represented by NL), join by NLs
63+
return join( map(l:output, { k, v -> substitute(v:val, '\n', '', 'g') }), '\n')
64+
endfunction
65+
66+
4967

5068
function! suda#read(expr, ...) abort range
5169
let path = s:strip_prefix(expand(a:expr))
@@ -57,6 +75,7 @@ function! suda#read(expr, ...) abort range
5775
\)
5876

5977
if filereadable(path)
78+
" TODO: (aarondill) can we use readfile() here?
6079
return substitute(execute(printf(
6180
\ '%sread %s %s',
6281
\ options.range,
@@ -67,17 +86,15 @@ function! suda#read(expr, ...) abort range
6786

6887
let tempfile = tempname()
6988
try
70-
let redirect = &shellredir =~# '%s'
71-
\ ? printf(&shellredir, shellescape(tempfile))
72-
\ : &shellredir . shellescape(tempfile)
73-
let result = suda#system(printf(
74-
\ 'cat %s %s',
75-
\ shellescape(fnamemodify(path, ':p')),
76-
\ redirect,
77-
\))
89+
" NOTE: use systemlist to avoid changing newlines. Get the results of the
90+
" command (as a list) to avoid having to spawn a shell to do a redirection
91+
let resultlist = suda#systemlist(['cat', fnamemodify(path, ':p')])
7892
if v:shell_error
79-
throw result
93+
throw resultlist
8094
else
95+
" write with 'b' to ensure contents are the same
96+
call writefile(resultlist, tempfile, 'b')
97+
" TODO: (aarondill) can we use readfile() here?
8198
let echo_message = execute(printf(
8299
\ '%sread %s %s',
83100
\ options.range,
@@ -110,6 +127,7 @@ function! suda#write(expr, ...) abort range
110127
let tempfile = tempname()
111128
try
112129
let path_exists = !empty(getftype(path))
130+
" TODO: (aarondill) can we use writefile() here?
113131
let echo_message = execute(printf(
114132
\ '%swrite%s %s %s',
115133
\ options.range,
@@ -125,17 +143,12 @@ function! suda#write(expr, ...) abort range
125143
" Using a full path for tee command to avoid this problem.
126144
let tee_cmd = exepath('tee')
127145
let result = suda#system(
128-
\ printf('%s %s', shellescape(tee_cmd), shellescape(path)),
129-
\ join(readfile(tempfile, 'b'), "\n")
130-
\)
146+
\ [tee_cmd, path],
147+
\ join(readfile(tempfile, 'b'), "\n") )
131148
else
132149
" `bs=1048576` is equivalent to `bs=1M` for GNU dd or `bs=1m` for BSD dd
133150
" Both `bs=1M` and `bs=1m` are non-POSIX
134-
let result = suda#system(printf(
135-
\ 'dd if=%s of=%s bs=1048576',
136-
\ shellescape(tempfile),
137-
\ shellescape(path)
138-
\))
151+
let result = suda#system(['dd', 'if='.tempfile, 'of='.path, 'bs=1048576'])
139152
endif
140153
if v:shell_error
141154
throw result

0 commit comments

Comments
 (0)