The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
#!/usr/local/bin/perl
#
# $Id: pcpmac,v 0.70 2005/08/09 15:47:00 dankogai Exp $
#

use strict;
use Getopt::Std;
use File::stat;
use File::Basename;

my %Opt;
getopts("fiprv", \%Opt);
$Opt{i} and delete $Opt{f}; # for safety

my $IAM = basename($0); 
my %Hardlink; # key = inode, val = name of first file copied

my $Dst = pop @ARGV; @ARGV or help();
my $Dstst = stat($Dst); # not lstat; symlink to directory allowed
unless (-d _){
    @ARGV == 1 or help();
    do_copy($ARGV[0], $Dst);
}else{
    $Dst =~ s,/+$,,o; 
    for my $src (@ARGV){
	$src =~ s,/+$,,o; 
	do_copy($src, $Dst . '/' . basename($src));
    }
}
exit;

use MacOSX::File::Copy;
use MacOSX::File::Info;

sub do_copy{
    my ($src, $dst) = @_;
    my $srcst = lstat($src) or warn "$src: Can't lstat!" and return;
    $srcst->ino == $Dstst->ino and $srcst->dev == $Dstst->dev
	and warn "$src and $dst are identical. skipped" and return;

    $Opt{v} and print STDERR "$src\n";

   if (my $dstst = lstat($dst) and -l _ or -f _){
	$Opt{i} and prompt($dst) or return;
	$Opt{f} and unlink $dst;	
    }

    $srcst = lstat($src);
    if    (-l _){ # just copy the linkage
	symlink(readlink($src), $dst) or warn "$src -> $dst : $!";
    }elsif(-f _){ 
	if ($srcst->nlink > 1){ # MacOSX has hard links!
	    if (my $link = $Hardlink{$srcst->ino}){
		link($link, $dst) or warn "$link -> $dst:$!";
	    }else{
		$Hardlink{$srcst->ino} = $dst;
	    }
	}
	copy($src, $dst) # simple file-to-file copy
	    or warn "$dst:$! ($MacOSX::File::OSErr)";
	# set attributes
	if ($Opt{p}){
	    chown $srcst->uid, $srcst->gid, $dst; 
	    chmod $srcst->mode & 07777, $dst or warn "$dst : $!";
	}else{
	    my $now = time();
	    utime $now, $now, $dst or warn "$dst: $!";
	}
    }elsif(-d _){ # tough part;
	unless ($Opt{r}){
	     warn "$IAM: $src is a directory. skipped";
	     return;
	 }
	mkdir $dst,0777 or die "$dst:$!";
	opendir my $d, $src or die "$src:$!";
	# see ._* is avoided
	my @f = grep !/^\.(?:\.?$|_)/o, readdir $d;
	closedir $d;
	my $finfo = getfinfo($src)
	    or die "$src:Error $MacOSX::File::OSErr";
	for my $f (@f){
	    my ($srcf, $dstf) = ("$src/$f", "$dst/$f");
	    # no cross-device traversal within source directory
	    # so you can casually go like 'pcpmac -R / /Volumes/backup'
	    lstat($srcf)->dev != $srcst->dev and next;
	    -e $dstf and die "$dstf: already exists!";
	    # else lets' do it recursively
	    do_copy($srcf, $dstf);
	}
	# copy finfo info after all traversal is done
	$finfo->set($dst) or warn "$dst : $MacOSX::File::OSErr";
	# set attributes
	if ($Opt{p}){
	    chown $srcst->uid, $srcst->gid, $dst; 
	    chmod $srcst->mode & 07777, $dst or warn "$dst : $!";
	}else{
	    # do nothing
	}
    }else{
	# do nothing for devices, sockets and fifos; 
	# devices are handled by devfs on Macs
    }
}

sub prompt{
    my $path = shift;
    $| = 1;
    print "Overwrite $path? [y/N]:";
    my $answer = <STDIN>; chomp $answer;
    return lc($answer) eq 'y';
}

sub help{
    # warn caller;
    print STDERR <<"EOT";
usage: $IAM [-r] [-f|-i] [-p][-v] src target
       $IAM [-r] [-f|-i] [-p][-v] src1 ... srcN directory
EOT
exit;
}
1;

__END__
=head1 NAME

pcpmac -- CpMac(1) or cp(1),  implemented as perl script

=head1 SYNOPSIS

 pcpmac [-r] [-f|-i] [-p][-v] source_file target_file
 pcpmac [-r] [-f|-i] [-p][-v] source_file ... target_directory

=head1 TIGER

As of Mac OS X v10.4 (Tiger), the ordinary L<cp(1)> does support resource fork.

=head1 DESCRIPTION

pcpmac, as its name implies, copies files with finder info and
resource fork.  

In the first synopsis form, pcpmac copies the contents of the
source_file to the target_file. In the second synopsis form, the contents
of each named source_file is copied to the destination target_directory.
The names of the files themselves are not changed.  If cpmac detects an 
attempt to copy a file to itself, the copy will fail.

The following options are available:

=item -r

If source_file designates a directory, cp copies the directory and
the entire subtree connected at that point.

=item -f

Foreach existing destination pathname, attempt to overwrite it. If
permissions do not allow copy to succeed, remove it and create a
new file, without prompting for confirmation. 

=item -i

Causes pcpmac to write a prompt to the standard error output before
copying a file that would overwrite an existing file.  If the response 
from the standard input begins with the character "y" or "Y", the
file copy is attempted.

For safety, this option cancels -f option.  This is the opposite of
BSD cp implementation.

=item -p

Causes cp to preserve in the copy as many of the modification time,
access time, file flags, file mode, user ID, and group ID as allowed 
by permissions.

Note that no symlinks are followed.  Instead the target will also
be symlink which points to the same path.  With this respect,  This
option corresponds to C<cp -PR> of BSD cp.

pcpmac also prevents from crossing devices when subtrees belong to
different volumes.  That way you can safely C<pcpmac /* /Volumes/backup>
without hanging yourself.  This is anologous to -x(dev) option of find
command but somehow nothing like that existed for cp so I made it.

pcpmac also goes one step forward when both of two arguments are same
I<directory>.  Compare C<cp -r dir1 dir1> and C<pcpmac dir1 dir1> where
dir1 are identical.  It is a kind of fun what cp does!

=item -v

Verbose mode.  Prints each copied item to standard output.

=head1 AUTHOR

Dan Kogai <dankogai@dan.co.jp>

=head1 SEE ALSO

L<cp(1)>

F</Developer/Tools/CpMac>

=head1 COPYRIGHT

Copyright 2002-2005 Dan Kogai <dankogai@dan.co.jp>

This library is free software; you can redistribute it
and/or modify it under the same terms as Perl itself.