Shell – Why does `watch` make `ls /tmp` list contents of $HOME

lsshellwatch

I'm trying to watch number of files in my /tmp/ directory. For this I thought this command would work:

watch sh -c 'ls /tmp/|wc -l'

But it appears to work as if ls had no arguments. Namely, I'm in ~, and I get number of files there instead of /tmp/. I found a workaround, which seems to work:

watch sh -c 'ls\ /tmp/|wc -l'

But why do I need to escape the space between ls and /tmp/? How is the command transformed by watch so that ls output is feeded to wc, but /tmp/ is not passed as argument to ls?

Best Answer

The difference may be seen via strace:

$ strace -ff -o bq watch sh -c 'ls\ /tmp/|wc -l'
^C
$ strace -ff -o nobq watch sh -c 'ls /tmp/|wc -l'
^C
$ grep exec bq* | grep sh
bq.29218:execve("/usr/bin/watch", ["watch", "sh", "-c", "ls\\ /tmp/|wc -l"], [/* 54 vars */]) = 0
bq.29219:execve("/bin/sh", ["sh", "-c", "sh -c ls\\ /tmp/|wc -l"], [/* 56 vars */]) = 0
bq.29220:execve("/bin/sh", ["sh", "-c", "ls /tmp/"], [/* 56 vars */]) = 0
$ grep exec nobq* | grep sh
nobq.29227:execve("/usr/bin/watch", ["watch", "sh", "-c", "ls /tmp/|wc -l"], [/* 54 vars */]) = 0
nobq.29228:execve("/bin/sh", ["sh", "-c", "sh -c ls /tmp/|wc -l"], [/* 56 vars */]) = 0
nobq.29229:execve("/bin/sh", ["sh", "-c", "ls", "/tmp/"], [/* 56 vars */]) = 0

In the backquote case, ls /tmp is passed as a single argument to the -c to sh, which runs as expected. Without this backquote, the command is instead word split when watch runs sh which in turn runs the supplied sh, so that only ls is passed as the argument to -c, meaning that the sub-subsh will only run a bare ls command, and lists the contents of the current working directory.

So, why the complication of sh -c ...? Why not simply run watch 'ls /tmp|wc -l' ?

Related Question