Chapter 15: Linux Commands: File Manipulation (cp
, mv
, rm
, mkdir
)
Chapter Objectives
Upon completing this chapter, you will be able to:
- Understand the fundamental concepts of the Linux filesystem hierarchy, including inodes and file attributes.
- Implement file and directory creation, copying, moving, and renaming operations using core command-line utilities.
- Configure command behavior using common options to handle tasks like recursive operations, interactive prompts, and forcing actions.
- Debug common errors such as “Permission Denied” and “Directory not empty” when manipulating files.
- Apply these commands to manage source code, configuration files, and build artifacts in a typical embedded development workflow on a Raspberry Pi 5.
- Recognize the potential risks associated with destructive commands and employ safe practices to prevent data loss.
Introduction
In the world of embedded Linux development, your most fundamental interactions with the system will involve managing files and directories. Every project, from a simple blinking LED driver to a complex multimedia application, is composed of a collection of files: source code, configuration scripts, documentation, compiled binaries, and system libraries. The ability to manipulate these files with precision and efficiency is not just a convenience; it is a foundational skill upon which all other development activities are built. The core utilities mkdir
, cp
, mv
, and rm
are the primary tools for this task. They are the digital equivalent of a machinist’s basic workshop tools—simple, powerful, and indispensable.
This chapter introduces these four essential commands. While they may seem basic, a deep understanding of their behavior, options, and interaction with the underlying filesystem is crucial. In an embedded context, you will constantly use them to structure project directories, back up configuration files before modification, move compiled artifacts to deployment locations, and clean up build environments. Misusing these commands, particularly rm
, can have catastrophic consequences, potentially deleting hours of work or even corrupting a target system’s root filesystem. By mastering these tools on your Raspberry Pi 5, you will build the confidence and competence needed to manage complex software projects and navigate any Linux-based environment effectively. We will move beyond simple memorization, exploring how these commands work and instilling the best practices required for professional embedded systems engineering.
Technical Background
To truly master file manipulation in Linux, one must look beneath the surface of the commands themselves and understand the structure they operate upon: the filesystem. The Linux filesystem is not merely a container for files; it is a sophisticated, hierarchical database, and commands like cp
, mv
, and rm
are specialized tools for interacting with its records.
The Filesystem Hierarchy and the Inode
At the heart of every Unix-like filesystem is the inode (index node). An inode is a data structure that stores all the metadata about a file or directory, except for its name and its actual data content. Think of it as the master record for a piece of data. This metadata includes the file’s size, its owner (user and group), its permissions (read, write, execute), timestamps (creation, last access, last modification), and, most importantly, pointers to the actual data blocks on the storage device where the file’s content is located. Every file and directory on the system is assigned a unique inode number.
The file’s name, which is how humans identify it, is stored separately in a directory. A directory is itself a special type of file whose content is simply a list of filenames and their corresponding inode numbers. When you issue a command like cat /home/user/report.txt
, the kernel traverses the filesystem tree. It starts at the root directory (/
), finds the entry for home
, reads its inode to find its data (a list of its contents), finds the entry for user
, reads its inode, and finally finds the entry for report.txt
. From this entry, it gets the inode number for report.txt
. Using this inode, the system can find all the metadata and the physical location of the report’s content.
This separation of name and metadata is what makes Linux filesystems so flexible. For instance, a single file can have multiple names (called hard links), where different directory entries point to the same inode.
Creating Structure: mkdir
The mkdir
(make directory) command is the simplest of the four, but it is the foundation of organization. Its sole purpose is to create new directories. When you type mkdir my_project
, you are instructing the system to perform several actions. First, it creates a new inode for the my_project
directory. Second, it adds an entry to the current directory’s file list, linking the name my_project
to the new inode number. Finally, it populates the new directory’s data block with two default entries: .
(a link to its own inode) and ..
(a link to the parent directory’s inode). This .
and ..
structure is fundamental to filesystem navigation. The -p
(parents) option is a powerful feature that allows mkdir
to create an entire path of nested directories if they don’t already exist, such as mkdir -p my_project/src/drivers
, preventing errors and simplifying setup scripts.
graph TD subgraph "mkdir -p project/src/drivers" A(Start) --> B{"Path: project/src/drivers"}; B --> C{Check for 'project'}; C -- "Exists" --> D{Check for 'project/src'}; C -- "Doesn't Exist" --> C_Create[Create 'project']; C_Create --> D; D -- "Exists" --> E{Check for 'project/src/drivers'}; D -- "Doesn't Exist" --> D_Create[Create 'project/src']; D_Create --> E; E -- "Exists" --> F(End); E -- "Doesn't Exist" --> E_Create[Create 'project/src/drivers']; E_Create --> F; end %% Styling classDef primary fill:#1e3a8a,stroke:#1e3a8a,stroke-width:2px,color:#ffffff; classDef success fill:#10b981,stroke:#10b981,stroke-width:2px,color:#ffffff; classDef decision fill:#f59e0b,stroke:#f59e0b,stroke-width:1px,color:#ffffff; classDef process fill:#0d9488,stroke:#0d9488,stroke-width:1px,color:#ffffff; class A primary; class F success; class B,C_Create,D_Create,E_Create process; class C,D,E decision;
Copying Files: cp
and the Cost of Duplication
The cp
(copy) command is used to duplicate files and directories. Its operation is a perfect illustration of the inode concept. When you execute cp source.c destination.c
, the system performs a fundamentally different operation from a move. It first creates a brand new inode for destination.c
. It then allocates new data blocks on the disk and meticulously copies the content from source.c
‘s data blocks into these new blocks. Finally, it updates the new inode with the file’s metadata (often with new timestamps) and links the name destination.c
to this new inode in the directory file.
This is a data-centric operation. The key takeaway is that a copy results in two independent entities: two inodes, two sets of data blocks, and two names. Modifying one file has no effect on the other. This is essential for creating backups or creating templates from existing files.
When copying directories, the -r
or -R
(recursive) option is mandatory. This tells cp
to descend into the source directory and replicate its entire structure and contents at the destination. For each file and subdirectory it encounters, it repeats the process of creating new inodes, allocating new data blocks, and copying content. For large directories, this can be a time-consuming and disk-space-intensive operation, a critical consideration in resource-constrained embedded systems.
Moving and Renaming: mv
and Filesystem Efficiency
The mv
(move) command is more nuanced than cp
. It serves two functions: moving a file to a different directory and renaming a file. The beauty of mv
lies in its efficiency when the source and destination are on the same filesystem.
Consider the command mv old_name.c new_name.c
. If both names are in the same directory, the system does not create a new inode or copy any data. It simply edits the directory file, removing the entry for old_name.c
and adding a new entry for new_name.c
that points to the exact same inode. The file’s data on the disk is untouched. This is an incredibly fast, metadata-only operation.
Now consider mv source.c ../backup/
. If the backup
directory is on the same filesystem partition, a similar efficiency occurs. The system removes the source.c
entry from the current directory file and adds a source.c
entry to the backup
directory’s file, pointing to the original inode. Again, the file’s data blocks are not moved or copied.
The situation changes when the destination is on a different filesystem (e.g., moving a file from the main SD card to a USB drive). In this scenario, the filesystem cannot simply repoint a name to an inode on a different partition. The mv
command intelligently detects this and behaves like a cp
followed by an rm
: it copies the data to the new location, verifies the copy was successful, and then deletes the original file. This is inherently slower and more resource-intensive.
Deleting Files: rm
and the Point of No Return
The rm
(remove) command is the most dangerous of the core file utilities. Its function is to delete files, but what it actually does is sever the link between a filename and its inode. When you run rm myfile.txt
, the system finds the myfile.txt
entry in the current directory file and removes it. It then decrements the “link count” in the file’s inode. If the link count drops to zero (meaning no other names point to this inode), the system marks the inode and its associated data blocks as free space, available for future use.
Crucially, the data is not immediately erased or overwritten. It remains on the disk until the operating system allocates that space for a new file. This is why file recovery tools can sometimes work—they scan the disk for orphaned inodes and data. However, on an active system, especially an SSD with TRIM functionality, that free space can be reclaimed very quickly. For all practical purposes, a standard rm
operation should be considered permanent and irreversible.
graph TD subgraph Filesystem State D(Directory) I("Inode 88 <br> <b>Link Count: 2</b> <br> Data Ptr: [...]") DB([Data Blocks]) D -- "link1.txt" --> I; D -- "hardlink2.txt" --> I; I --> DB; end Start(Start: rm link1.txt) --> P1[Find 'link1.txt' in directory]; P1 --> P2[Remove directory entry]; P2 --> P3[Decrement Inode 88's link count]; P3 --> Q1{"Is link count now zero?"}; Q1 -- "No (Count is 1)" --> End1[File data persists.<br>Still accessible via 'hardlink2.txt']; subgraph "Final State (After rm link1.txt)" D_After(Directory) I_After(Inode 88 <br> <b>Link Count: 1</b>) DB_After([Data Blocks]) D_After -- "hardlink2.txt" --> I_After; I_After --> DB_After; end Q1 -- "Yes" --> P4[Mark inode and data blocks as free]; P4 --> End2[Data is now recoverable only by special tools.<br><b>Considered deleted.</b>]; %% Styling classDef primary fill:#1e3a8a,stroke:#1e3a8a,stroke-width:2px,color:#ffffff; classDef success fill:#10b981,stroke:#10b981,stroke-width:2px,color:#ffffff; classDef decision fill:#f59e0b,stroke:#f59e0b,stroke-width:1px,color:#ffffff; classDef process fill:#0d9488,stroke:#0d9488,stroke-width:1px,color:#ffffff; classDef check fill:#ef4444,stroke:#ef4444,stroke-width:1px,color:#ffffff; classDef kernel fill:#8b5cf6,stroke:#8b5cf6,stroke-width:1px,color:#ffffff; class Start,P1,P2,P3,P4 process; class Q1 decision; class End1 success; class End2 check; class D,D_After process; class I,I_After,DB,DB_After kernel;
To remove a directory, you must use the -r
(recursive) option, as in rm -r my_project
. This command will descend into the directory and remove all its contents file by file before removing the directory itself. A common and extremely dangerous mistake is to run rm -rf /
(with sudo
), which recursively and forcefully attempts to delete the entire root filesystem, rendering the system unbootable. This is why experienced developers develop a healthy fear of rm
and often use the -i
(interactive) option, which prompts for confirmation before each deletion, or they double-check their paths before pressing Enter.
Warning: The
rm -rf
command is exceptionally powerful and dangerous. A typo can lead to catastrophic data loss. Always double-check the path you are providing, especially when running as the root user. There is no “undo” button.
graph TD Start((Start: Manage a file)) --> Q1{What is your goal?}; Q1 --> |"I need a duplicate"| P_Copy[Use <b>cp</b> command]; P_Copy --> P_Copy_Dir{"Is it a directory?"}; P_Copy_Dir -- "Yes" --> P_Copy_Dir_Cmd[Use <br><i>cp -r source_dir dest_dir</i>]; P_Copy_Dir -- "No" --> P_Copy_File_Cmd[Use <br><i>cp source_file dest_file</i>]; P_Copy_Dir_Cmd --> End((End)); P_Copy_File_Cmd --> End; Q1 --> |"I need to rename or relocate"| P_Move[Use <b>mv</b> command]; P_Move --> P_Move_Q{"Is destination on a<br>different filesystem?"}; P_Move_Q -- "Yes" --> P_Move_Warn[<i>Be aware:</i><br>This will be a slow copy+delete operation.]; P_Move_Q -- "No" --> P_Move_Info[<i>FYI:</i><br>This will be a fast metadata change.]; P_Move_Warn --> P_Move_Cmd[Use <br><i>mv source_item destination</i>]; P_Move_Info --> P_Move_Cmd; P_Move_Cmd --> End; Q1 --> |"I need to permanently delete"| P_Delete[Use <b>rm</b> command]; P_Delete --> P_Delete_Warn{{"<b>WARNING:</b><br>This action is irreversible!"}}; P_Delete_Warn --> P_Delete_Q{"Is it a non-empty directory?"}; P_Delete_Q -- "Yes" --> P_Delete_Dir_Cmd[Use <br><i>rm -r directory</i><br>Double-check your path!]; P_Delete_Q -- "No" --> P_Delete_File_Cmd[Use <br><i>rm file</i>]; P_Delete_Dir_Cmd --> End; P_Delete_File_Cmd --> End; %% Styling classDef primary fill:#1e3a8a,stroke:#1e3a8a,stroke-width:2px,color:#ffffff; classDef success fill:#10b981,stroke:#10b981,stroke-width:2px,color:#ffffff; classDef decision fill:#f59e0b,stroke:#f59e0b,stroke-width:1px,color:#ffffff; classDef process fill:#0d9488,stroke:#0d9488,stroke-width:1px,color:#ffffff; classDef warning fill:#eab308,stroke:#eab308,stroke-width:1px,color:#1f2937; classDef check fill:#ef4444,stroke:#ef4444,stroke-width:1px,color:#ffffff; class Start primary; class End success; class Q1,P_Copy_Dir,P_Move_Q,P_Delete_Q decision; class P_Copy,P_Move,P_Delete,P_Copy_Dir_Cmd,P_Copy_File_Cmd,P_Move_Cmd,P_Delete_Dir_Cmd,P_Delete_File_Cmd process; class P_Move_Warn,P_Move_Info warning; class P_Delete_Warn check;
Practical Examples
Let’s apply this theory to a practical scenario on your Raspberry Pi 5. We will simulate setting up a small C project for a sensor, organizing files, compiling it (in a simplified way), and cleaning up. Open a terminal on your Raspberry Pi.
1. Setting Up the Project Structure
A well-organized project is easier to navigate and manage. We’ll create a main project directory with subdirectories for source code, header files, build artifacts, and documentation.
Build and Configuration Steps:
First, navigate to your home directory.
cd ~
Now, let’s create the main directory for our project, which we’ll call temp_sensor
.
mkdir temp_sensor
Next, we’ll move into our new project directory and create the internal structure. We can do this one by one, but a more efficient way is to use the -p
option with mkdir
to create parent directories as needed, even though in this case temp_sensor
already exists. This is a good habit to develop.
cd temp_sensor
mkdir -p src include build doc
Let’s verify our structure using the ls
command.
ls
Expected Output:
build doc include src
This simple structure separates our concerns: C source files will go in src
, header files in include
, compiled output in build
, and project notes in doc
.
2. Creating and Copying Source Files
Now, let’s create a placeholder main source file and a sensor driver file. We’ll use the touch
command to create empty files initially.
touch src/main.c
touch src/ds18b20_driver.c
Imagine our ds18b20_driver.c
is a generic driver we want to adapt. It’s good practice to make a backup before modifying it. We’ll use cp
to create a copy.
# cp [source] [destination]
cp src/ds18b20_driver.c src/ds18b20_driver.c.bak
The .bak
extension is a common convention for backup files. Let’s say we also need a header file for our driver. We’ll create it in the src
directory first and then move it to the correct location.
touch src/ds18b20_driver.h
ls src
Expected Output:
ds18b20_driver.c ds18b20_driver.c.bak ds18b20_driver.h main.c
3. Moving and Renaming Files with mv
The header file ds18b20_driver.h
is in the wrong place. It belongs in the include
directory. We’ll use mv
to move it.
# mv [source] [destination_directory]
mv src/ds18b20_driver.h include/
Let’s verify the move was successful by listing the contents of both directories.
ls src
ls include
Expected Output:
# ls src
ds18b20_driver.c ds18b20_driver.c.bak main.c
# ls include
ds18b20_driver.h
Now, suppose we decide on a better name for our main application file. Instead of main.c
, we want to call it temp_monitor.c
. We can use mv
to rename it. Renaming is just moving a file to the same directory but with a new name.
# mv [old_name] [new_name]
mv src/main.c src/temp_monitor.c
ls src
Expected Output:
ds18b20_driver.c ds18b20_driver.c.bak temp_monitor.c
4. Simulating a Build and Cleaning Up with rm
Let’s simulate a build process by creating some dummy output files in the build
directory.
touch build/temp_monitor.o
touch build/ds18b20_driver.o
touch build/app.elf
Our build
directory now contains these intermediate (.o
) and final (.elf
) files. After a successful build, we might want to clean up the intermediate object files but keep the final executable. We can use rm
for this.
Tip: The
-v
(verbose) option forcp
,mv
, andrm
is very useful for seeing exactly what the command is doing, especially in scripts.
Let’s try removing one file with verbose output.
rm -v build/temp_monitor.o
Expected Output:
removed 'build/temp_monitor.o'
We can also use wildcards to remove multiple files that match a pattern. Let’s remove all .o
files.
rm build/*.o
ls build
Expected Output:
app.elf
The command rm build/*.o
expands to rm build/ds18b20_driver.o
(since the other .o file was already removed), deleting the file.
Now, imagine we want to perform a full clean of the project, removing the entire build
directory and the backup file we created earlier. This is where recursive removal is needed.
First, let’s remove the backup file.
rm src/ds18b20_driver.c.bak
Next, to remove the build
directory and everything inside it, we use rm -r
.
Warning: Before running
rm -r
, it’s often wise to runls
on the target directory (ls build
) to be absolutely sure what you are about to delete.
rm -r build
ls
Expected Output:
doc include src
The build
directory is now gone. If we wanted to be prompted for every single file inside build
before deletion, we could have used rm -ri build
. The -i
(interactive) flag provides a crucial safety net.
Let’s create a directory and try to remove it without the -r
flag to see the error.
mkdir test_dir
touch test_dir/a_file.txt
rm test_dir
Expected Output:
rm: cannot remove 'test_dir': Is a directory
This is a safety feature. Linux forces you to be explicit about removing a directory and its contents by requiring the -r
flag. To clean up our test directory, we must use rm -r test_dir
.
File Structure Example Summary
After our initial setup, our project directory looked like this:
temp_sensor/
├── build/
├── doc/
├── include/
└── src/
After creating, copying, and moving files, the structure evolved to:
temp_sensor/
├── build/
├── doc/
├── include/
│ └── ds18b20_driver.h
└── src/
├── ds18b20_driver.c
├── ds18b20_driver.c.bak
└── temp_monitor.c
This organized structure, managed entirely with mkdir
, cp
, and mv
, is the hallmark of a professional development environment. The final cleanup, using rm
, is a critical part of the development cycle, ensuring a clean state for the next build.
Common Mistakes & Troubleshooting
Even seasoned developers make mistakes with these fundamental commands. Understanding the common pitfalls is the first step toward avoiding them.
Exercises
- Project Sandbox Creation:
- Objective: Create a nested directory structure for a new “led_controller” project using a single command.
- Steps:
- Navigate to your home directory (
~
). - Use one
mkdir
command to create the following structure:led_controller/source
,led_controller/scripts
, andled_controller/documentation/schematics
. - Verify the entire structure was created correctly using
ls -R led_controller
.
- Navigate to your home directory (
- Expected Outcome: All three directories, including the nested
schematics
directory, are created without any errors.
- File Population and Backup:
- Objective: Create several dummy files and safely back up a critical configuration file.
- Steps:
- Navigate into the
led_controller/source
directory. - Create three empty files:
main.c
,led_hal.c
, andconfig.h
. - Copy
config.h
toconfig.h.orig
in the same directory as a backup of the original version. - Verify that both
config.h
andconfig.h.orig
exist.
- Navigate into the
- Expected Outcome: The
source
directory contains four files:main.c
,led_hal.c
,config.h
, andconfig.h.orig
.
- Refactoring the Project:
- Objective: Rename files and move them to more appropriate locations within the project structure.
- Steps:
- From inside the
led_controller
directory, rename thesource
directory tosrc
. - Move the
config.h
andconfig.h.orig
files from thesrc
directory into a new directory namedinclude
. You will need to create this directory first. - Verify the final structure. The
src
directory should contain only the.c
files, and the newinclude
directory should contain the.h
files.
- From inside the
- Expected Outcome: The project directory contains
src
,include
,scripts
, anddocumentation
.src
holds the C files, andinclude
holds the header files.
- Interactive and Forceful Deletion:
- Objective: Understand the difference between safe, interactive deletion and standard recursive deletion.
- Steps:
- Navigate to the
led_controller
directory. - Use
rm
with the interactive flag (-i
) to deletesrc/main.c
. You should be prompted for confirmation; typey
and press Enter. - Attempt to remove the
documentation
directory usingrm
. Observe the error. - Now, remove the entire
documentation
directory and its contents using the correct recursive flag.
- Navigate to the
- Expected Outcome: The interactive prompt appears for the first deletion. An error occurs when trying to
rm
a directory. Thedocumentation
directory is successfully removed with the recursive option.
- Project Cleanup Script:
- Objective: Create and use a simple shell script to automate the cleanup of a simulated build.
- Steps:
- Inside the
led_controller
directory, create abuild
directory. - Create some dummy artifact files inside
build
:main.o
,led_hal.o
, andcontroller_app
. - In the
scripts
directory, create a file namedclean.sh
. - Edit
clean.sh
(usingnano
or another editor) to contain the following commands:#!/bin/bash echo "Cleaning project..." rm -v ../build/*.o rm -v ../src/*.orig echo "Cleanup complete."
- Make the script executable:
chmod +x scripts/clean.sh
. - Run the script from the
led_controller
directory:./scripts/clean.sh
. - Verify that the
.o
and.orig
files have been deleted, but thecontroller_app
file remains.
- Inside the
- Expected Outcome: The script executes, printing messages and showing the verbose output from
rm
. The specified files are deleted, demonstrating an automated cleanup process.
Summary
mkdir
: Creates new directories. The-p
option is essential for creating nested directory paths in a single step.cp
: Copies files and directories. It creates entirely new inodes and data blocks. The-r
option is required for copying directories recursively.mv
: Moves or renames files and directories. On the same filesystem, it’s a fast, metadata-only operation. Across different filesystems, it performs a copy-then-delete operation.rm
: Removes files by unlinking them from their inode. For most practical purposes, this is an irreversible action. The-r
flag is required to remove directories and their contents, while-f
(force) overrides prompts and can be very dangerous.- Safety: The
-i
(interactive) flag forcp
,mv
, andrm
provides a crucial safety net by prompting the user before overwriting or deleting. - Permissions: Your ability to manipulate a file is governed by the permissions of its parent directory. “Permission Denied” errors often relate to a lack of write permission on the directory, not the file itself.
- Filesystem Concepts: Understanding the distinction between a file’s name (stored in a directory) and its metadata/content (referenced by an inode) is key to understanding how these commands work efficiently.
Further Reading
- GNU Coreutils Manual: The official and most authoritative documentation for
cp
,mv
,rm
, andmkdir
. - The Linux Command Line, by William Shotts: An excellent, comprehensive book for learning the command line from the ground up. Chapters on file manipulation are particularly relevant.
- “The Linux Programming Interface” by Michael Kerrisk: While an advanced text, the chapters on filesystems and directories provide unparalleled depth on the underlying system calls (
link
,unlink
,rename
) that these command-line tools use. - Raspberry Pi Documentation – The command line: Official introductory material from the Raspberry Pi Foundation.
- explainshell.com: A fantastic interactive tool that breaks down shell commands and shows how each part and option works. Useful for deconstructing complex commands.