Gert Gottschalk
Gert Gottschalk

Reputation: 1716

Escaping shell cmd line nested quotes

I have a lot of background jobs running that are feeding their respective log files. Using the watch command I am monitoring the respective last line in the log as follows.

watch "ls -rtdc1 base_*.log | tail -20 | xargs -I % sh -c 'tail -vn1 % | cut -c1-180'"

That looks nice, however it has the file names from tail -v and following line from the log with a line break. I want to keep both on the same line. The following little awk cmd will just combine two lines and it tests out fine individually.

awk 'NR%2{printf "%s ",$0;next;}1'

Ok, now combining the two is the challenge. A lot of single and double quotes to watch out for. I tried the following line at it failed.

watch "ls -rtdc1 base_*.log | tail -20 | xargs -I % sh -c 'tail -vn1 % | awk \'NR%2{printf "%s ",$0;next;}1\' | cut -c1-180'"

So my question is to find the proper escaping sequence for the cmd line.

Your feedback is much appreciated.

Upvotes: 2

Views: 141

Answers (2)

Ole Tange
Ole Tange

Reputation: 33685

Charles has a good answer for your specific problem. If you are asking the general problem of quoting, then GNU Parallel can do that for you:

$ parallel --shellquote
NR%2{printf "%s ",$0;next;}1
[Ctrl-D]

so awk NR%2\{printf\ \"%s\ \",\$0\;next\;\}1 should work. Next is to quote the command for sh -c:

$ parallel --shellquote
tail -vn1 % | awk NR%2\{printf\ \"%s\ \",\$0\;next\;\}1 | cut -c1-180
[Ctrl-D]

Giving:

tail\ -vn1\ %\ \|\ awk\ NR%2\\\{printf\\\ \\\"%s\\\ \\\",\\\$0\\\;next\\\;\\\}1\ \|\ cut\ -c1-180

However, since you use % in the awk command, you need to use another replacement string for xargs. Let us use {}:

ls -rtdc1 base_*.log | tail -20 | xargs -I {} sh -c tail\ -vn1\ {}\ \|\ awk\ NR%2\\\{printf\\\ \\\"%s\\\ \\\",\\\$0\\\;next\\\;\\\}1\ \|\ cut\ -c1-180

Finally to quote it for watch:

$ parallel --shellquote
ls -rtdc1 base_*.log | tail -20 | xargs -I {} sh -c tail\ -vn1\ {}\ \|\ awk\ NR%2\\\{printf\\\ \\\"%s\\\ \\\",\\\$0\\\;next\\\;\\\}1\ \|\ cut\ -c1-180
[Ctrl-D]

Giving:

ls\ -rtdc1\ base_\*.log\ \|\ tail\ -20\ \|\ xargs\ -I\ \{\}\ sh\ -c\ tail\\\ -vn1\\\ \{\}\\\ \\\|\\\ awk\\\ NR%2\\\\\\\{printf\\\\\\\ \\\\\\\"%s\\\\\\\ \\\\\\\",\\\\\\\$0\\\\\\\;next\\\\\\\;\\\\\\\}1\\\ \\\|\\\ cut\\\ -c1-180

This should therefore work:

watch ls\ -rtdc1\ base_\*.log\ \|\ tail\ -20\ \|\ xargs\ -I\ \{\}\ sh\ -c\ tail\\\ -vn1\\\ \{\}\\\ \\\|\\\ awk\\\ NR%2\\\\\\\{printf\\\\\\\ \\\\\\\"%s\\\\\\\ \\\\\\\",\\\\\\\$0\\\\\\\;next\\\\\\\;\\\\\\\}1\\\ \\\|\\\ cut\\\ -c1-180

Would you ever write it this way by hand? Probably not. But the whole idea is to make the computer work for you - not the other way around.

Upvotes: 1

Charles Duffy
Charles Duffy

Reputation: 295288

The Right Thing is to structure your code in such a way that you don't need to quote your code at all to make it be treated literally.

Consider the following (noting that everywhere I have a Function definition goes here placeholder below, it should be replaced with the myfn definition given, or something equivalent thereto):

#!/usr/bin/env bash
#              ^^^^-- needed for exported functions

myfn() {
  # aside: parsing ls is a Really Bad Idea; don't do this.
  # See https://mywiki.wooledge.org/ParsingLs for discussion of why not.
  # See https://mywiki.wooledge.org/BashFAQ/003 for alternate practices.
  ls -rtdc1 base_*.log \
  | tail -20 \
  | while read -r line; do
      tail -v1 "$line" \
      | awk 'NR%2 {printf "%s ",$0; next; }1' \
      | cut -c1-180
    done
}
export -f myfn
watch 'bash -c myfn'

Alternately, without the export -f (and requiring bash only as the outer shell, not the inner one):

#!/usr/bin/env bash
#              ^^^^- needed for declare -f

myfn() { echo "Function definition goes here"; }

watch "$(declare -f myfn); myfn"

Alternately, requiring only /bin/sh:

#!/bin/sh

cat_myfn() {
  cat <<'EOF'
myfn() { echo "Function definition goes here"; }
EOF
}

watch "$(cat_myfn)
myfn"

Upvotes: 1

Related Questions