The cp
utility will happily overwrite the target file if that file already exists, without prompting the user.
A function that implements basic cp
capability, without using cp
would be
cp () {
cat "$1" >"$2"
}
If you want to prompt the user before overwriting the target (note that it may not be desireable to do this if the function is called by a non-interactive shell):
cp () {
if [ -e "$2" ]; then
printf '"%s" exists, overwrite (y/n): ' "$2" >&2
read
case "$REPLY" in
n*|N*) return ;;
esac
fi
cat "$1" >"$2"
}
The diagnostic messages should go to the standard error stream. This is what I do with printf ... >&2
.
Notice that we don't really need to rm
the target file as the redirection will truncate it. If we did want to rm
it first, then you'd have to check whether it's a directory, and if it is, put the target file inside that directory instead, just the way cp
would do. This is doing that, but still without explicit rm
:
cp () {
target="$2"
if [ -d "$target" ]; then
target="$target/$1"
fi
if [ -d "$target" ]; then
printf '"%s": is a directory\n' "$target" >&2
return 1
fi
if [ -e "$target" ]; then
printf '"%s" exists, overwrite (y/n): ' "$target" >&2
read
case "$REPLY" in
n*|N*) return ;;
esac
fi
cat "$1" >"$target"
}
You may also want to make sure that the source actually exists, which is something cp
does do (cat
does it too, so it may be left out completely, of course, but doing so would create an empty target file):
cp () {
if [ ! -f "$1" ]; then
printf '"%s": no such file\n' "$1" >&2
return 1
fi
target="$2"
if [ -d "$target" ]; then
target="$target/$1"
fi
if [ -d "$target" ]; then
printf '"%s": is a directory\n' "$target" >&2
return 1
fi
if [ -e "$target" ]; then
printf '"%s" exists, overwrite (y/n): ' "$target" >&2
read
case "$REPLY" in
n*|N*) return ;;
esac
fi
cat "$1" >"$target"
}
This function uses no "bashisms" and should work in all sh
-like shells.
With a little bit more tweaking to support multiple source files and a -i
flag that activates the interactive prompting when overwriting an existing file:
cp () {
local interactive=0
# Handle the optional -i flag
case "$1" in
-i) interactive=1
shift ;;
esac
# All command line arguments (not -i)
local -a argv=( "$@" )
# The target is at the end of argv, pull it off from there
local target="${argv[-1]}"
unset argv[-1]
# Get the source file names
local -a sources=( "${argv[@]}" )
for source in "${sources[@]}"; do
# Skip source files that do not exist
if [ ! -f "$source" ]; then
printf '"%s": no such file\n' "$source" >&2
continue
fi
local _target="$target"
if [ -d "$_target" ]; then
# Target is a directory, put file inside
_target="$_target/$source"
elif (( ${#sources[@]} > 1 )); then
# More than one source, target needs to be a directory
printf '"%s": not a directory\n' "$target" >&2
return 1
fi
if [ -d "$_target" ]; then
# Target can not be overwritten, is directory
printf '"%s": is a directory\n' "$_target" >&2
continue
fi
if [ "$source" -ef "$_target" ]; then
printf '"%s" and "%s" are the same file\n' "$source" "$_target" >&2
continue
fi
if [ -e "$_target" ] && (( interactive )); then
# Prompt user for overwriting target file
printf '"%s" exists, overwrite (y/n): ' "$_target" >&2
read
case "$REPLY" in
n*|N*) continue ;;
esac
fi
cat -- "$source" >"$_target"
done
}
Your code has bad spacings in if [ ... ]
(need space before and after [
, and before ]
). You also shouldn't try redirecting the test to /dev/null
as the test itself has no output. The first test should furthermore use the positional parameter $2
, not the string file
.
Using case ... esac
as I did, you avoid having to lowercase/uppercase the response from the user using tr
. In bash
, if you had wanted to do this anyway, a cheaper way of doing it would have been to use REPLY="${REPLY^^}"
(for uppercasing) or REPLY="${REPLY,,}"
(for lowercasing).
If the user says "yes", with your code, the function puts the filename of the target file into the target file. This is not a copying of the source file. It should fall through to the actual copying bit of the function.
The copying bit is something you've implemented using a pipeline. A pipeline is used to pass data from the output of one command to the input of another command. This is not something we need to do here. Simply invoke cat
on the source file and redirect its output to the target file.
The same thing is wrong with you calling of tr
earlier. read
will set the value of a variable, but produces no output, so piping read
to anything is nonsensical.
No explicit exit is needed unless the user says "no" (or the function comes across some error condition as in bits of my code, but since it's a function I use return
rather than exit
).
Also, you said "function", but your implementation is a script.
Do have a look at https://www.shellcheck.net/, it's a good tool for identifying problematic bits of shell scripts.
Using cat
is just one way to copy the contents of a file. Other ways include
dd if="$1" of="$2" 2>/dev/null
- Using any filter-like utility who can be made to just pass data through, e.g.
sed "" "$1" >"2"
or awk '1' "$1" >"$2"
or tr '.' '.' <"$1" >"$2"
etc.
- etc.
The tricky bit is to make the function copy the metadata (ownership and permissions) from source to target.
Another thing to notice is that the function I've written will behave quite differently from cp
if the target is something like /dev/tty
for example (a non-regular file).
Best Answer
read
has a parameter for timeout, you can use:If you want
read
to wait for a single character (whole line + Enter is default), you can limit input to 1 char:After proper input, return value will be 0, so you can check for it like this:
I guess there is no need to implement background jobs in your situation, but if you want to, here is an example: