How can I create directories during a copy in bash/zsh/ksh?
-
13-09-2019 - |
Question
I get the following messages often, for instance when coping dev files to a master branch
cp: /Users/Masi/gitHub/shells/zsh/dvorak: No such file or directory
cp: /Users/Masi/gitHub/shells/zsh/dvorak2: No such file or directory
I would like to be asked about the creation of the given folders such that my initial command will be run if I answer yes to the question(s).
My attempt in pseudo-code when I am trying to copy a file to a directory which does not exists
if no such a directory exists, then asks users about to create it:
if yes, then mkdir directory AND run the initial command again
else do noting
Problems
- To change the warning message: Which file does control the "No such file or directory" -command?
- To scrape the Path in the initial command AND mkidr Path without the file: How would scrape the Path in the initial command?
- To scrape from the end with your chosen language such as AWK: How would you remove the last match in the Path when / is the field separator? I am not sure how you can scrape letters starting from the end with AWK.
Solution
Here is a function I wrote which will work in zsh, bash or ksh.
Note: It has debugging enabled (it echoes the commands it would run rather than executing them). If you comment out that line, it will actually run them.
Caution: It hasn't been thoroughly tested.
To use it, put this script in a file called cpmd
in /usr/local/bin
(or elsewhere in your path). To activate it, from the shell prompt type the following command (or add it to your startup script - for bash it would be ~/.bashrc
):
source cpmd
Then you can copy a file using a command like this:
cpmd carparts /home/dave/Documents/nonexistent/newdir/
Neither directory "nonexistent" or "newdir" exist yet. Both directories are created then the file named "carparts" is copied to "newdir".
If you don't include a slash ("/") at the end, the last part is treated as a file name and any non-existent directories before that are created:
cpmd supplies /home/dave/Documents/anothernew/consumables
The directory "anothernew" is created then "supplies" is copied with the new filename "consumables".
If all the directories in the destination already exist, cpmd
acts like the regular cp
command.
function cpmd {
# copies files and makes intermediate dest. directories if they don't exist
# for bash, ksh or zsh
# by Dennis Williamson - 2009-06-14
# http://stackoverflow.com/questions/993266/unable-to-make-nosuchdirectory-message-useful-in-zsh
# WARNING: no validation is performed on $1 and $2
# all cp commands below are hardcoded with -i (interactive) to prevent overwriting
if [[ -n $KSH_VERSION ]]
then
alias local=typeset
local func="$0"
local lastchar="${2: -1}"
readcmd () { read "$2?$1"; }
elif [[ -n $ZSH_VERSION ]]
then
local func="$0"
# the following two lines are split up instead of doing "${2[-1]}"
# to keep ksh from complaining when the function is loaded
local dest="$2"
local lastchar="${dest[-1]}"
readcmd () { read "$2?$1"; }
elif [[ -n $BASH_VERSION ]]
then
local func="$FUNCNAME"
local lastchar="${2:(-1)}"
readcmd () { read -p "$1" $2; }
else
echo "cpmd has only been tested in bash, ksh and zsh." >&2
return 1
fi
local DEBUG='echo' # COMMENT THIS OUT to make this function actually work
if [[ ${#@} != 2 ]]
then
echo "$func: invalid number of parameters
Usage:
$func source destination
where 'destination' can include nonexistent directories (which will
be created). You must end 'destination' with a / in order for it to
specify only directories. Without the final slash, the 'source' will
be copied with a new name (the last portion of 'destination'). If you
are copying multiple files and 'destination' is not a directory, the
copy will fail." >&2
return 1
fi
local dir=$(dirname "$2")
local response
local nl=$'\n'
# destination ($2) is presumed to be in one of the following formats:
# .../existdir test 1 (-d "$2")
# .../existdir/existfile test 2 (-f "$2")
# .../existdir/newfile test 3 (-d "$dir" && $lastchar != '/')
# .../existdir/newdir/ (else)
# .../newdir/newdir/ (else)
# .../newdir/newfile (else)
if [[ -d "$2" || -f "$2" || (-d "$dir" && $lastchar != '/') ]]
then
$DEBUG cp -i "$1" "$2"
else
if [[ $lastchar == '/' ]]
then
dir="$2"
fi
local prompt="$func: The destination directory...${nl} ${dir}${nl}...does not exist. Create? (y/n): "
while [[ -z $response ]]
do
readcmd "$prompt" response
case $response in
y|Y) response="Y" ;;
n|N) ;;
*) response=
prompt="$func: Invalid response.${nl} Create destination directory? (y/n): ";;
esac
done
if [[ $response == "Y" ]]
then
$DEBUG mkdir -p "$dir" && $DEBUG cp -i "$1" "$2"
else
echo "$func: Cancelled." >&2
fi
fi
}
OTHER TIPS
That error message is coming from the cp
command, not zsh. If you want to improve the output, you're going to have to write the logic for truncating and examining the path along with checking to see if it exists or not.
There are commands to assist with this, have a look at basename(1) and dirname(1).