linux

Understanding Linux cp Command and Implementing in Python

How the Linux cp command works under the hood, and how to replicate its file-copying logic in a short Python script.

As a regular Linux user, I like the simplicity of the Unix philosophy: “Have one tool, and have that tool do its job well.” This post looks at how the cp command works under the hood, then walks through a basic Python implementation of it.

cp is simple on the surface, but underneath it is a small, disciplined loop of open, read, write, and close system calls.

How the cp command works

When you type cp into your terminal, you’re invoking a program that’s part of the GNU Core Utilities, built to handle a range of file system operations.

The typical workflow of the cp command

  1. Argument Parsing: The command-line arguments, including any options (like -r for recursive copy or -i for interactive prompts), are parsed.
  2. Path Resolution: The command resolves the absolute or relative paths provided for the source and destination.
  3. Permission Checks: Before proceeding, cp checks if you have the read permission for the source file and the write permission for the destination directory.
  4. File Opening: Utilizing the open() system call, cp opens the source file for reading. If the destination file doesn’t exist, it’s created using the creat() system call; otherwise, it’s opened with open().
  5. Data Transfer: Through a loop that uses read() and write() system calls, data is transferred from the source to the destination file in chunks. This way, cp efficiently handles files of any size without consuming excessive memory.
  6. Metadata Copying: The command duplicates the source file’s metadata, such as permissions and timestamps, to the new file using system calls like fchmod() and futimens().
  7. Resource Cleanup: After the copy operation, cp closes both file descriptors using the close() system call.

Now let’s go a level deeper and look at the system calls cp uses.

The system calls behind cp

System calls are the interface between a running process and the operating system. Here’s a closer look at the ones cp uses:

open()

The open() system call is used by cp to obtain a file descriptor for the source and destination files. This system call takes a pathname and flags as arguments, determining how the file should be accessed. When copying a file, cp opens the source file in read-only mode (O_RDONLY) to ensure the file is not modified. If the destination file does not exist, cp uses open() with the O_CREAT flag to create it, and with O_WRONLY to open it for writing.

read()

After opening the source file, cp uses the read() system call to read data from the file into a buffer. This buffer temporarily stores the data as it’s being copied. The read() function takes three arguments: the file descriptor, the buffer into which the data is read, and the number of bytes to read.

write()

write() is the system call used to transfer data from the buffer to the destination file. It takes a file descriptor for the destination file, the buffer with the data, and the number of bytes to write from the buffer. cp will repeatedly read from the source and write to the destination until all data is copied.

close()

Once the copy operation is complete, cp needs to release the file descriptors so they can be reused by the system. The close() system call is used for this purpose, closing both the source and destination file descriptors.

fchmod()

Copying a file also involves duplicating its permissions. The fchmod() system call is used by cp to set the permissions of the destination file to match those of the source file. It requires the file descriptor of the open file and the mode (permission settings) to be applied.

futimens()

The futimens() system call allows cp to preserve the timestamps of the source file, setting the access and modification times of the destination file to match. It takes a file descriptor and an array of timespec structures representing the new times.

creat()

creat() is worth mentioning as it’s often used as a shorthand for open() with flags set to create a new file or rewrite an old one. It’s equivalent to open() with O_CREAT | O_WRONLY | O_TRUNC flags.

These system calls are the building blocks that let cp do its job: copying file content, permissions, and timestamps from one location to another on Linux.

Replicating cp in Python

python
import os
import sys


def copy(src: str, dest: str) -> None:
    """
    Copy the contents of the source file to the destination file or directory.
    If dest is a directory, the file will be copied into it with the same filename.

    Args:
        src (str): The path to the source file.
        dest (str): The path to the destination file or directory.

    Raises:
        FileNotFoundError: If the source file does not exist or is not a file.
        IsADirectoryError: If the destination is a directory but the filename is not provided.
    """
    # Ensure the source file exists and is a file
    if not os.path.isfile(src):
        raise FileNotFoundError(f"Source file {src} not found or is not a file.")

    # Check if destination is a directory
    if os.path.isdir(dest):
        # Construct the full file path by joining the directory with the basename of the source file
        dest = os.path.join(dest, os.path.basename(src))

    # Open the source file in binary read mode and destination file in binary write mode
    with open(src, "rb") as fsrc, open(dest, "wb") as fdst:
        # Read the source file content in chunks
        while True:
            chunk = fsrc.read(1024)  # Read in 1KB chunks
            if not chunk:
                break
            fdst.write(chunk)


# Command-line interface
if __name__ == "__main__":
    if len(sys.argv) < 3:
        print("Usage: python script.py source_file destination_path")
        sys.exit(1)

    source_path = sys.argv[1]
    destination_path = sys.argv[2]

    try:
        copy(source_path, destination_path)
        print(f"Copied {source_path} to {destination_path}")
    except Exception as e:
        print(f"Error: {e}")
        sys.exit(1)

The script reads the source file in manageable chunks (1KB in this case) and writes these chunks to the destination file, ensuring that the content is preserved during the copy process.

If the destination path is a directory, the script appends the name of the source file to the destination path to maintain the original filename in the new location. This behavior mimics the standard cp command in Linux when the target is a directory.

Wrapping up

The cp command is a good example of the Unix philosophy: simple tools that do one thing well. Looking at the system calls it uses gives you insight into how the operating system handles files. Building it again in Python shows how much a high-level language does for you compared to working at the system-call level directly.

Our Python script doesn’t cover every feature of cp, such as recursive copying or prompting the user for input, but it gets across how the command works.

I hope this gave you something new. If you have feedback, leave a comment below, or send me an email if you’d rather not comment publicly.

If you want to build projects like Docker or BitTorrent, or understand tools like Git by recreating them in the language of your choice, I’d recommend Code Crafters.



If you liked this post, you can support my work by buying me a coffee. It would mean a lot. If you share this on X, tag me @muhammad_o7, and follow me on LinkedIn

Want new posts in your inbox? Subscribe by email or follow the RSS feed.