11

I want to delete files, which have size = 0. So I tried:

find ./ -size 0 | xargs rm

But, it has a problem with files in which names begin with space.

Searching on internet I found this:

find ./ -size 0 -exec rm -i {} \;

It works. However, I think my way using xargs is too sophisticated for this.

What does {} \; mean?

Could someone explain me?

My English is not very good, so please use simple writing.

3
  • 1
    by the way: There's -print0 in find and -0 in xargs. Using them is very much imperative when doing stuff like this. Because if it finds "/ bla"... 😅
    – Daniel B
    May 3, 2016 at 21:15
  • @DanielB -print0 and xargs -0 are both non standard and often useless, find alone being able to achieve the same goal without these hacks.
    – jlliagre
    May 3, 2016 at 22:16

5 Answers 5

11

{} has absolutely no meaning to bash, so is passed unmodified as an argument to the command executed, here find.

On the other hand, ; has a specific meaning to bash. It is normally used to separate sequential commands when they are on the same command line. Here the backslash in \; is precisely used to prevent the semicolon to be interpreted as a command separator by bash and then allow it to be passed as a parameter to the underlying command, find. Quoting the semicolon, i.e. ";" or ';', could have been an alternate way to have it stayed unprocessed.

The command:

find ./ -size 0 -exec rm -i {} \;

means: find in the current directory (note that the / is useless here, . cannot be but a directory anyway) anything that has a size of 0 and for each object found, run the command rm -i name, i.e. interactively prompt for each file if you want to remove it. {} is replaced by each file name found in the executed command. One nice feature is that this file name is strictly a single argument, whatever the file name (even containing embedded spaces, tabs, line feeds, and whatever characters). This wouldn't be the case with xargs, unless non portable hacks are used. The final ; is there to end the -exec clause. The reason why its end needs to be delimited is that other find options might follow the -exec one, although it is rarely done. e.g.:

find . -name "*.js" -exec ls -l {} \; -name "special*" -exec wc -l {} \;

One issue with this command is that it won't ignore non plain files, so might prompt the user to delete sockets, block and character devices, pipes, and directories. It will always fail with the latter even if you answer yes.

Another issue, although not really critical here, is that rm will be called for each file that has a zero size. If you substitute the -exec ending from /; to +, find will optimize the sub-process creation by only calling rm the minimal possible number of times, often just once.

Here is then how I would modify this command:

find . -type f -size 0 -exec rm -i {} +
10
  • {} is parameter substitution
    – clhy
    May 6, 2016 at 0:18
  • 2
    @The_IT_Guy_You_Don't_Like It is not. More precisely, parameter substitution requires a $ character before { and requires at least a variable name to be present between the curly braces. {} alone has no meaning for the shell and is left unchanged by it.
    – jlliagre
    May 6, 2016 at 7:18
  • Why does this not work without the +? What's it doing here? Jan 8, 2019 at 11:17
  • 2
    @CGTheLegend It doesn't work without the + because the -exec expression takes a variable number of arguments so needs to be terminated by something to be correctly parsed, and further expressions to be properly found. The terminator can be either + or ;.
    – jlliagre
    Jan 8, 2019 at 11:35
  • excellent. The reason why its end needs to be delimited is that other find clauses might follow the -exec one, although it is rarely done. could you elaborate on this. This will clarify things further down.
    – samshers
    Aug 29, 2020 at 5:03
2

When using find -exec, {} is expanded to each result found.

For example, if you have a directory example containing 3 files a.txt, b.txt and c.txt, find example/ -exec rm -i {} \; will expand to:

find example/ -exec rm -i example/a.txt \;
find example/ -exec rm -i example/b.txt \;
find example/ -exec rm -i example/c.txt \;

The \; at the end is simple an escaped ; to indicate the end of the exec pattern. Otherwise, it would be interpreted by shell itself.

1
  • 2
    The command wouldn't really expand to the three commands you wrote which would result in multiple prompts for the same file and leads to "file not found" errors if you accept to remove them, but simply to: rm -i "example/a.txt" ; rm -i "example/b.txt" ; rm -i "example/c.txt"
    – jlliagre
    May 3, 2016 at 22:10
1

In conjunction with the find command's exec option, the {} part is replaced by the name of the files found when the command is executed. The \; is important too, because that is what defines the end of the command being executed

For instance

find ~ -name \*.bak -exec -rm -f {} \;

would delete all files ending in .bak anywhere in the user's home directory or in folders contained inside it. By executing rm -f on each file found.

xargs takes standard input lines, usually from a pipe, and forms the tailing part of the arguments from it when it executed the command you give it

1

To take each case in turn:-

xargs

What this program does is to add each line from input as a parameter to whatever command is passed as its own parameter , then run the resultant command, so if find lists three files a, b and c in the current directory it would give as output:

./a
./b
./c

If this is piped to xargs rm, then this command will be executed:

rm ./a ./b ./c

However, if find also finds a file called " d e ", then the executed command becomes:

rm ./a ./b ./c ./ d e 

To handle this, find provides an option -print0, which adds a null character (\0) after each file, instead of the usual new-line. To tell xargs that the input is in this form, add the parameter -0, so the command:

find . ... -print0|xargs -0 rm

will build and execute the command:

rm "./a" "./b" ."/c" "./ d e "

This will work on all file names, including names containing blanks, tabs and new-lines.

find -exec

When this option is called, then what follows (up to \;) is executed for each file found. This is not terribly useful unless there is some means to refer to the currently found file and that is what {} does. It can be called more than once in the run string, eg to copy a selection of files to a back-up directory:

find . ... -exec cp "{}" "/BackUp/{}" \;

This in the find example above would execute in turn:

cp "./a" "/BackUp/./a"
cp "./b" "/BackUp/./b"
cp "./c" "/BackUp/./c"

Note that, outside their use in the find command, { and } have special meanings in bash (multiple commands and expanding lists), but they involve other characters between the brackets: the string {} is copied literally, so does not need escaping.

2
  • +1, excellent - find . ... -exec cp "{}" "/BackUp/{}" \;
    – samshers
    Aug 29, 2020 at 13:01
  • { and } have special meanings in bash (multiple commands and expanding lists) - any examples
    – samshers
    Aug 29, 2020 at 13:02
0

This is an old question but I want to add some more information:

find ./ -size 0 -exec rm -i {} \;

In the previous command, the \; is an escaped semicolon. This prevents the command from being handled by the shell (i.e., normally ; would separate commands).

The -exec argument interprets everything as a command up to that escaped semicolon \; (i.e., rm -i {} it will be inner command to be executed by find). Within the inner command the {} represent the parameter expansion. In pain English, it means "insert the file name found here".

So, if the found files were "file-a.txt" and "file-b.txt", find would execute rm -i file-a.txt then rm -i file-b.txt.

One issue with this command is that it won't ignore non plain files, so might prompt the user to delete sockets, block and character devices, pipes, and directories. It will always fail with the latter even if you answer yes (i.e., directories needs to be deleted recursively)

Another issue, although not really critical here, is that rm will be called for each file that has a zero size. If you substitute the -exec ending from /; to +, find will optimize the sub-process creation by only calling rm the minimal possible number of times, often just once.

Here is then how I would modify this command:

find ./ -type f -size 0 -exec rm -i {} +

curly brackets or braces: {} can be used in a different ways

Brace expansion

Braces can be used to build sequences:

### prints out the numbers from 0 to 10
echo {0..10}

## prints out the same numbers, but in reverse order
echo {10..0}

## prints every second number, from 10 to 0
echo {10..0..2}

## prints every second letter, from z and working its way backwards to a.
echo {z..a..2}

Also we can combine two or more sequences:

## prints out a pair of letters, from aa to zz.
echo {a..z}{a..z}

Adding prefixes and suffixes:

### adds '"' as prefix and suffix
echo \"{These,words,are,quoted}\"
# output: "These" "words" "are" "quoted"

# concatenates the files file1, file2, and file3 into combined_file.
cat {file1,file2,file3} > combined_file

# copies "file22.txt" to "file22.backup"
cp file22.{txt,backup}

Note:

No spaces are allowed within the braces {...} unless the spaces are quoted or escaped.

echo {file1,file2}\ :{\ A," B",' C'}
# output: file1 : A file1 : B file1 : C file2 : A file2 : B file2 : C

Extended Brace expansion.

Braces can be used to build arrays. Arrays in Bash are defined by putting elements between parenthesis () and separating each element using a space, like this:

month=("Jan" "Feb" "Mar" "Apr" "May" "Jun" "Jul" "Aug" "Sep" "Oct" "Nov" "Dec")

To access an element within the array, you use its index within brackets []:

 # Array indexes start at [0], so [3] points to the fourth item
$ echo ${month[3]}
## output: Apr

Therefore, we can create an array with something like this:

## builds an array that contains all the 2-letter combinations of the entire alphabet.
letter_combos=({a..z}{a..z})

## contains all the binary numbers for an 8-bit register, in ascending order,
## from 00000000, 00000001, 00000010, etc., to 11111111. 
dec2bin=({0..1}{0..1}{0..1}{0..1}{0..1}{0..1}{0..1}{0..1})

This last one is particularly interesting because dec2bin now we can use this to build an 8-bit decimal-to-binary converter. Say you want to know what 25 is in binary. You can do this:

$ echo ${dec2bin[25]}
## output: 00011001

But Teo there are not better ways of converting decimal to binary?

  • Yes, there are, but it is still interesting, right?

Grouping Commands

{ ... } can be used for placing a list of commands to be executed in the current shell context. No sub-shell is created. The semicolon ; (or newline) following list is required.

Parentheses () are used to run commands in a sub-shell:

menu_type=bar
echo $menu_type
## output: bar

## new lets called in a sub-shell
(menu_type=superbar; echo $menu_type)
## output: superbar

## back to the context
echo $menu_type
## output: bar

We can not get access to the new value superbar of menu_type.

However, if we run something like this:

{ menu_type=superbar; echo $menu_type; }
## output: superbar

echo $menu_type
## output: superbar

{ ... } does not create a sub-shell, that is why we can get access to the menu_type value.

{ ... } are also referred to as an inline group, in effect, it creates an anonymous function (i.e., a function without a name). In plain English, unlike in a "standard" function, the variables inside a { ... } remain visible to the remainder of the script.

Also, { ... } can be used to group the output from several commands into stdout or to receive a redirection into its stdin. Let's see an example:

#!/bin/bash
# rpm-check.sh
#  Queries an rpm file for description, listing, and whether it can be installed.
#  Saves output to a file.

SUCCESS=0
E_NOARGS=65

if [ -z "$1" ]; then
  echo "Usage: `basename $0` rpm-file"
  exit $E_NOARGS
fi  

{ # Begin command group.
  echo
  echo "Archive Description:"
  rpm -qpi $1       # Query description.
  echo
  echo "Archive Listing:"
  rpm -qpl $1       # Query listing.
  echo
  rpm -i --test $1  # Query whether rpm file can be installed.
  if [ "$?" -eq $SUCCESS ]
  then
    echo "$1 can be installed."
  else
    echo "$1 cannot be installed."
  fi  
  echo              # End command group.
} > "$1.test"       # Redirects output of everything in block to file.

echo "Results of rpm test in file $1.test"

exit 0

Now, lest see how to proceed with an I/O redirection in the group's stdin:

#!/bin/bash
File=/etc/fstab

## reads the first two lines of the file
{
  read line1
  read line2
} < $File

echo "First line in $File is:"
echo "$line1"
echo
echo "Second line in $File is:"
echo "$line2"

exit 0

Another example of saving the output of a command group into a file

## exec commands sequentially and redirects the output of the ls command into the png-list.txt file
echo "I found all these png files:"; find . -iname "*.png"; echo "Within this bunch of files:"; ls > png-list.txt

## exec commands sequentially and redirects the output of the group into the png-list.txt file
{ echo "I found all these png files:"; find . -iname "*.png"; echo "Within this bunch of files:"; ls; } > png-list.txt

What is the difference, Teo?

Well, young padawan. The second one creates the file png-list.txt with all the outputs, starting with the line “I found all these png files:“, and finishing up with the ls command output.

Sub-shell Hack

Bash makes a sub-shell for a { ... } brace group command if and only if it's a part of pipeline, for example:

$ { A=1; { A=2; sleep 2; } ; echo $A; }
## output: 2

$ { A=1; { A=2; sleep 2; } | sleep 1; echo $A; }
## output: 1

Note:

There is space between the braces and the command list enclosed within them. That’s because { and } are reserved words (i.e., commands built into the shell). Also, the list of commands has to end with a semicolon ; or use newlines to separate commands.

Parameter expansion

Okay, getting back to

month=("Jan" "Feb" "Mar" "Apr" "May" "Jun" "Jul" "Aug" "Sep" "Oct" "Nov" "Dec")
echo ${month[3]}
## output: Apr

Here the braces {} are not being used as apart of a sequence builder, but as a way of generating parameter expansion. Parameter expansion involves what it says on the box:

it takes the variable or expression within the braces and expands it to whatever it represents.

What this means Teo?

Well, it means ${...} tells the shell to expand whatever is inside it. In this case, month is the array we defined earlier, that is:

month=("Jan" "Feb" "Mar" "Apr" "May" "Jun" "Jul" "Aug" "Sep" "Oct" "Nov" "Dec")

And, item 3 within the array points to "Apr" (i,e. the first index in an array in Bash is [0]). That means that echo ${month[3]}, after the expansion, translates to echo "Apr".

Interpreting a variable as its value is one way of expanding it, but there are a few more we can leverage. We can use parameter expansion to manipulate what you read from variable (i.e. by cutting a chunk off the end).

Suppose you have a variable like:

a="This sentence is too longgg"

## chops off the last two gs
echo ${a%gg}
## output: This sentence is too long

This can be useful for converting files from one format to another. For example, if we have a command that takes a JPEG image called image.jpg and converts into a PNG image called image.png:

convert image.jpg image.png

We can rewrite it like this:

i='image.jpg'
## chops off the extension 'jpg' and adds 'png'
convert $i "${i%jpg}png"
## output: convert image.jpg image.png

But teo how can be this more useful than just writing in the name of the file?

Well, when we have a directory containing hundreds of JPEG images, you need to convert to PNG, just run the following in it:

for i in *.jpg; do convert $i ${i%jpg}png; done

… and all the pictures get converted automatically. Your welcome young padawan.

If you need to chop off a chunk from the beginning of a variable, instead of %, use #:

$ a="Hello World!"

## chops off the word 'Hello' and adds 'Goodbye'
$ echo Goodbye${a#Hello}
## output: Goodbye World!

Placeholder for text

Used after xargs -i (i.e., replace strings option). The {} double curly brackets are a placeholder for output text.

## Execute 'echo ./<file>' for each file in the directory
ls . | xargs -i -t echo ./{} $1
#            ^^         ^^

Pathname

A pathname is a filename that includes the complete path. As an example, /home/<user>/Notes/todo.txt. This is sometimes referred to as the absolute path. We will encounter {} mostly in find constructs that contains -exec <command> \;. However, this is not a shell builtin. If <command> contains {}, then find substitutes the full path name of the selected file for "{}".

# Removes all core dump files from user's home directory.
find ~/ -name 'core*' -exec rm {} \;

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .