How to Properly Quote This Bash Pipeline for Watch

How do I properly quote this bash pipeline for watch?

The problem is about the single quotes for awk, you could fix it by escaping single quotes.

watch 'echo "scale=2;$(cat io | grep wchar | awk '"'"'{print $2}'"'"')/(1024^3)" | bc'

It is all about how to escaping single quotes inside single quotes, there is a good explanation "BASH, escaping single-quotes inside of single-quoted strings"

The watch command does not work with a command using a pipe

If you want to pipe a command and watch that, add quotes around it as such:

watch "ls | wc -l"

Add aliases for a watch command having pipe

alias fdbtop='watch -tc '\''fdbcli --exec "status json" | python /home/ubuntu/build-target/healthservice/fdbtop.py'\'

Within a string in single quotes no character is special. To insert a single quote in a string in single quotes you must use '\''. (The first ' terminates the string, \' adds a single quote, the last ' opens the string again.)

Actively display elapsed time using watch in bash

This is a basic script doing what you want, I presume:

#!/bin/bash

convertsecs() { #Function used to convert seconds into HH:MM:SS
((h=${1}/3600))
((m=(${1}%3600)/60))
((s=${1}%60))
printf "%02d:%02d:%02d\n" $h $m $s
}
typeset -fx convertsecs # export to sub-shells
# maybe this is implicit with your version of Bash

NOW=$(exec date +%s)

cmd() {
echo it"'"s fact of life than shell # Replace by *your*
echo quoting can be a '"nightmare"' # actual commands
}
typeset -fx cmd

watch -n 1 \
'bash -c '"'"'cat <(cmd) \
<(echo Elapsed $(convertsecs $(($(exec date +%s) - '$NOW'))))'"'"

The key issue is to properly quote the command passed to watch. In addition, I've exported the function convertsecs so it is available in sub-shells.

Notice how I wrap the "user commands" in a function. That way, I don't have to worry about proper quoting of those commands when passed as an argument to watch.

Moreover, here I used cat with process substitution to append the line "Elapsed ..." after the output of my main command. I made that choice to clearly separate the "useful stuff" (cmd) from the "cosmetic stuff" (Elapsed ...).

How can I make the watch command interpret vt100 sequences?

Edit:

More recent versions of watch support color. You will need to use an extra level of quoting to preserve the quotes and escapes in the particular situation of the example in the question:

watch 'echo -e "\033[31mHello World\033[0m"'

From man watch:

  -c, --color
Interpret ANSI color sequences.

Previously:

From man watch:

Non-printing characters are stripped from program output. Use "cat -v"
as part of the command pipeline if you want to see them.

But they don't get interpreted, so I don't think there's any way.

Why piping input to read only works when fed into while read ... construct?

How to do a loop against stdin and get result stored in a variable

Under bash (and other shell also), when you pipe something to another command via |, you will implicitly create a fork, a subshell that is a child of current session. The subshell can't affect current session's environment.

So this:

TOTAL=0
printf "%s %s\n" 9 4 3 1 77 2 25 12 226 664 |
while read A B;do
((TOTAL+=A-B))
printf "%3d - %3d = %4d -> TOTAL= %4d\n" $A $B $[A-B] $TOTAL
done
echo final total: $TOTAL

won't give expected result! :

  9 -   4 =    5 -> TOTAL=    5
3 - 1 = 2 -> TOTAL= 7
77 - 2 = 75 -> TOTAL= 82
25 - 12 = 13 -> TOTAL= 95
226 - 664 = -438 -> TOTAL= -343
echo final total: $TOTAL
final total: 0

Where computed TOTAL could'nt be reused in main script.

Inverting the fork

By using bash Process Substitution, Here Documents or Here Strings, you could inverse the fork:

Here strings

read A B <<<"first second"
echo $A
first

echo $B
second

Here Documents

while read A B;do
echo $A-$B
C=$A-$B
done << eodoc
first second
third fourth
eodoc
first-second
third-fourth

outside of the loop:

echo : $C
: third-fourth

Here Commands

TOTAL=0
while read A B;do
((TOTAL+=A-B))
printf "%3d - %3d = %4d -> TOTAL= %4d\n" $A $B $[A-B] $TOTAL
done < <(
printf "%s %s\n" 9 4 3 1 77 2 25 12 226 664
)
9 - 4 = 5 -> TOTAL= 5
3 - 1 = 2 -> TOTAL= 7
77 - 2 = 75 -> TOTAL= 82
25 - 12 = 13 -> TOTAL= 95
226 - 664 = -438 -> TOTAL= -343

# and finally out of loop:
echo $TOTAL
-343

Now you could use $TOTAL in your main script.

Piping to a command list

But for working only against stdin, you may create a kind of script into the fork:

printf "%s %s\n" 9 4 3 1 77 2 25 12 226 664 | {
TOTAL=0
while read A B;do
((TOTAL+=A-B))
printf "%3d - %3d = %4d -> TOTAL= %4d\n" $A $B $[A-B] $TOTAL
done
echo "Out of the loop total:" $TOTAL
}

Will give:

  9 -   4 =    5 -> TOTAL=    5
3 - 1 = 2 -> TOTAL= 7
77 - 2 = 75 -> TOTAL= 82
25 - 12 = 13 -> TOTAL= 95
226 - 664 = -438 -> TOTAL= -343
Out of the loop total: -343

Note: $TOTAL could not be used in main script (after last right curly bracket } ).

Using lastpipe bash option

As @CharlesDuffy correctly pointed out, there is a bash option used to change this behaviour. But for this, we have to first disable job control:

shopt -s lastpipe           # Set *lastpipe* option
set +m # Disabling job control
TOTAL=0
printf "%s %s\n" 9 4 3 1 77 2 25 12 226 664 |
while read A B;do
((TOTAL+=A-B))
printf "%3d - %3d = %4d -> TOTAL= %4d\n" $A $B $[A-B] $TOTAL
done

9 - 4 = 5 -> TOTAL= -338
3 - 1 = 2 -> TOTAL= -336
77 - 2 = 75 -> TOTAL= -261
25 - 12 = 13 -> TOTAL= -248
226 - 664 = -438 -> TOTAL= -686

echo final total: $TOTAL
-343

This will work, but I (personally) don't like this because this is not standard and won't help to make script readable. Also disabling job control seem expensive for accessing this behaviour.

Note: Job control is enabled by default only in interactive sessions. So set +m is not required in normal scripts.

So forgotten set +m in a script would create different behaviours if run in a console or if run in a script. This will not going to make this easy to understand or to debug...

Multiple 'find' for watchers at once - only executes first one

If your task is simply to make sure the downstream copies of the two (or 1 + number of themes) directories are always up to date, a cron job which syncs them every n minutes might be the simplest and most robust solution. cron has a per-minute granularity, though something like every five minutes may well be sufficient for your use case. rsync by design does not copy files which are already up to date, so it should be reasonably quick to execute in the no-op case.

* * * * * bin/rsyncthemes

(This is once per minute; on Linux you can say */5 in the first field to run every 5 minutes, but this is not portable to all cron dialects.) ... where rsyncthemes contains something like

#!/bin/sh
# cron jobs always start in your home directory,
# but you want to put an absolute path here
# to run this in the directory where you want rsync to copy these files
cd somewhere

for dir in code/themes/one \
code/themes/other \
code/base
do
rsync -avh "$dir"/* ./
done

where obviously this needs to be marked executable (chmod a+x rsyncthemes) and saved in the directory we specify in the cron job (viz. bin in your home directory).

If you are hellbent on using entr to avoid running rsync needlessly, though, I'm guessing you want something like

for dir in code/themes/one \
code/themes/other \
code/base
do
while true; do
find "$dir"/src -name '*.html' -o -name '*.ts' -o -name '*.scss' |
entr -d rsync -avh "$dir"/* ./
done & # <-- run each loop as a background job
done

In some more detail, we are running three while true loops as background jobs, one for each of the directories enumerated in the for loop. They are running in parallel with your interactive shell, and will stop running when you log out (unless you separately set them up to stay running with something like nohup). The & operator is what runs something in the background, and putting it after the while loop's done statement makes it apply to the entire loop.

I'm guessing you may need to use entr -n in order for the jobs to run in the background.

The wildcard in the rsync command gets expanded by the shell, so you might need to refactor this to not use a wildcard, or to evaluate the wildcard only after find finishes, if you expect it to be possible for new files to appear in these directories occasionally. (Quoting the entire rsync command and passing it with entr -s should trivially accomplish this, if I'm reading the entr man page correctly.)

Using a pipe to read a file, run script and write to the same file

cat $1 | ./words_num word | tee $1


Related Topics



Leave a reply



Submit