dockershdocker-exec

Passing a complex shell script via docker exec sh -c "..."


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.


Solution

  • 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.