first i should say that i assume a standard pc linux distribution for all that follows. for example, i am using a more-or-less default arch linux installation while testing.
ps -ocommand | grep \[x]ss-lock
...will first print series of running process command lines and then filter that list to only those that match the regex \[x]ss-lock
. the \[x]...
used in the regex is a fairly common work-around for handling the results race involved with listing a bunch of processes named xss-lock
by filtering another list with a command containing the words xss-lock
.
that's not the best way to do it, though. a linux system's ps
is typically a procps-ng ps
which supports the -C
ommand name filter, which, as I have been told, likely originated with AIX's ps
.
in any case, you can drop part of the race like:
ps -C xss-lock -ocommand
...to print only those command-lines for running processes if the command name (which you might also list like ps -Aocomm=
) matches xss-lock
entirely - as a grep -Fx
filter might do. to actually filter with regexes there is also the pgrep
tool which can do similarly race-free regex matching.
eponymously, the procps-ng ps
operates by parsing files in the /proc
tree. for your ps -ocommand
, for example, it reads the /proc/$pid/cmdline
file for each match it should print and replaces the \0
NULs (less the last - which becomes a \n
ewline instead) therein (which delimit each argument in the file) with a space each.
if you want shell-quoted argument lists you'll have to do similar. the most simple way to shell quote any list is to do it with '
hard-quotes. it is most simple because a hard-quoted string is always flat - there's no depth-recursion in parsing because a hard-quoted string cannot contain another hard-quote. so...
'it'\''s not a single string'
...is not a single hard-quoted string, but is three concatenated strings. the first, it
is surrounded on both sides by hard-quotes; the second is a single, backspace-quoted apostrophe; and the third, s not a single string, is hard-quoted like the first. the shell combines all three quoted-strings into a single shell word when parsing.
we can do the same with a /proc/$pid/cmdline
file. we need to split each file on \0
NUL bytes, pre-quote every apostrophe found therein like '\''
, then surround every object with apostrophes and insert one or more \t
ab or space characters between each.
now that i'm thinking about it, my first version of this was both needlessly complicated and, as a result, actually prone to error. intuitively i attempted to hard-quote each command's arguments - so all objects for a single /proc/$pid/file
less the first - but that doesn't work (and in, fact, completely inverts the entire resulting command-line's quote-level) if the command name contains an odd number of apostrophes. and it doesn't matter anyway - 'cat'
works just as well as cat
when run at a command-line. in fact, it works better because the first is a lot more likely to be the cat
found in the /proc/$pid/file
than otherwise if shell-aliases might be interpreted (as would happen with cat
).
so i did a similar thing to meuh's perl script, but with GNU tools. specifically, the sed -z
option instructs a GNU sed
to split input on \0
NUL bytes rather than \n
ewlines, and its -s
option instructs to treat each file argument as a separate input stream (so the $
last line might be individually referenced for each argument and the H
old space is reinitialized for each). the ps -Csh -opid=
prints a single pid number per line for every running process matching the command name sh
, and sed "s|$|/cmdline|"
just appends that pathname to each.
because $IFS
is unset
, the $(cmdsub's)
output will definitely be split by the shell into separate arguments on white-space - which is only a single \n
ewline per pathname - and the sed -sze
process gets an argument list of all of the /proc/$pid/cmdline
pathnames which existed at the time ps -C
went looking for them. this last bit reintroduces the whole race game, which is made apparent by the error message printed when sed -sze
tries to read the cmdline
file for the command sub's sh
process which no longer exists - as it has quit by the time sed -sze
is called at all.
sh -c '
cd /proc; unset IFS
sed -sze "H;1h;$"\!"d;x" \
-e "s/$1/&\\\\&&/g" \
-e "s/\x00/$1 $1/g" \
-e "s/.*/$1&$1\n/" \
$( ps -Csh -opid= |
sed "s|$|/cmdline|") /dev/null
' -- \'
'./sh' '-IE'
'sh'
'./sh' '-E'
'../sh'
'sh' '-c' '
cd /proc; unset IFS
sed -sze "H;1h;$"\!"d;x" \
-e "s/$1/&\\\\&&/g" \
-e "s/\x00/$1 $1/g" \
-e "s/.*/$1&$1\n/" \
$( ps -Csh -opid= |
sed "s|$|/cmdline|") /dev/null
' '--' ''\'''
...i went through the sh -c
embedded quoting hassle in the first place for testing, and for demonstration purposes - i needed a process that would hang around with a lot of spaced-out arguments while i ran the test.
if I drop that last empty line of input and end the quoted command just after /dev/null
(which is used to keep the sed -sze
process from hijacking stdin in case of no ps -Csome_process
results), the sh -c
process (on a system with a dash
sh
) will merely exec
sed -sze
and replace itself with it rather than waiting to check the next line of input. in that case sed -sze
will manage to read its own /proc/$pid/cmdline
file - because it retains sh -c
's pid:
sh -c '
cd /proc; unset IFS
sed -sze "H;1h;$"\!"d;x" \
-e "s/$1/&\\\\&&/g" \
-e "s/\x00/$1 $1/g" \
-e "s/.*/$1&$1\n/" \
$( ps -Csh -opid= |
sed "s|$|/cmdline|") /dev/null' -- \'
'./sh' '-IE'
'sh'
'./sh' '-E'
'../sh'
'sed' '-sze' 'H;1h;$!d;x' '-e' 's/'\''/&\\&&/g' '-e' 's/\x00/'\'' '\''/g' '-e' 's/.*/'\''&'\''\n/' '2508/cmdline' '3773/cmdline' '5099/cmdline' '26599/cmdline' '31487/cmdline' '31488/cmdline' '31881/cmdline' '/dev/null'
sed: can't read 31488/cmdline: No such file or directory
'sh'
Here is a similar version, but it individually quotes each command entire, and stacks the escaped hard-quotes within each another layer deep:
eval "set $(
sh -c '
cd /proc; unset IFS
sed -sze "H;$"\!"d;x" \
-e "s/$1/$2\\\\$2$2/g" \
-e "s/\x00\([^\x00]*\)/$2\1$2 /g"\
-e "s/.*/$1&$1\\\\\n /" \
$( ps -Csh -opid= |
sed "s|$|/cmdline|") /dev/null
' -- \' "'\\\''")"
i=
for arg do printf "$arg $((i+=1)):\t%s\n" "$arg"
done; eval "$5"
arg 1: './sh' '-IE'
arg 2: 'sh'
arg 3: './sh' '-E'
arg 4: '../sh'
arg 5: 'sh' '-c' '
cd /proc; unset IFS
sed -sze "H;$"\!"d;x" \
-e "s/$1/$2\\\\$2$2/g" \
-e "s/\x00\([^\x00]*\)/$2\1$2 /g"\
-e "s/.*/$1&$1\\\\\n /" \
$( ps -Csh -opid= |
sed "s|$|/cmdline|") /dev/null
' '--' ''\''' ''\''\\'\'''\'''
arg 6: 'sh'
''\''./sh'\'' '\''-IE'\'' '\
''\''sh'\'' '\
''\''./sh'\'' '\''-E'\'' '\
''\''../sh'\'' '\
''\''sh'\'' '\''-c'\'' '\''
cd /proc; unset IFS
sed -sze "H;$"\!"d;x" \
-e "s/$1/$2\\\\$2$2/g" \
-e "s/\x00\([^\x00]*\)/$2\1$2 /g"\
-e "s/.*/$1&$1\\\\\n /" \
$( ps -Csh -opid= |
sed "s|$|/cmdline|") /dev/null
'\'' '\''--'\'' '\'''\''\'\'''\'''\'' '\'''\''\'\'''\''\\'\''\'\'''\'''\''\'\'''\'''\'' '\
sed: can't read 31725/cmdline: No such file or directory
''\''sh'\'' '
ps
. – Jonathan Leffler Nov 15 '15 at 15:06