![在 bash 中创建我自己的 cp 函数](https://linux22.com/image/112012/%E5%9C%A8%20bash%20%E4%B8%AD%E5%88%9B%E5%BB%BA%E6%88%91%E8%87%AA%E5%B7%B1%E7%9A%84%20cp%20%E5%87%BD%E6%95%B0.png)
cp
对于一项作业,我被要求巧妙地编写一个 bash 函数,该函数具有与函数(复制)相同的基本功能。它只需将一个文件复制到另一个文件,因此无需将多个文件复制到新目录。
由于我是 bash 语言的新手,我无法理解为什么我的程序无法运行。原始函数要求覆盖文件(如果它已经存在),所以我尝试实现它。它失败。
该文件似乎在多行上失败,但最重要的是在检查要复制到的文件是否已存在的情况下([-e "$2"]
)。即便如此,它仍然显示如果满足该条件(文件名...)则应该触发的消息。
任何人都可以帮助我修复这个文件,可能为我对语言的基本理解提供一些有用的见解吗?代码如下。
#!/bin/sh
echo "file variable: $2"
if [-e file]&> /dev/null
then
echo "The file name already exists, want to overwrite? (yes/no)"
read | tr "[A-Z]" "[a-z]"
if [$REPLY -eq "yes"] ; then
rm "$2"
echo $2 > "$2"
exit 0
else
exit 1
fi
else
cat $1 | $2
exit 0
fi
答案1
如果目标文件已经存在,该cp
实用程序将很乐意覆盖该文件,而不提示用户。
cp
实现基本功能而不使用的函数cp
将是
cp () {
cat "$1" >"$2"
}
如果您想在覆盖目标之前提示用户(请注意,如果该函数是由非交互式 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"
}
诊断消息应发送至标准错误流。这就是我所做的printf ... >&2
。
请注意,我们实际上并不需要rm
目标文件,因为重定向会截断它。要是我们做过首先想要rm
它,然后你必须检查它是否是一个目录,如果是,则将目标文件放在该目录中,就像这样cp
。这是这样做的,但仍然没有明确的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"
}
您可能还想确保源确实存在,这是cp
做do (cat
也这样做,所以当然可以完全省略它,但这样做会创建一个空的目标文件):
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"
}
该函数不使用“bashisms”,并且应该在所有sh
类似的 shell 中工作。
通过更多的调整来支持多个源文件和一个-i
在覆盖现有文件时激活交互式提示的标志:
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
}
您的代码中的间距错误if [ ... ]
(需要在 之前、之后[
和之前留出空间]
)。您也不应该尝试将测试重定向到,/dev/null
因为测试本身没有输出。此外,第一个测试应该使用位置参数$2
,而不是字符串file
。
像我一样使用case ... esac
,您可以避免使用小写/大写来自用户的响应tr
。在 中bash
,如果您无论如何都想这样做,一种更便宜的方法是使用REPLY="${REPLY^^}"
(for uppercasing) 或REPLY="${REPLY,,}"
(for lowercasing)。
如果用户对您的代码说“是”,则该函数会将目标文件的文件名放入目标文件中。这不是源文件的副本。它应该落到函数的实际复制位。
复制位是您使用管道实现的。管道用于将数据从一个命令的输出传递到另一命令的输入。这不是我们需要在这里做的事情。只需调用cat
源文件并将其输出重定向到目标文件即可。
你之前打电话也有同样的问题tr
。 read
将设置变量的值,但不产生输出,因此通过管道传输read
任何内容都是无意义的。
除非用户说“不”,否则不需要显式退出(或者该函数遇到一些错误情况,如我的代码位中所示,但因为它是我使用的函数return
而不是exit
)。
另外,你说的是“函数”,但你的实现是一个脚本。
看看https://www.shellcheck.net/,它是识别 shell 脚本中有问题的部分的好工具。
使用cat
只是一复制文件内容的方法。其他方式包括
dd if="$1" of="$2" 2>/dev/null
- 使用任何类似过滤器的实用程序,可以使数据仅通过,例如
sed "" "$1" >"2"
或awk '1' "$1" >"$2"
等tr '.' '.' <"$1" >"$2"
。 - ETC。
棘手的一点是让函数将元数据(所有权和权限)从源复制到目标。
cp
另一件需要注意的事情是,如果目标是/dev/tty
(非常规文件)之类的东西,我编写的函数的行为将完全不同。