Copying Directory Structure With a Bash Script - With Help From ChatGPT AI

Published: 2024 January 22

system administration Linux Bash shell script software development artificial intelligence

Introduction

In this post we’ll create a Bash command for copying the directory structure of one directory into another directory using a Bash script - and we’ll use ChatGPT to generate the script.

ChatGPT Conversation


Our Prompt

Write a Bash command to recursively copy the directory structure of one directory to another directory.

ChatGPT’s Response

To recursively copy just the directory structure (without copying the actual files), you can use the `find` command in combination with `mkdir -p` and some text manipulation. Here’s how you can do it:

bash
cd /path/to/source_directory
find . -type d -exec mkdir -p /path/to/destination_directory/{} \;

Here’s what this command does:

  1. `cd /path/to/source_directory`: Navigate to the directory whose structure you want to copy.
  2. `find . -type d`: This finds all directories (`-type d`) starting from the current directory (.).
  3. `-exec mkdir -p /path/to/destination_directory/{} ;`: For each directory found, it creates the corresponding directory structure in the destination directory.

Make sure to replace `/path/to/source_directory` with the path of the directory whose structure you want to copy and `/path/to/destination_directory` with the path where you want to replicate the directory structure.

That’s not a bad response. It doesn’t handle sub-directories that have a space in the name, but it’s a good place to start. Let’s give it a chance to fix that issue.


Our Prompt

Can you alter this to handle directories with spaces in their names?

ChatGPT’s Response

Certainly! To handle directories with spaces in their names, you can modify the command to properly quote the directory names. Here’s an updated version:

bash
cd "/path/to/source_directory"
find . -type d -print0 | while IFS= read -r -d '' dir; do
    mkdir -p "/path/to/destination_directory/${dir#./}"
done

Here’s the breakdown of the changes:

  1. The `-print0` option for `find` and `-d ‘'` option for `read` use null characters (`\0`) as the delimiter instead of newline characters. This helps in handling file or directory names containing spaces or other special characters.

  2. `${dir#./}` removes the leading `./` from the directory path, ensuring that the structure is replicated correctly in the destination directory.

Remember to replace `/path/to/source_directory` with the path to your source directory and `/path/to/destination_directory` with the path where you want the directory structure to be copied.

That seems to give us what we’re wanting, so let’s look at the command more closely.


The Command

Let’s break down this command to understand what it’s doing.

find . -type d -print0 | while IFS= read -r -d '' dir; do
    mkdir -p "/path/to/destination_directory/${dir#./}"
done

There are three basic components to this command:

  1. find...

  2. |

  3. while... do... done

Let’s look at each in turn.

find command

find . -type d -print0 ...

find - “Find files or directories under the given directory tree, recursively.” (From tldr pages.)

find . -type d -print0 - Put together, this command finds files of type directory in the current working directory and prints the full name to standard output followed by a null character.

pipe operator (|)

... | ...

| - The pipe operator “connects the STDOUT (standard output) file descriptor of the first process to the STDIN (standard input) of the second. What happens then is that when the first process writes to its STDOUT, that output can be immediately read (from STDIN) by the second process.” (From this StackOverflow answer.)

while loop

... while IFS= read -r -d '' dir; do
    mkdir -p "/path/to/destination_directory/${dir#./}"
done

while [TEST-CONDITION]; do [CONSEQUENT-COMMANDS]; done - This is the basic while loop structure in Bash. (From the Bash Reference Manual.)

We’ll look at the test condition and the consequent command separately.

while loop test condition

The test condition of IFS= read -r -d '' dir has two components:

  1. Redefining IFS, and
  2. The read command.

Redefining IFS

read comand

read [OPTION(S)] [NAME(S)] - This is a basic read command form in Bash. (From the Bash Reference Manual for Bash Builtin Commands.)

IFS= read -r -d '' dir - Put together, this command sets the IFS variable to Null in the scope of the read command, and read then splits the standard input into “words” when it finds a Null, treating backslash and newline as normal characters that are part of the “word”. The read command then assigns the word to the dir variable.

consequent commands in the while loop

The consequent command of mkdir -p "/path/to/destination_directory/${dir#./}" is last part of the overall command.

mkdir [OPTION(S)] [NAME(S)] - This is a basic mkdir command form. (From the GNU Coreutils Manual for mkdir.)


ChatGPT’s Response

Let’s break down `${dir#./}`:

  • `${dir}`: This is the variable representing each directory found by the `find` command during each iteration of the loop.

  • `#./`: This is a parameter expansion syntax in Bash. `${dir#./}` removes the shortest match of `./` (if it exists) from the beginning of the `$dir` variable. This is used to ensure that the directory structure is replicated correctly in the destination directory without including the leading `./`.

So, for example, if `dir` is `./path/to/source_directory/subdir`, `${dir#./}` would result in `path/to/source_directory/subdir`. This ensures that the directory structure is maintained in the destination directory without creating unnecessary subdirectories starting with `./`.

In summary, `${dir#./}` is a way to remove the leading `./` from the directory path obtained from the `find` command, ensuring that the structure is replicated correctly in the destination directory.


That seems to comport with the Bash Reference Manual for Shell Parameter Expansion section dealing with ${parameter#word}, which states that “the word is expanded to produce a pattern… If the pattern matches the beginning of the expanded value of parameter, then the result of the expansion is the expanded value of parameter with the shortest matching pattern (the ‘#’ case)… deleted.”

Based on that, it appears that the line ${dir#./} is a standard shell parameter expansion where the leading ./ characters, if any, are removed.

mkdir -p "/path/to/destination_directory/${dir#./}" - Taken together, the consequent command makes a directory (and all required parent directories) at the path specified, and appends the directory names from the earlier find command, ignoring any leading ./ characters.

The assembled command

find . -type d -print0 | while IFS= read -r -d '' dir; do
    mkdir -p "/path/to/destination_directory/${dir#./}"
done

Taken all together, this command will:

  1. find files of type directory in the current working directory and prints the full name to standard output followed by a null character, which is then
  2. piped to the next command, which is a
  3. while loop that separates those full names (by the inserted null character) and creates a matching directory at the specified target location.

Conclusion

Using ChatGPT to build the baseline Bash command was much faster than attempting to piece it together manually. Trial and error was involved, but that would be true for a script built manually as well. And when there were questions about a portion of command, ChatGPT was helpful at explaining that portion.

We have the command we’re looking for and, thanks to ChatGPT, saved a lot of time in building it.

Setting Up a Raspberry Pi as a Linux Print Server with CUPS

Published: 2024 January 19

Setting Up a Raspberry Pi Zero 2 W with Ubuntu Server

Published: 2024 January 04

Bash and The Linux Shell - Basic Terminal Command Quick Reference

Published: 2023 October 18