ZSH+GREP+REGEX. Why this snippet act as rm -r /

297 views Asked by At

This is a little anecdote from earlier on why not running root is vital.

I was sorting my home directory and deleted a few compressed files I had, I wrote ls . | grep -P 'zip|tar|7z' | xargs rm and thought, hey I could also write this as rm -r $(ls . | grep -P '...') I suppose.

The second part I didn't mean to use it since there was nothing to delete, it was morelike a mental exercise, I wrote it next to the last command with a 'divider' to visually compare them.

ls . | grep -P 'zip|tar|7z' | xargs rm **//** rm -r $(ls . | grep -P '...')

Being **//** the "divider" and ... the mental "substitute" for 'zip|tar..'

I thought this wouldn't run but to my surprise, it acted as rm -r /and tried to delete everything, luckily permissions saved me and nothing was deleted.

But I'm curious why it'd work that way, my guess is that rm **//** somehow translated to rm / but I'm not sure.

2

There are 2 answers

0
Kusalananda On

In the zsh shell, **//** would expand to all names under / as well to all names below the current directory (recursively).

From an empty directory on my system:

$ echo **//**
/altroot /bin /boot /bsd /bsd.booted /bsd.rd /bsd.sp /dev /etc /extra /home /mnt /root /sbin /sys /tmp /tmp_mnt /usr /var /vol

Why? Well, **/ matches all directories recursively under the current directory. More importantly, it matches the current directory, but since the current directory's name is not available inside the current directory, there's no entry returned for that.

However, when you add a / to that to create **//, then you get a lone / back for the current directory. Again in an empty directory:

$ echo **//
/

Then, if you add a further ** to make **//**, you pick up all names from the root directory, together with all names from the current directory and below (directory names from the current directory and below would occur twice in the list).

Your xargs is calling

rm **//** rm -r $(ls . |  grep -P '...')

If you're using GNU rm, it will helpfully rearrange the command line so that it is interpreted the same as

rm -r **//** rm $(ls . |  grep -P '...')

What this does should now be clear.


If you want to delete all regular files in the current directory that have filename suffixes .zip, .tar or .7z, use

rm ./*.(zip|tar|7z)(.)

in the zsh shell. If want to do that recursively down into subdirectories, use

rm ./**/*.(zip|tar|7z)(.)

The glob qualifier (.) makes the globbing pattern only match regular files. You could even restrict it to files above a certain size, say 10MB, with ./**/*.(zip|tar|7z)(.Lm+10).

2
user1934428 On

One difference is that the ls ... | xargs .... solution also works if there are really a lot of files involved, while your rm $( .... ) might produce a argument list too long error. But if this is not an issue in your case, an even simpler attempt would be (assuming here Zsh; I don't understand why you tagged this bash, since you explicitly refer to Zsh only in your question)

rm *(zip|tar|7z)*(N)

which would express your original statement; I believe however that you really meant

rm -- *.(zip|tar|7z)(N)

because the solution you posted would also remove a file tarpit.txt, for instance. The (N) flag is a frail attempt to treat the case, that you don't have any file matching the pattern. Without the (N), you would get an error message from Zsh, and rm would receive the unexpanded file pattern, and, since it is unlikely that a file of this name exists, would output a second error message. By using (N), Zsh would simply pass nothing in this case (without complaining), and in fact rm would be invoked without arguments. Of course you would then get a rm: missing operand on stderr, and if you don't like this, you can filter this message.

UPDATES:

As Kusalananda has pointed out in his/her comment, omitting the (N) would, by default, make zsh only print an error message, if no files match the pattern, but not cause rm to be invoked.

Also added the -- flag to rm to allow removal of, i.e., a file called -rf.tar.