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 {} \;
-print0
in find and-0
in xargs. Using them is very much imperative when doing stuff like this. Because if it finds "/ bla"... 😅-print0
andxargs -0
are both non standard and often useless,find
alone being able to achieve the same goal without these hacks.