Why does "cp -r a/. b" behave as it does?

4

I thought that a and a/. is the same path. However, I've found that cp and rsync copy directory content instead of directory itself when /. is added to the source path. I also tried a/inner/..; this did the trick as well.

$ cp -r a b  # Copies dir a into dir b.
$ cp -r a/. b  # Copies files from dir a into dir b.
$ cp -r a/inner/.. b  # Also copies files from dir a into dir b.
$ cd a && cp -r . ../b  # One more way to copy inner files.

I understand that this is useful. But I'm a bit confused because it seems this feature breaks standards.

How does this work? Is this feature documented somewhere? Is this a feature of the OS, cp or bash?

raacer

Posted 2014-03-04T15:32:39.307

Reputation: 315

1cp a b doesn't copy a directory called "a" to a new directory called "b" in Ubuntu. It outputs cp: omitting directory 'a' and does nothing. You have to use the -r option (or something that implies it, such as -a) to copy directories. – Wyzard – 2014-03-04T20:54:49.783

Sorry, I've missed the -r option in examples, but I use it on real task. Fixed. Thanks for the note. – raacer – 2014-03-05T01:00:03.073

Answers

1

I understand that this is useful. But I'm a bit confused because it seems this feature breaks standards.

cp behavior when copying recursively from a/. to b is perfectly consistent with its "normal" behavior.

By default, cp's doesn't create parent directories. This can be modified with the parents switch:

   --parents
          use full source file name under DIRECTORY

But what does that mean?

It means that while the command

cp --parents -r some/path/to/source dest

will copy the source directory's contents into dest/some/path/to/source, the command

cp -r some/path/to/source dest

will copy the source directory's contents into dest/source.

Likewise, the command

cp -r some/path/to/source/. dest

will copy the source directory's contents into dest/., which is just dest.

I thought a and a/. is the same path.

a and a/. is the same path. But as an argument to cp, it is just a string.

Note that the commands

cp --parents -r some/path/to/source dest

and

cd some/path/to && cp --parents -r source dest

will behave differently as well.


What about cp -r a/inner/.. b? Taking into account your explanation, should not it copy files to b/.. (i.e. to the current directory)?

Well, yes. This is an exception.

At least in the GNU version of cp, there is a special case for for the .. basename.

From coreutils-8.22/src/cp.c:

          if (parents_option)
            {
              [removed]
            }
          else
            {
              char *arg_base;
              /* Append the last component of 'arg' to 'target_directory'.  */

              ASSIGN_BASENAME_STRDUPA (arg_base, arg);
              /* For 'cp -R source/.. dest', don't copy into 'dest/..'. */
              dst_name = (STREQ (arg_base, "..")
                          ? xstrdup (target_directory)
                          : file_name_concat (target_directory, arg_base,
                                              NULL));
            }

The motivation seems to be to avoid copying outside the destination folder, which – while perfectly consistent with cp's behavior in every other case – is a little counterintuitive and could have unpleasant consequences.

After all, I don't think anybody would expect the command

cp -r .. ~

to affect files outside his home directory...

Dennis

Posted 2014-03-04T15:32:39.307

Reputation: 42 934

Brilliant! But what about cp -r a/inner/.. b? Taking into account your explanation, should not it copy files to b/.. (i.e. to the current directory)? – raacer – 2014-03-05T01:14:11.557

@raacer: I've updated by answer. – Dennis – 2014-03-05T02:22:04.807

1

$ mkdir a b a/inner
$ touch a/a{1..3} b/b{1..3}
$ ls -R
.:
a  b

./a:
a1  a2  a3  inner

./a/inner:

./b:
b1  b2  b3
$ cp a b
cp: omitting directory ‘a’
$ cp a/. b
cp: omitting directory ‘a/.’
$ cp a/inner/.. b
cp: omitting directory ‘a/inner/..’
$ cd a && cp . ../b
cp: omitting directory ‘.’
$ cd ..
$ ls -R
.:
a  b

./a:
a1  a2  a3  inner

./a/inner:

./b:
b1  b2  b3

None of the things you say happen, actually happen. The four cp commands do nothing. Perhaps you have an alias for cp loaded. You can check this with alias cp.

Daniel Andersson

Posted 2014-03-04T15:32:39.307

Reputation: 20 465

1Better yet, which cp should tell the full story. This might be some convenience version/version with training wheels provided by OP's distribution. – vonbrand – 2014-03-04T20:34:54.417

1cp -r behaves like stated in the question. – Dennis – 2014-03-04T20:54:46.987

I just missed the -r option in my examples. Sorry for confusing. – raacer – 2014-03-05T01:01:46.473

@raacer: No problems. My guess was that you actually had alias cp='cp -r' set, but this explains it as well :-) . – Daniel Andersson – 2014-03-05T06:11:24.877

0

Apropos rsync(1), read its manual carefully. It uses some unusual conventions to mean "the contents of the directory" vs "the directory and its contents."

For cp(1) and other Linux commands, at the kernel level a and a/. and even a/inner/.. refer to the exact same directory. That doesn't mean the application can't parse them for itself and give them funky different meanings, but it is easier to just ship the string to the kernel, who will do the right thing.

vonbrand

Posted 2014-03-04T15:32:39.307

Reputation: 2 083

I thought so too about cp. Dennis provided some useful explanation how this actually works. – raacer – 2014-03-05T01:17:51.010

-1

If I recall correctly I believe the . in this instance works similar to an *. In essence, a wildcard character.

Bradley Forney

Posted 2014-03-04T15:32:39.307

Reputation: 651

It seems this is not true. * is a shell feature. Bash'es manual says nothing about . in the Pathname Expansion section. You can ensure that dot is not a part of standard path expansion by comparing the result of stat ./* and stat ./.. So while it gives us same result with cp, this is not really same, and probably works different way. Just the result is similar. – raacer – 2014-03-04T16:16:25.560

Is it possible that it's using . as a wildcard from some regex implementations? – Bradley Forney – 2014-03-04T19:40:58.053