Creating my own cp function in bash

1,386

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).

Share:
1,386

Related videos on Youtube

user3531263er
Author by

user3531263er

Updated on September 18, 2022

Comments

  • user3531263er
    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
      smw about 7 years
      Start with www.shellcheck.net
    • muru
      muru about 7 years
      If 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
      Floris about 7 years
      One 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
      Barmar about 7 years
      You need to review the basic syntax of the [ command in your if.
    • Barmar
      Barmar about 7 years
      And 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
    user3531263er about 8 years
    yes source code not binaries. It's all working now, however I can't make NuSMV actually run
  • Patrick Trentin
    Patrick Trentin about 8 years
    you should update your question with the new error, or make a new question
  • user3531263er
    user3531263er about 8 years
  • Olivier Dulac
    Olivier Dulac about 7 years
    to 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
    gardenhead about 7 years
    One of the best answers I've seen on here! I learned about both -ef file comparisons and local -a.
  • Kusalananda
    Kusalananda about 7 years
    @gardenhead Yes, -ef takes notices if the two files are the same (takes care of links too) and local -a creates a local array variable.
  • timmaay92
    timmaay92 about 7 years
    Thank you so much! Really nice elaborate answer, really helpful in comprehending the language as well