Bash file operations are surprisingly flexible, but the way it handles file descriptors means you’re often not working directly with files, but with channels to files that the shell manages.
Let’s watch some files move.
# Create a temporary directory to play in
mkdir /tmp/bash-file-ops-demo
cd /tmp/bash-file-ops-demo
# Create a file with some content
echo "Hello, Bash!" > original.txt
# Read the file content
cat original.txt
# Output: Hello, Bash!
# Append to the file
echo "Another line." >> original.txt
cat original.txt
# Output:
# Hello, Bash!
# Another line.
# Copy the file
cp original.txt copy.txt
cat copy.txt
# Output:
# Hello, Bash!
# Another line.
# Move (rename) the file
mv copy.txt moved.txt
cat moved.txt
# Output:
# Hello, Bash!
# Another line.
# Delete the file
rm moved.txt
ls
# Output: original.txt
# Clean up
cd ..
rm -rf /tmp/bash-file-ops-demo
This gives you a basic feel for the common commands. But what’s really happening under the hood? Bash uses file descriptors, which are small integers that represent open files or other I/O resources. When you > or >> to a file, Bash opens that file, associates it with a file descriptor (usually 1 for standard output, or 2 for standard error), and redirects the output. cat reads from its standard input (file descriptor 0) and writes to its standard output (file descriptor 1).
The core problem these tools solve is persisting and manipulating data outside of a running program’s memory. You need a way to store information, retrieve it, duplicate it, and get rid of it. Bash provides a simple, text-stream-based interface for this.
Consider read. It’s not just reading a file; it’s reading from a file descriptor.
# Create a file with multiple lines
printf "line1\nline2\nline3\n" > multi.txt
# Read line by line
while IFS= read -r line; do
echo "Read: $line"
done < multi.txt
# Output:
# Read: line1
# Read: line2
# Read: line3
Here, < multi.txt is input redirection. It tells the while loop to use multi.txt as its standard input (file descriptor 0). The read command then reads one line at a time from that descriptor. IFS= prevents leading/trailing whitespace from being trimmed, and -r prevents backslash escapes from being interpreted.
The most common point of confusion is redirection versus command arguments. echo "hello" > file.txt writes "hello" to file.txt. echo "hello" file.txt would try to print the literal string "hello" and then the literal string "file.txt" as arguments to echo. The > is an operator that intercepts the standard output of the command on its left and sends it to the file on its right, before the command even finishes processing all its arguments or produces output.
The exec command is a powerful tool for manipulating file descriptors directly. You can open files and assign them to specific descriptors for the current shell process.
# Open a file for reading and assign it to file descriptor 3
exec 3< original.txt
# Read from file descriptor 3
read -r line1 <&3
read -r line2 <&3
echo "Line 1: $line1"
echo "Line 2: $line2"
# Output:
# Line 1: Hello, Bash!
# Line 2: Another line.
# Close file descriptor 3
exec 3<&-
This exec 3< original.txt command opens original.txt for reading and associates it with file descriptor 3. The <&3 syntax in read -r line1 <&3 means "read from file descriptor 3". This is useful for complex scripts where you need to manage multiple input or output streams simultaneously.
The next thing you’ll likely run into is managing permissions and ownership during copy/move operations, especially when dealing with cp -p or mv across different file systems.