Creating my own cp function in bash
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"
orawk '1' "$1" >"$2"
ortr '.' '.' <"$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).
Related videos on Youtube
user3531263er
Updated on September 18, 2022Comments
-
user3531263er over 1 year
I downloaded NuSMV source code for mac and started installing using the README. However, there is a step which asks me to build using 'cmake..' when I run that I get the issue The source directory does not appear to contain CMakeLists.txt.
Any help please?
-
smw about 7 yearsStart with www.shellcheck.net
-
muru about 7 yearsIf this were code review: do not ever do
[ ... ] &> /dev/null
. The tests supported by[ ... ]
are always silent, it's only in case of syntax errors that any output is produced. Silencing such a test is simply digging your own grave. -
Floris about 7 yearsOne point:
cat $1 | $2
"pipes" the output of the first command to the command in your second variable. But that is a file name, not the name of a command to execute. -
Barmar about 7 yearsYou need to review the basic syntax of the
[
command in yourif
. -
Barmar about 7 yearsAnd you need to learn the difference between piping with
|
and redirecting to a file with>
. These are basic syntax issues that already should have been covered in your class.
-
-
user3531263er about 8 yearsyes source code not binaries. It's all working now, however I can't make NuSMV actually run
-
Patrick Trentin about 8 yearsyou should update your question with the new error, or make a new question
-
user3531263er about 8 yearsI have done that here stackoverflow.com/questions/36662953/running-nusmv-on-osx
-
Olivier Dulac about 7 yearsto add some interresting precision on how it works:
cat "$1" >"$2"
: will work in 2 times: the shell first sees> "$2"
, which means: create-or-clobber(=empty) the file "$2", and redirect the command's stdout to that file. Then it launches the command:cat "$1"
, and redirects its stdout to file"$2"
(already emptied or created empty). -
gardenhead about 7 yearsOne of the best answers I've seen on here! I learned about both
-ef
file comparisons andlocal -a
. -
Kusalananda about 7 years@gardenhead Yes,
-ef
takes notices if the two files are the same (takes care of links too) andlocal -a
creates a local array variable. -
timmaay92 about 7 yearsThank you so much! Really nice elaborate answer, really helpful in comprehending the language as well