0

I am writing a bash script to copy ONE or MORE THAN ONE or ALL files to a destination, and create the destination if it does not exist.

I have already gone through this solution but it does not complete my requirements.

My code (Which does not work for idk what reasons):

# copy files and make a directory if does not exist
mkcp() {
    # last argument is destination
    dir="${@: -1}"
    # create directory if does not exist
    mkdir -p "$dir"
    # loop over all arguments
    for file in "$@"
    do
        # if file is last argument (directory) then break loop
        if [ "$file" == "$dir" ]; then
            break
        fi
        # else keep copying files
        cp -R "$file" "$dir"
    done
}

I want all these commands to be working:

# copies "text.txt" to "testdir" (testdir may or may not exist so it must be created first)
$ mkcp test.txt ~/desktop/testdir

# copies "test1.txt" and "test2.txt" to "testdir" (conditions are same)
$ mkcp test1.txt test2.txt ~/desktop/testdir

# copies "all files" to "testdir" (conditions are same)
$ mkcp * ~/desktop/testdir

If there's any other solution that can complete my requirements, I am okay with it too.

Note: The mkcp function is stored in .zshrc.

glenn jackman
  • 238,783
  • 38
  • 220
  • 352
Vishwa Mittar
  • 378
  • 5
  • 16
  • 1
    Clearly, `mkdir -p dir` should be `mkdir -p "$dir"`. Similar problems are in `if [ file == dir ]; then` and `cp -R "$file" dir`. Variables must be prefixed with a `$` and should be surrounded by quotes unless you know exactly why not. – Jonathan Leffler Apr 19 '22 at 00:49
  • Consider whether the GNU-style `cp` interface with `-t target-dir` as a way to specify the target directory would be better. It is very useful. – Jonathan Leffler Apr 19 '22 at 00:51
  • @JonathanLeffler sorry I am new to bash. I made all the changes you mentioned, now I am getting the error `mkcp:9: = not found`. It would be great if you write the correct script in answer. – Vishwa Mittar Apr 19 '22 at 01:00
  • Wait, .zshrc? Well this is not a bash question at all – glenn jackman Apr 19 '22 at 02:42

4 Answers4

2

I added this bash answer before I realized this is a zsh question. Not deleting it in hopes others find it useful.


You can take a sublist of the positional arguments using the ${var:offset:length} expansion. See Shell Parameter Expansion in the manual.

Perhaps this:

mkcp() {
    local dir="${@: -1}"

    mkdir -p "$dir"

    for file in "${@:0:$#}"     # all but last
    do
        cp -vR "$file" "$dir"
    done
}

In fact, it can be simpler, assuming you have GNU cp with the -t option:

mkcp() {
    local dir="${@: -1}"
    mkdir -p "$dir"
    cp -t "$dir" -vR "${@:0:$#}"
}

I added cp's -v option for extra verbosity, so you can see what files are being copied.

glenn jackman
  • 238,783
  • 38
  • 220
  • 352
  • The tag says zsh; the text says bash. The question is self-inconsistent. – Jonathan Leffler Apr 19 '22 at 08:34
  • Sorry, I made a typo in my command, that's why it was not working. But now I fixed it and it's working smoothly. Thanks. – Vishwa Mittar Apr 19 '22 at 11:56
  • @JonathanLeffler : You are right, but since the OP further down explains that he is going to put this function into .zshrc, I assume that his mentioning of _bash_ is a typo. – user1934428 Apr 19 '22 at 12:25
2

You assume that the last argument must be a directory, and if no such directory exists, it should be created. In zsh, I would do it like this:

mkcp() {
  local destdir=$@[-1]
  if [[ -f $destdir ]]
  then
    echo Missing destination directory 2>&1 
    return 1
  else
    mkdir -p $destdir
    if [[ -d $destdir ]]
    then
      cp "$@"
    else
      echo Can not create $destdir
      return 1
    fi
  fi
}

The problem is not the copying (the plain cp command can do it and no loop is needed), so the focus here is on error checking.

Note that with my approach, switches can be passed implicitly to cp, for instance

mkcp -r foo bar baz

copies recursively the subdirectories too, and

mkcp -rv foo bar baz

is in addition printing the names of the files which are copied.

user1934428
  • 19,864
  • 7
  • 42
  • 87
0

This is what the main changes I suggest should look like:

mkcp() {
    # last argument is destination
    dir="${@: -1}"
    # create directory if does not exist
    mkdir -p "$dir"
    # loop over all arguments
    for file in "$@"
    do
        # if file is last argument (directory) then break loop
        if [ "$file" == "$dir" ]; then
            break
        fi
        # else keep copying files
        cp -R "$file" "$dir"
    done
}

I'm not clear how you'd get the error message mkcp:9: = not found; to debug that, I'd need to see your modified code. OTOH, I'm not convinced that debugging that is going to be particularly constructive.

Jonathan Leffler
  • 730,956
  • 141
  • 904
  • 1,278
  • I've edited my question with the changes mentioned by you, it's the same code in your answer, and still getting this error. This question (505 upvotes) is already answered but it's for a single file. It can be useful if someone wants to copy multiple files at once. – Vishwa Mittar Apr 19 '22 at 01:15
  • 1
    @VishwaMittar, you have `if [ file == "$dir" ]`, this answer correctly has `if [ "$file" == "$dir" ]` – glenn jackman Apr 19 '22 at 02:32
  • @glennjackman I just realized it. Thanks for the correction tho. – Vishwa Mittar Apr 19 '22 at 02:52
0

The error is on this command

# copies "all files" to "testdir" (conditions are same)
$ mkcp * ~/desktop/testdir

You're passing a string "*" as command to a custom function, so when you send *mkcp * you're actually saying "cp -R * /path" where * is just a string, not 'all'.

You can try to do this way:

mkcp() {
    # last argument is destination
    dir="${@: -1}"
    # create directory if does not exist
    mkdir -p "$dir"
    
    # loop over all arguments
    for file in "$@"
    do
        if [[ "${file}" == "*" ]]; then
            LIST_FILE=$(ls $dir)
            for i in $LIST_FILE
            do
               cp $i $dir
            done
            break
        fi
        # if file is last argument (directory) then break loop
        if [ file == "$dir" ]; then
            break
        fi
        # else keep copying files
        cp -R "$file" "$dir"
    done
}

If you need to 'debug' what the script is executing, try to run the script as:

bash -x mkcp * ~/desktop/testdir

The -x will how each step of the script.

DharmanBot
  • 1,066
  • 2
  • 6
  • 10
maryf
  • 140
  • 6
  • Hi, I copied your code and ran the command `bash -x mkcp * ~/desktop/testdir` but I am getting error: `bash: mkcp: No such file or directory` – Vishwa Mittar Apr 19 '22 at 01:32
  • I think in your code `LIST_FILE=$(ls $dir)` will be `LIST_FILE=$(ls .)` since we are copying all files from the current directory. I might be wrong but it's just a suggestion. – Vishwa Mittar Apr 19 '22 at 01:34
  • 1
    @maryf, no, the shell will expand the `*` **before** launching the function. See [3.5 Shell Expansions](https://www.gnu.org/software/bash/manual/bash.html#Shell-Expansions) in the manual, and read the Filename Expansion section. – glenn jackman Apr 19 '22 at 02:34