#!/usr/bin/perl

use 5.006;
use strict;
use warnings;

my $VERSION = $File::Value::VERSION;

use Getopt::Long qw(:config bundling_override);
use Pod::Usage;
use File::Value ':all';

my %opt = (
	force		=> 0,
	help		=> 0,
	lshigh		=> 0,
	lslow		=> 0,
	man		=> 0,
	mknext		=> 0,
	mknextcopy	=> 0,
	version		=> 0,
	verbose		=> 0,
);

# main
{
	GetOptions(\%opt,
		'force|f',
		'help|h|?',
		'lshigh',
		'lslow',
		'man',
		'mknext',
		'mknextcopy',
		'version',
		'verbose|v',
	) or pod2usage(1);

	help(), exit(0)
		if $opt{help};
	pod2usage(-exitstatus => 0, -verbose => 2)
		if $opt{man};
	print "$VERSION\n" and exit(0)
		if $opt{version};
	pod2usage("$0: --mknext cannot be given with --lshigh or --lslow")
		if ($opt{lshigh} || $opt{lslow}) && $opt{mknext};
	help(), exit(1)
	#pod2usage("$0: no file or directory names given")
		unless @ARGV;

	foreach my $node (@ARGV) {

		my $as_dir = ($node =~ s,/+$,,);	# a dir if ends in '/'
		my $prnode = $node			# print-friendly name
			. ($as_dir ? '/' : '');		# has '/' added back

		my ($n, $msg);
		if ($opt{lshigh} or $opt{lslow}) {
			# we're only asked to report either or both of
			# the low version and the high version
			my @nodes;
			$node =~ s/\d+$//;
			if ($opt{lslow}) {
				($n, $msg) = list_low_version($node);
				$n == -1 and print(STDERR
					"$prnode: has no numbered versions\n"),
					exit 2
				;
				push @nodes, $msg;	# got it, $msg is node
			}
			if ($opt{lshigh}) {
				($n, $msg) = list_high_version($node);
				$n == -1 and print(STDERR
					"$prnode: has no numbered versions\n"),
					exit 2
				;
				push @nodes, $msg;	# got it, $msg is node
			}
			print join(" ",
				grep { (-d $_ and s,$,/,) or $_ } @nodes),
				"\n";
			next;
			# yyy support "missing" versions ??
		}
		elsif ($opt{mknext} or $opt{mknextcopy}) {
			($n, $msg) = snag_version($node, {
				as_dir => $as_dir,
				no_type_mismatch => ! $opt{force},
				mknextcopy => $opt{mknextcopy}});
			if ($n == -1) {
				print STDERR "$prnode: $msg\n";
				exit 2;
			}
			# got it:  $msg is the node name
			print "$msg", ($as_dir ? '/' : ''), "\n";
			next;
		}
		else {				# simple snag
			if ($opt{force} && -e $node) {
				-d $node and
					rmdir($node) || die "$node: $!"
				or
					unlink($node) || die "$node: $!"
				;
			}
			$msg = $as_dir ?
				snag_dir($node) : snag_file($node);
			if ($msg eq '') {
				print "$prnode\n";
				next;
			}
			if ($msg eq '1') {
				print "$prnode already exists";
				print ", but as a ", ($as_dir ?
						"file" : "directory")
					if ($as_dir != -d $node);
				print "\n";
				exit 1;
			}
			print STDERR "$prnode: $msg\n";
			exit 2;
		}
	}
	exit 0;
}

sub help {
	print << 'EOI';

snag - capture, without clobbering, a file or directory version 

Basic usage:
  snag <name>                if <name> doesn't exist, create as a file
  snag <name>/               if <name> doesn't exist, create as a directory

Version-aware usage:
  snag --lshigh <name>[/]    list highest existing version of <name>
  snag --lslow <name>[/]     list lowest existing version of <name>
  snag --mknext <name>[/]    create next highest unused version of <name>
  snag --mknextcopy <name>   like --mknext (files only) followed by a copy

A version number is just a terminal digit string in <name> (default "1").
That string's value becomes the next version number if no numbered versions
exist, and its length is the minimum width of the next version number
string.  See "snag --man" for full documentation.

EOI
	return 1;
}

__END__

=pod

=for roff
.nr PS 12p
.nr VS 14.4p

=head1 NAME

snag - command to reserve a previously unused file or directory version

=head1 SYNOPSIS

=over

=item B<snag> [B<-f>] I<name>[/] ...

=item B<snag> [B<-f>] [B<--lshigh | --lslow | --mknext> | --mknextcopy] I<name>[/] ...

=back

=head1 DESCRIPTION

The B<snag> command provides a robust way to "capture without clobbering"
a specified filesystem node I<name> or a version of that name.  The first
form of the command (not version-aware) creates a previously non-existing
filesystem node, I<name>.  If I<name> ends with a '/' character, the node
is taken to be a directory, otherwise it is taken to be a file.  It
outputs the created node name on success and exits with status 0.  Other
errors result in exit status 2 and a message on stderr.

Unlike the L<touch(1)> command, B<snag> is guaranteed to fail if the node
exists already (exit status 1).  Because it attempts to create the node
first and tests for existence afterwards, it is not susceptible to the
race condition that arises when these steps are reversed.  There is an
exception when B<-f> (B<--force>) is given, in which case an attempt will
be made first to remove a pre-existing node; caution should be exercised
as a race condition makes it possible to succeed in removing a node but
to fail in re-capturing it.

Versions are only relevant for the second form of the B<snag> command,
where "version" has no other meaning than a filesytem node name that may
end in a string of digits.  The node I<name> is considered a base for
numbered version names and any terminal digits in I<name> ("1" by default
if there are no terminal digits) are interpreted specially.  The length
of the terminal digit string determines the minimum width of the version
number, zero-padded if necessary, and the value of the digit string is
the first version number to use if no numbered versions exist.

This second form of the command provides a safe and efficient way to
capture an unused version.  If a race condition is detected, it will make
several attempts to capture a higher unused version before giving up.

If B<--lshigh> ("list high") is given, no node will be created, but the
highest existing numbered version will be returned, where candidate
versions will be any node name beginning with the base I<name> and ending
in any string of digits.  Similarly for B<--lslow> ("list low"), but for
the lowest existing numbered version.

If B<--mknext> is given, an attempt will be made to create the next highest
numbered version by adding one to the current highest version number.  If
a race condition is detected, several attempts will be made.  The next
highest version is determined by first finding the highest current
version number and adding 1 to it.  It is an error if the type (file or
directory) of the requested version is different from that of the current
high version unless B<--force> is given.

Where files are concerned, the B<--mknextcopy> option behaves like
B<--mknext> but with the new file receiving a copy of the unnumbered
file.  It is an error in this case if the specified node does not exist
already as an unnumbered file.

=head1 EXAMPLES

  $ snag myfile                # create an empty file
  myfile
  $ snag myfile                # fails if it exists
  myfile already exists
  $ cp ~/protostuff myfile     # get new content into myfile
  $ snag --mknextcopy myfile   # copy original as version 1
  myfile1
  $ vi myfile                  # make changes to your original
  $ snag --mknextcopy myfile   # save changes as version 2
  myfile2
  $ vi myfile                  # continue making changes

  $ snag v4/                   # create a numbered directory
  v4/
  $ snag --mknext v001/        # next is 5, but "001" pads to 005
  v005/
  $ snag --mknext v001/        # "001" is first version if none
  v006/
  $ snag --mknext v001/        # but is ignored if versions exist
  v007/
  $ rmdir v006                 # leaving a hole in the series
  $ snag -mknext v005/         # doesn't effect next highest
  v008/
  $ snag v999/                 # leave a big gap and show that
  v999/
  $ snag -mknext v005/         # "005" is only a minimum width
  v1000/

=head1 OPTIONS

=over

=item B<-f>, B<--force>

Force the overwrite of an existing node or the creation of a next
version of a different type from that of the current highest version.

=item B<-h>, B<--help>

Print extended help documentation.

=item B<--lshigh>, B<--lslow>

Don't create a node, but print highest or lowest existing numbered
version for the given I<name>.

=item B<--man>

Print full documentation.

=item B<--mknext>

Attempt to create the next highest numbered version.

=item B<--mknextcopy>

Where files are concerned, behave like B<--mknext> but with the
unnumbered filename's contents being copied to the new file.  This can be
useful when maintaining a file's most current state in an unnumbered or
zero-numbered filename (e.g., "myfile" or "myfile0"), and with every
other version numbered chronologically.

=item B<-v>, B<--version>

Print the current version number and exit.

=back

=head1 SEE ALSO

touch(1)

=head1 AUTHOR

John Kunze I<jak at ucop dot edu>

=head1 COPYRIGHT

Copyright 2009-2010 UC Regents.  Open source BSD license.

=begin CPAN

=head1 README

=head1 SCRIPT CATEGORIES

=end CPAN

=cut