5

In a script, if I put #!/bin/bash at the top, this will work fine:

cp directory/{foo,bar} destination

However, with sh, this will give the error 'no such file or directory'. In sh, is there another way to accomplish the same end goal with one command?

8

As you've observed, not all shells support brace expansion.

Way 1: Two separate paths (i.e., writing the prefix twice)

If your goal is really just to do it in one command, a simple way is:

cp directory/file1 directory/file2 destination

The cp command supports multiple source arguments followed by a destination argument. In fact, that's what's happening with the command that works for you in Bash. Bash performs brace expansion, which converts the command you've been using into the one shown above. That is, when you run cp directory/{file1,file2} destination in Bash, the cp command never sees any curly braces.

A command like that is simple and easy to understand. I suggest considering it.

Way 2: A variable for the prefix

But if your goal is to avoid repeating yourself, then you might prefer not to do it that way, especially if directory is actually a long path. After all, if you have to edit the path later in the script, you'd have to make changes to both occurrences.

You can store directory (or whatever, presumably longer path that is) in a shell variable:

dir='directory'
cp "$dir/file1" "$dir/file2" destination

$dir is parameter expansion. The shell replaces it with the value held in the variable dir. When using parameter expansion, you should not omit the double quotes--except in the rare case that you want field splitting and globbing to be performed. (This applies to Bash as well.)

That's two commands, but if your actual goal is to avoid repeating yourself, that may be what you want. You could write

dir='directory'; cp "$dir/file1" "$dir/file2" destination

to make it "one command," but that's no shorter and it's a bit less readable.

(Note that you cannot simply remove the ; there. The result of doing would be a well-formed command, but it would not do what you want. It would run the cp command, passing an an environment variable called dir with the value directory into the cp command's environment. Since it is the shell and not the cp command, that expands dir, this would not work.)

Way 3: cdig to the prefix

If your script isn't doing anything else after performing this operation, or it doesn't matter what the current directory is for subsequent commands, then if destination is an absolute path, you can use:

cd directory
cp file1 file2 destination

But I don't recommend that for scripting unless your script performs subsequent actions that benefit from being run from that directory, or can be more tersely or simply written when they are run from that directory. Except when the directory a script is run from is meaningful to the purpose of the script, relying on it can make the script harder to reason about; furthermore, if the cd command succeeds but the cp command fails, the resulting error messages may be less than fully illuminating. Instead, I mention the technique of cding to the source directory because it is a useful pattern for interactive shell use.

(As Hannu pointed out, you can enclose any number of commands together inside ( ) to run them in a subshell, and changes to the shell environment, including the effect of cd, will not persist after the command has run. This is a widely useful technique, and it was a substantial omission that I had not mentioned it. Note, however, that this does not mitigate any problems relating to having a relative path for destination.)

Note that if the cd command fails, the other commands will still typically run. This could copy different files you don't want copied (if there are other files with the same names in the directory the script was running from) or producing confusing error messages. So you may prefer:

cd directory && cp file1 file2 destination

The && causes the second command to run only if the first command succeeded. Note that that still does not undo the effect of running the first command. After the whole thing runs, you'll still be in directory, unless you ran it in a subshell with ( ).

  • 1
    Thank you for the extensive answer, the goal is indeed to not repeat myself rather than explicitly doing it in 'one command'. – Arrrow Jan 27 at 15:55
  • 1
    Bash supports "subshells", dunno if that exists is sh -> to launch one, use parentheses as in ( cd directory && cp file1 file2 destination ) - the cd and copy won't affect the shell you launch it from. ref: ` man bash | less -ip "compound commands" ` . – Hannu Jan 27 at 18:12
  • @Hannu Yes, this is a good point, and all POSIX compatible shells -- including whatever shell provides sh, which in Ubuntu is dash -- do support the ( ) syntax for running commands in a subshell. – Eliah Kagan Jan 27 at 18:20
  • For this answer it doesn't really matter, but the files do not necessarily have such similar file names like {file1,file2}. could be {foo,bar} just as well. – Arrrow Jan 28 at 10:28
4

Edit 1: After the question was modified the first alternative is no longer valid and the second alternative is modified 'file1 file2' --> 'foo bar' and extended with extra examples.


It works for me in sh with

cp directory/file[12] destination

and it should work for you too.

A more advanced alternative is a for loop

for i in foo bar;do cp directory/"$i" destination;done

This is easy to extend for example to

for i in foo bar more files;do cp directory/"$i" destination;done

or

for i in dir1/foo* /dir2/bar*;do cp "$i" destination;done

or

for i in dir1/* /dir2/*;do cp "$i" destination;done

Edit 2: Instead of advanced constructs using cp it is worthwhile to learn and start using rsync, which is a tool with a lot of built-in features to copy locally or via a network and specifying what to copy.

See man rsync

  • Do these options work if the files don't have such similar names, or if it is one file and one directory with differing names? – Arrrow Jan 28 at 10:22
  • The first alternative needs similar names. The second 'more advanced alternative` can have any names (and any nunber of names even with wildcards) instesd of 'file1 file2' in the for cormmand. – sudodus Jan 28 at 10:25

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.