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:
- `cd /path/to/source_directory`: Navigate to the directory whose structure you want to copy.
- `find . -type d`: This finds all directories (`-type d`) starting from the current directory (.).
- `-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:
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.
`${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:
-
find...
-
|
-
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.)
-
.
- Tells find to search the current working directory. -
-type d
- “File is of type… directory.” (From manual pages for find.) -
-print0
- “Print the full file name on the standard output, followed by a null character (instead of the newline character that -print uses). This allows file names that contain newlines or other types of white space to be correctly interpreted by programs that process the find output.” (From manual pages for find.)
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:
- Redefining IFS, and
- The read command.
Redefining IFS
-
IFS
- (Internal Field Separator) “A list of characters that separate fields; used when the shell splits words as part of expansion.” (From the Bash Reference Manual for IFS.)IFS=
- This sets the value of the IFS (Internal Field Separator) variable to Null, which when used here with the read command causes it “to preserve leading and trailing white space” (according to this StackOverflow answer). And because it is not terminated with a semicolon, “redefines [the IFS variable] only for the read invocation” (according to this StackOverflow answer).
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.)
-
read
- “Shell builtin for retrieving data from standard input.” (From tldr pages.) -
-r
- “If this option is given, backslash does not act as an escape character. The backslash is considered to be part of the line.” (From the Bash Reference Manual for Bash Builtin Commands.) -
-d [DELIM]
- “The first character of delim is used to terminate the input line, rather than newline. If delim is the empty string, read will terminate a line when it reads a NUL character.” (From the Bash Reference Manual for Bash Builtin Commands.)-d ''
- Per the above specification, this argument instructs read to terminate a line when it reads a NUL character.
-
read ... [NAME(S)]
- “One line is read from the standard input, … split into words…, and the first word is assigned to the first name, the second word to the second name, and so on.” (From the Bash Reference Manual for Bash Builtin Commands.)read ... dir
- Per the above specification, this portion takes the first word split by read and assigns it to the variable dir.
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.)
-
mkdir
- “Creates directories with the specified names.” (From the GNU Coreutils Manual for mkdir.) -
-p
- This option “makes any missing parent directories for each argument”. (From the GNU Coreutils Manual for mkdir.) -
${[PARAMETER]}
- This is “the basic form of parameter expansion” in Bash. (From the Bash Reference Manual for Shell Parameter Expansion.)-
${dir}
- Per the above definition, this portion of the [NAME] field in the mkdir command substitutes the directory string from the earlier read command. -
${dir#./}
- This line, filled with special characters, proved somewhat obscure and difficult to search. We turned again to ChatGPT for an explanation.
-
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:
- 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
- piped to the next command, which is a
- 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.