0

I have a script that works fine in sh on a linux host as well as inside an alpine container. But when I try executing that using docker exec <containerID> sh -c "<script>" it misbehaves. The script's function is to output stuff similar to ps.

systick=$(getconf CLK_TCK); for c in /proc/*/cmdline; do d=$(dirname $c); name=$(grep Name: $d/status); pid=$(basename $d); uid=$(grep Uid: $d/status); uid=$(echo ${uid#Uid:} | xargs); uid=${uid%% *}; user=$(grep :$uid:[0-9] /etc/passwd); user=${user%%:*}; cmdline=$(cat $c|xargs -0 echo); starttime=$(($(awk '{print $22}' $d/stat) / systick)); uptime=$(awk '{print int($1)}' /proc/uptime); elapsed=$(($uptime-$starttime)); echo $pid $user $elapsed $cmdline; done

EDIT: sh -c "<script>" has the same behavior.

Charles Duffy
  • 280,126
  • 43
  • 390
  • 441
Sam Thomas
  • 647
  • 7
  • 25
  • Sure, when you are more explicit with exactly what is wrong. Saying a script or a command is misbehaving is probably as vague as it can get. – alex067 Mar 04 '20 at 23:48
  • @alex067 doesn't seem like the case. I have given everything that is enough to reproduce the problem. as for the exact failure - i didn't state it because the message varies between shells and operating system. I tried on ubuntu with an ubuntu container, ubuntu with an alpine container. on alpine itself and more. Got an answer too. Thanks for the help though! Much appreciated – Sam Thomas Mar 04 '20 at 23:52

2 Answers2

2

You are not able to run this script from docker exec because the variables will be interpolated before they sent to the container (i.e., you are going to get values from your local machine, not from within the container).

In order to run it as you wish, you need to replace $ with \$ for every occurrence of $ in your script.

What might work better is to put your script into a file, then map the file to a location within the container, using -v (i.e., -v script.sh:/path/to/script.sh), and call the script via docker exec /path/to/script.sh

richyen
  • 8,114
  • 4
  • 13
  • 28
1

Part 1: A Working Answer

A Working One-Liner (Quoted For Use By Docker)

getProcessDataDef='shellQuoteWordsDef='"'"'shellQuoteWords() { sq="'"'"'"'"'"'"'"'"'"; dq='"'"'"'"'"'"'"'"'"'"'"'"'"'"'"'"'"'; for arg; do printf "'"'"'"'"'"'"'"'"'%s'"'"'"'"'"'"'"'"' " "$(printf '"'"'"'"'"'"'"'"'%s\n'"'"'"'"'"'"'"'"' "$arg" | sed -e "s@${sq}@${sq}${dq}${sq}${dq}${sq}@g")"; done; printf '"'"'"'"'"'"'"'"'\n'"'"'"'"'"'"'"'"'; }'"'"'; shellQuoteNullSeparatedStream() { xargs -0 sh -c "${shellQuoteWordsDef};"'"'"' shellQuoteWords "$@"'"'"' _; }; getProcessData() { systick=$(getconf CLK_TCK); for c in /proc/*/cmdline; do d=${c%/*}; pid=${d##*/}; name=$(awk '"'"'/^Name:/ { print $2 }'"'"' <"$d"/status); uid=$(awk '"'"'/^Uid:/ { print $2 }'"'"' <"$d"/status); pwent=$(getent passwd "$uid"); user=${pwent%%:*}; cmdline=$(shellQuoteNullSeparatedStream <"$c"); starttime=$(awk -v systick="$systick" '"'"'{print int($22 / systick)}'"'"' "$d"/stat); uptime=$(awk '"'"'{print int($1)}'"'"' /proc/uptime); elapsed=$((uptime-starttime)); echo "$pid $user $elapsed $cmdline"; done; }; getProcessData'
sh -c "$getProcessDataDef"  # or docker exec <container> sh -c "$getProcessDataDef"

A Working One-Liner (Before Quoting/Escaping)

shellQuoteWordsDef='shellQuoteWords() { sq="'"'"'"; dq='"'"'"'"'"'; for arg; do printf "'"'"'%s'"'"' " "$(printf '"'"'%s\n'"'"' "$arg" | sed -e "s@${sq}@${sq}${dq}${sq}${dq}${sq}@g")"; done; printf '"'"'\n'"'"'; }'; shellQuoteNullSeparatedStream() { xargs -0 sh -c "${shellQuoteWordsDef};"' shellQuoteWords "$@"' _; }; getProcessData() { systick=$(getconf CLK_TCK); for c in /proc/*/cmdline; do d=${c%/*}; pid=${d##*/}; name=$(awk '/^Name:/ { print $2 }' <"$d"/status); uid=$(awk '/^Uid:/ { print $2 }' <"$d"/status); pwent=$(getent passwd "$uid"); user=${pwent%%:*}; cmdline=$(shellQuoteNullSeparatedStream <"$c"); starttime=$(awk -v systick="$systick" '{print int($22 / systick)}' "$d"/stat); uptime=$(awk '{print int($1)}' /proc/uptime); elapsed=$((uptime-starttime)); echo "$pid $user $elapsed $cmdline"; done; }; getProcessData "$@"

What Went Into That One-Liner

shellQuoteWordsDef='shellQuoteWords() { sq="'"'"'"; dq='"'"'"'"'"'; for arg; do printf "'"'"'%s'"'"' " "$(printf '"'"'%s\n'"'"' "$arg" | sed -e "s@${sq}@${sq}${dq}${sq}${dq}${sq}@g")"; done; printf '"'"'\n'"'"'; }'

shellQuoteNullSeparatedStream() {
  xargs -0 sh -c "${shellQuoteWordsDef};"' shellQuoteWords "$@"' _
}

getProcessData() {
  systick=$(getconf CLK_TCK)
  for c in /proc/*/cmdline; do
    d=${c%/*}; pid=${d##*/}
    name=$(awk '/^Name:/ { print $2 }' <"$d"/status)
    uid=$(awk '/^Uid:/ { print $2 }' <"$d"/status)
    pwent=$(getent passwd "$uid")
    user=${pwent%%:*}
    cmdline=$(shellQuoteNullSeparatedStream <"$c")
    starttime=$(awk -v systick="$systick" '{print int($22 / systick)}' "$d"/stat)
    uptime=$(awk '{print int($1)}' /proc/uptime)
    elapsed=$((uptime-starttime))
    echo "$pid $user $elapsed $cmdline"
  done
}

What Went Into The Shell-Quoting Helper Used By That One-Liner

To allow easier reading and editing, the function stringified above looks like:

# This is the function we're including in our code passed to xargs in-band above:
shellQuoteWords() {
  sq="'"; dq='"'
  for arg; do
    printf "'%s' " "$(printf '%s\n' "$arg" | sed -e "s@${sq}@${sq}${dq}${sq}${dq}${sq}@g")"
  done
  printf '\n'
}

Part 2: How That Answer Was Created

Python has an excellent shlex.quote() function (or pipes.quote() in Python 2) that can be used to generate a shell-quoted version of a string. In this context, that can be used as follows:

Python 3.7.6 (default, Feb 27 2020, 15:15:00)
[Clang 7.1.0 (tags/RELEASE_710/final)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> s = r'''
... shellQuoteWords() {
...   sq="'"; dq='"'
...   for arg; do
...     printf "'%s' " "$(printf '%s\n' "$arg" | sed -e "s@${sq}@${sq}${dq}${sq}${dq}${sq}@g")"
...   done
...   printf '\n'
... }
... '''
>>> import shlex
>>> print(shlex.quote(s))
'
shellQuoteWords() {
  sq="'"'"'"; dq='"'"'"'"'"'
  for arg; do
    printf "'"'"'%s'"'"' " "$(printf '"'"'%s\n'"'"' "$arg" | sed -e "s@${sq}@${sq}${dq}${sq}${dq}${sq}@g")"
  done
  printf '"'"'\n'"'"'
}
'

That result is itself a perfectly valid string in shell. That is to say, one can run:

s='
shellQuoteWords() {
  sq="'"'"'"; dq='"'"'"'"'"'
  for arg; do
    printf "'"'"'%s'"'"' " "$(printf '"'"'%s\n'"'"' "$arg" | sed -e "s@${sq}@${sq}${dq}${sq}${dq}${sq}@g")"
  done
  printf '"'"'\n'"'"'
}
'
eval "$s"
shellQuoteWords "hello world" 'hello world' "hello 'world'" 'hello "world"'

...and get completely valid output.

The same process was followed to generate a string that evaluated to the definition of getProcessData.

Charles Duffy
  • 280,126
  • 43
  • 390
  • 441
  • @SamThomas, ...btw, was I correct to assume that `getent` would be available on your target system? If not, it might make sense to switch to `user=$(id -u "$uid")`. (`getent`, while a Linuxism, is generally more robust than reading `/etc/passwd` directly because it works even on systems with a different NSS module providing directory service). – Charles Duffy Mar 18 '20 at 02:01
  • Thats a good point, It is there, but it's good to have an alternative. Thanks Charles for spending your evening on this. – Sam Thomas Mar 18 '20 at 02:36
  • @SamThomas, no worries, it was an interesting puzzle. BTW, I think I never followed through on my promise in the chat to describe how `cmdline=$(cat $c|xargs -0 echo)` could be unfaithful. The simplest example is probably `printf '%s\0' mv "hello world" "goodbye world" | xargs -0 echo` -- see how it spits out `mv hello world goodbye world`, which will do a very different thing than the original `mv "hello world" "goodbye world"` command. – Charles Duffy Mar 18 '20 at 14:51
  • 1
    By contrast, with the function definition from this answer, `printf '%s\0' mv "hello world" "goodbye world" | shellQuoteNullSeparatedStream` emits `'mv' 'hello world' 'goodbye world'`, which will actually do the same thing when run as the original command. – Charles Duffy Mar 18 '20 at 14:53
  • Aaaah!! Now it makes perfect sense – Sam Thomas Mar 18 '20 at 15:41