uutils / coreutils

Cross-platform Rust rewrite of the GNU coreutils
https://uutils.github.io/
MIT License
17.55k stars 1.26k forks source link

cp -r behaves differently if target_dir exists or not #6671

Open therealchfkch opened 2 weeks ago

therealchfkch commented 2 weeks ago

Describe the bug

When i use cp -r source-dir target-dir and target-dir does not exist yet, it copies the content (without the source-dir folder) to target-dir. On the other hand, when target-dir already exists, the result is target-dir/source-dir with the contents.

I am not sure which of the 2 expected behaviors are intended or how to avoid it.

How to reproduce

  1. Create a directory (named source-dir for this test) and place one or more dummy files in it (file1.md, file2.md).
  2. execute cp -r source-dir target-dir
  3. make sure the files are directly located in the root of target-dir
  4. execute cp -r source-dir target-dir again
  5. now the files are in a subfolder of target-dir named source-dir

Expected behavior

step 4 produces results like in step 2 & 3 as described above

OR

step 2 produces results like step 4 & 5 as described above

Additional Information

output after step 2:

erd target-dir/
0 B ┌─ file1.md 0 B ├─ file2.md target-dir

2 files

output after step 4:

erd target-dir/
0 B ┌─ file1.md 0 B ├─ file2.md ┌─ source-dir 0 B ├─ file1.md 0 B ├─ file2.md target-dir

1 directory, 4 files

Run with --debug and -v flag

output of step 2

cp -r source-dir target-dir --debug -v
'/home/roman/source-dir' -> '/home/roman/target-dir/' '/home/roman/source-dir/file1.md' -> '/home/roman/target-dir/file1.md' copy offload: unknown, reflink: unsupported, sparse detection: no '/home/roman/source-dir/file2.md' -> '/home/roman/target-dir/file2.md' copy offload: unknown, reflink: unsupported, sparse detection: no

output of step 4

cp -r source-dir target-dir --debug -v
'/home/roman/source-dir' -> '/home/roman/target-dir/source-dir' '/home/roman/source-dir/file1.md' -> '/home/roman/target-dir/source-dir/file1.md' copy offload: unknown, reflink: unsupported, sparse detection: no '/home/roman/source-dir/file2.md' -> '/home/roman/target-dir/source-dir/file2.md' copy offload: unknown, reflink: unsupported, sparse detection: no

Notes

I have also opened an issue at nushell, but i think this is an upstream issue, so i opened this one after some discussion.

therealchfkch commented 2 weeks ago

Nushell issue (just in case) https://github.com/nushell/nushell/issues/13717

tavianator commented 2 weeks ago

This is specified by POSIX. Quoting from https://pubs.opengroup.org/onlinepubs/9799919799/utilities/cp.html (emphasis mine):

  • If target exists and names an existing directory, the name of the corresponding destination path for each file in the file hierarchy shall be the concatenation of target, a single <slash> character if target did not end in a <slash>, and the pathname of the file relative to the directory containing source_file.

  • If target does not exist and two operands are specified, the name of the corresponding destination path for source_file shall be target; the name of the corresponding destination path for all other files in the file hierarchy shall be the concatenation of target, a character, and the pathname of the file relative to source_file.

therealchfkch commented 2 weeks ago

Ah okay. But i do not fully understand how to achieve the result of step 2 if the directory already exiats.

Also (a bit offtopic), i used the "old" coreutils (i guess GNU), and there it behaves like step 2 even if the target exists. Is their cp not POSIX compliant?

Sorry i am not very experienced in this.

tavianator commented 2 weeks ago

Also (a bit offtopic), i used the "old" coreutils (i guess GNU), and there it behaves like step 2 even if the target exists. Is their cp not POSIX compliant?

Are you sure? What version of cp are you using? Here's what I see:

tavianator@graphene $ mkdir source-dir target-dir
tavianator@graphene $ touch source-dir/file-1 source-dir/file-2
tavianator@graphene $ cp -r source-dir target-dir
tavianator@graphene $ tree target-dir
target-dir
└── source-dir
    ├── file-1
    └── file-2

2 directories, 2 files
tavianator@graphene $ cp --version
cp (GNU coreutils) 9.5
Copyright (C) 2024 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <https://gnu.org/licenses/gpl.html>.
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.

Written by Torbjörn Granlund, David MacKenzie, and Jim Meyering.

By the way this issue title ("target_dir is empty") and description ("target-dir does not exist yet") don't match.

therealchfkch commented 1 week ago

Ok so i have made a mistake and mixed something up. First of all, you are right, the descritption of the topic is slightly off, i am editing it.

The major mistake i made was that i mixed up my parameters. The correct version i tried (and succeded in GNU coreutils 9.5) was cp -r source-dir/. target-dir which then always copies only the content of source-dir to target-dir, not the directory source-dir itself.

How can i reach this with uutils/coreutils? Should they not be parameter-compatible? I could not find anything with cp --help, because i think it is again rather something POSIX-specific? Again, i am quite new to the technical side of this.

┌── ~
└─▶ ❯mkdir source-dir                                                                                                        

┌── ~
└─▶ ❯ touch source-dir/file1.md source-dir/file2.md                                                                           

┌── ~
└─▶ ❯ cp -r source-dir/. target-dir                                                                                           

┌── ~
└─▶ ❯ erd target-dir/                                                                                                         
0 B ┌─ file1.md
0 B ├─ file2.md
 target-dir

2 files

┌── ~
└─▶ ❯ cp -r source-dir/. target-dir                                                                                           

┌── ~
└─▶ ❯ erd target-dir/                                                                                                         
0 B ┌─ file1.md
0 B │  ┌─ file1.md
0 B │  ├─ file2.md
 ├─ source-dir
0 B ├─ file2.md
 target-dir

1 directory, 4 files

┌── ~
└─▶ ❯ rm -r target-dir/source-dir                                                                                             

┌── ~
└─▶ ❯ erd target-dir/                                                                                                         
0 B ┌─ file1.md
0 B ├─ file2.md
 target-dir

2 files
// The above part is in nushell (uutils), below is bash with GNU coreutils.
┌── ~
└─▶ ❯ bash                                                                                                                    
$ cp -r source-dir/. target-dir

$ erd target-dir/
0 B ┌─ file1.md
0 B ├─ file2.md
 target-dir

2 files
tavianator commented 1 week ago

Ah, with source-dir/., the GNU behaviour is right and uutils is wrong.