在bash中创建我自己的cp函数


8

对于一项作业,我被要求巧妙地编写一个bash函数,该函数具有与该函数cp(副本)相同的基本功能。它仅需将一个文件复制到另一个文件,因此无需将多个文件复制到新目录。

由于我是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

17
从www.shellcheck.net开始
steeldriver '17

2
如果这是代码审查:永远不要这样做[ ... ] &> /dev/null。所支持的测试[ ... ]始终是静默的,只有在语法错误的情况下才产生任何输出。沉默这样的测试只是在挖自己的坟墓。
muru

1
要点:cat $1 | $2将第一个命令的输出“传递”到第二个变量中的命令。但这是文件名,而不是要执行的命令的名称。
弗洛里斯(Floris)

您需要在中查看[命令的基本语法if
巴马尔(Barmar)'17年

而且,您需要了解使用进行管道|传递和使用重定向到文件之间的区别>。这些是您班上应该已经讨论的基本语法问题。
巴马尔(Barmar)'17

Answers:


26

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 确实可以做到(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^^}"(用于大写字母)或REPLY="${REPLY,,}"(用于小写字母)。

如果用户对您的代码说“是”,则该函数会将目标文件的文件名放入目标文件中。这不是源文件的副本。它应该属于函数的实际复制位。

复制位是您使用管道实现的。管道用于将数据从一个命令的输出传递到另一命令的输入。这不是我们在这里需要做的事情。只需cat在源文件上调用并将其输出重定向到目标文件即可。

tr早些时候打来的电话,也是一样。 read将设置变量的值,但不产生任何输出,因此read对任何内容进行管道传递都是毫无意义的。

除非用户说“ no”,否则不需要显式退出(否则该函数会遇到一些错误情况,如我的代码中的某些部分所示,但由于它是我使用的函数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"等方式传递数据。
  • 等等

棘手的一点是使函数将元数据(所有权和权限)从源复制到目标。

需要注意的另一件事是,我编写的函数的行为cp与目标(/dev/tty例如非常规文件)之类的行为有很大不同。


要增加一些精确的工作原理,请执行以下操作cat "$1" >"$2" ::将工作两次:shell首先看到> "$2",这意味着:create-or-clobber(= empty)文件“ $ 2”,并将命令的stdout重定向到该文件。然后,它启动命令:cat "$1",并将其标准输出重定向到文件"$2"(已清空或创建为空)。
奥利维尔·杜拉克

我在这里看到的最好的答案之一!我了解了-ef文件比较和local -a
Gardenhead

@gardenhead是的,请-ef注意两个文件是否相同(也要注意链接),并local -a创建一个本地数组变量。
库萨兰达

非常感谢!很好的精心解答,对理解语言也很有帮助
timmaay92
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.