To understand spaces

This is a one-line bash script by Ishmael Smyrnow to clear macOS icon cache:

sudo rm -rfv /Library/Caches/com.apple.iconservices.store; sudo find /private/var/folders/ ( -name com.apple.dock.iconcache -or -name com.apple.iconservices ) -exec rm -rfv {} ; ; sleep 3;sudo touch /Applications/* ; killall Dock; killall Finder

source

I try to understand how to change it back to its multiple-line version. Here is what I have now:

sudo rm -rfv /Library/Caches/com.apple.iconservices.store
sudo find /private/var/folders/ -name com.apple.dock.iconcache -exec rm -rfv {} ;
sudo find /private/var/folders/ -name com.apple.iconservices -exec rm -rfv {} ;
sleep 3
sudo touch /Applications/*
killall Dock
killall Finder

I do understand in Ishmael’s version semicolons are used for newlines and ; for literal semicolons. But what is the purpose to have a space between the two semicolons and after the asterisk? Is it safe to remove them?

 -exec rm -rfv {} ; ;
/Applications/* ;
Asked By: jsx97

||

When to and not to use spaces in shell language syntax is not trivial.

It’s also important to note there different kinds of spaces:

  • space and tab are used to separate tokens in the syntax

  • newline separates tokens but also separates commands. For instance:

    array=(
      foo bar
      baz
    )
    

    (a syntax from zsh now supported by several other shells) or ksh’s (from C):

    (( 1
    + 2
    ))
    

    Or POSIX sh (but also from ksh):

    var=$((
      a + 
      b
    ))
    

    above space, tab or newline can separate the elements.

    But:

    cmd a
    cmd b
    

    runs two separate commands.

    Where newline can be used in place of space is not always clear and can vary between shells.

  • in some shells (including bash) some other characters classified as blank in the locale can be used as token delimiters in the syntax and that can include characters such as U+00A0 the non-breaking space (only in locales where it’s classified as blank and encoded on one byte in the case of bash).

Then there are quoting operators ("...", '...', $'...', $"...", ) which can be used to remove whatever special meaning a character otherwise has in the shell syntax.

And there are a number of special characters (mainly ();<>&| and to some extent `$) which either alone or possibly combined with others form tokens in the syntax that can also delimit words and so don’t need spaces around them.

For instance, in ksh, you can write:

[[(a == b||d>c)&&0<1 ]]&&(echo yes)|cat

Instead of:

[[ ( a == b || d > c ) && 0 < 1 ]] && ( echo yes ) | cat

As ||, (, &&, <, &&… are individual tokens that can delimit.

In bash (5.1 at least), you’ll find that you need:

[[(a == b||d>c)&&0 <1 ]]&&(echo yes)|cat

Or otherwise you seem to be hitting a sort of bug:

$ bash -c '[[(a == b||d>c)&&0<1 ]]&&(echo yes)|cat'
bash: -c: line 1: unexpected token 284 in conditional command
bash: -c: line 1: syntax error near `&0<'
bash: -c: line 1: `[[(a == b||d>c)&&0<1 ]]&&(echo yes)|cat'

(possibly because outside of [[...]], in cmd 0<file, 0< in itself is an operator so likely treated as a separate token).

In:

foo ;;bar

There’s no ambiguity, that’s a foo word, space treated as separator, a quoted ; (; being the same as ';' or ";" here) and the ; token and then bar. So it is treated the same as:

foo ';' ; bar

Adding spaces doesn’t harm and helps with legibility.

That code could also be written (and improved to):

sudo zsh -c '
  rm -rfv /Library/Caches/com.apple.iconservices.store
  find /private/var/folders/ 
    "("
       -name com.apple.dock.iconcache -o 
       -name com.apple.iconservices 
    ")" -prune -exec rm -rfv {} +
  sleep 3
  touch /Applications/*'
killall Dock
killall Finder

Where you’ll notice:

  • sudo is run only once with a multiline quoted string passed as an argument to a separate zsh invocation inside which all the characters lose their special meaning to the outer shell.
  • followed by newline is another special case not mentioned above. That makes the newline disappear (for the inner shell; for the outer shell the backslash and newline are inside single quotes so remain literal).
  • we call only one find
  • we use + instead of ; to delimit (for find, not for the shell) the list of arguments that to make up the command that -exec executes, so rm is called once with all the directories to remove instead of once for each.
  • we use -prune so find doesn’t attempt to descend into the directories that rm is going to remove anyway.
  • I use "(" instead of ( as I find it more legible. Usually I would use '(' be here the whole thing is inside '...' so we can’t use that.
  • we use zsh -c instead of sh -c because sh has that misfeature whereby when globs don’t match they are passed literally, so in sh, touch /Applications/* would create a file called * in /Applications if there was no non-hidden file there. In zsh, if there’s no match, you get an error instead.
Answered By: Stéphane Chazelas
Categories: Answers Tags: ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.