views:

591

answers:

8

Suppose you want to make a bash script which supports no options but acts like cp, because the cp supplied by your system does not accept multiple sources.

The usage for the system's (hypothetical and broken) cp is:

cp source target    # target may be a directory

The usage for the script will be:

cp.sh source... target    # target must be a directory

Here's a starting point for the script:

#!/bin/bash
tgt="$1"
shift
for src in "$@"; do
    echo cp $src $tgt
done

When run with the arguments "a b c d" (note that d is the target), it outputs:

cp b a
cp c a
cp d a

The goal is to fix the script to output this instead, while keeping the code simple:

cp a d
cp b d
cp c d
+1  A: 

Why? The cp command already does that. Do a man cp and you will see.

If you still insist, here are two ways to get the last argument. Method 1: place command line in an array and extract the last element:

arg=("$@")
last_arg=${arg[(($# - 1))]}

The first line puts the command line arguments into the array arg. If your command line contains a b c d then arg[0] == 'a', ... argv[3] == 'd'.

The second line extract the last argument. The (($# - 1)) takes the number of arguments (4 in this case), subtract 1 from it (to get 3). That expression then becomes:

last_arg=${arg[3]}

which points to the last argument.


The second method is not very portable, it makes use of the BASH_ARGV variable, which is $@ but in reverse order. If your command line is a b c d then ${BASH_ARGV[0]} == 'd', ... ${BASH_ARGV[3]} == 'a':

last_arg=${BASH_ARGV[0]}

I hope this helps.

Hai Vu
Of course "cp" already supports this behavior. I have another program to run which doesn't, however.
John Zwinck
A: 

Use this to extract the last parameter:

eval tgt=\$$#

then just process the same way you are and when you hit $# just exit the loop

Here's the whjole script:

eval tgt=\$$#

for src in $@
do
 if [ "$src" == "$tgt" ];
 then
    exit
fi
echo cp $src $tgt
done

Pretty simple if you ask me!

ennuikiller
This method beats mine hand down. Hat off to you.
Hai Vu
The part about stopping when $# is hit requires tracking the loop index in addition, which I don't much care for. And `eval` isn't necessary, as some of the other answers show.
John Zwinck
you dont need an index, just compare the current value to tgt. What I meant by exiting when you hit $# means when you hit the value of $#
ennuikiller
A: 
#!/bin/bash
t=(x "$@")
i=1; while [ $i -lt $# ]; do
  echo cp ${t[$i]} ${t[$#]}
  i=$(($i + 1))
done
DigitalRoss
Do not use `$*`; it does not preserve spaces within arguments (it generates extra arguments). Use `"$@"` - almost always.
Jonathan Leffler
Quite true, I always use "$@" when chaining a command, not sure why I didn't do it here...
DigitalRoss
+1  A: 
#!/bin/bash
tgt="${@: -1}"    # get the last parameter
for src in "$@"; do
    if [[ $src != $tgt ]]; then
        echo cp "$src" "$tgt"
    fi
done
Dennis Williamson
Not bad, but I prefer the "unset" method for removing the last argument over the comparison used here (imagine what happens if the source actually does equal the target with this code).
John Zwinck
But you wouldn't want to hear this, would you? "cp: `d' and `d' are the same file"
Dennis Williamson
Or "cp: omitting directory `d'"
Dennis Williamson
+4  A: 

/test.bash source1 source2 target1

#!/bin/bash

target=${!#} 

if [ ! -d $target ] ; then
    echo "$target must be a directory " >&2
    exit 1;
fi

args=("$@")
unset args[${#args[@]}-1]

for src in "${args[@]}"; do
    echo cp $src $target
done

will output

cp source1 target1
cp source2 target1
Lance Rushing
I really like this one! I'm adding quotes around the uses of $target and $src (to support paths with spaces), but otherwise this is pretty much perfect.
John Zwinck
You should add an `if [ -f "$src" ]` (or `-a`) inside your `for` loop.
Dennis Williamson
+1  A: 

You can use array slicing to leave off the last of the arguments:

tgt="${!#}"
for src in "${@:1:$#-1}"; do
    cp "$src" "$tgt"
done
Gordon Davisson
A: 

You don't need any bash-specific features:

eval last=\${$#}
while [ $# -gt 1 ]; do
  echo "cp $1 $last"
  shift
done
Idelic
That relies on $300 working as the 300th argument (when there are 300 arguments). It doesn't work in all Bourne shells (e.g. Solaris 10 /bin/sh: `set -- a b c d e f g h i j k l; echo $10` generates 'a0' and not 'j'.
Jonathan Leffler
Ditto for Korn shell (again, on Solaris 10).
Jonathan Leffler
Of course, for $# > 9 you need to use ${N} (e.g. ${10}, ${300}). That works fine for me on Solaris 10, using ksh, bash, /bin/sh.
Idelic
A: 

You can do this directly without writing a script using xargs:

echo source1 source2 | tr "\n" "\0" | tr " " "\0" | 
   xargs --verbose -0 -I{} cp {} dest
Amro