#! /usr/bin/perl -w
#
# atool - A script for managing file archives of various types.
#
# Copyright (C) 2001  Oskar Liljeblad
#
#   This program is free software; you can redistribute it and/or modify
#   it under the terms of the GNU General Public License as published by
#   the Free Software Foundation; either version 2 of the License, or
#   (at your option) any later version.
#
#   This program is distributed in the hope that it will be useful,
#   but WITHOUT ANY WARRANTY; without even the implied warranty of
#   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#   GNU General Public License for more details.
#
#   You should have received a copy of the GNU General Public License
#   along with this program; if not, write to the Free Software
#   Foundation, Inc., 59 Temple Place, Suite 330, Boston,
#   MA  02111-1307  USA
#
# See the atool(1) manual page for usage details.
#
# This file uses tab stops with width 2.
#

use File::Basename;
use File::Spec;
use Getopt::Long;
use POSIX;
use locale;
use strict;

# Subroutine prototypes (needed for perl 5.6)
sub runcmds($$;@);                #
sub getmode();                    #
sub multiarchivecmd($$$$@);       #
sub singlearchivecmd($$$$@);      #
sub maketarcmd($$$$);             #
sub cmdexec($@);                  #
sub parsefmt($$);                 #
sub makeoutdir();                 #
sub explain($);                   #
sub extract(@);                   #
sub shquotemeta($);               #
sub tailslash($);                 #
sub de($);                        #
sub makespec(@);                  #
sub backticks(@);                 #
sub readconfig($$);               #
sub formatext($);                 #
sub stripext($);                  #
sub findformat($$);               #
sub unlink_directory($);          #
sub find_comparable_file($);      #
sub makeabsolute($);              #
sub quote($);                     #
sub shell_execute(@);             #
sub save_outdir($);               #
sub handle_empty_add(@);          #

# Configuration options and their built-in defaults
$::cfg_use_tar_j_option   = 1;                  # does tar support -j?
$::cfg_use_tar_z_option   = 1;                  # does tar support -z?
$::cfg_use_gzip_for_z     = 1;                  # use gzip to decompress .Z files?
$::cfg_use_find_cpio_print0 = 1;                # use -print0/-0 find/cpio options?
$::cfg_use_jar            = 0;                  # use jar or zip for .jar archives?
$::cfg_use_file           = 1;                  # use file(1) for unknown extensions?
$::cfg_path_pager         = 'pager';            # pager program
$::cfg_path_jar           = 'jar';              # jar program
$::cfg_path_tar           = 'tar';              # tar program
$::cfg_path_zip           = 'zip';              # zip program
$::cfg_path_unzip         = 'unzip';            # unzip program
$::cfg_path_gzip          = 'gzip';             # gzip program
$::cfg_path_bzip          = 'bzip';             # bzip program
$::cfg_path_bzip2         = 'bzip2';            # bzip2 program
$::cfg_path_compress      = 'compress';         # compress program
$::cfg_path_lzop          = 'lzop';             # lzop program
$::cfg_path_rar           = 'rar';              # rar program
$::cfg_path_unrar         = 'unrar';            # unrar program
$::cfg_path_lha           = 'lha';              # lha program
$::cfg_path_unace         = 'unace';            # unace program
$::cfg_path_unarj         = 'unarj';            # unarj program
$::cfg_path_rpm           = 'rpm';              # rpm program
$::cfg_path_rpm2cpio      = 'rpm2cpio';         # rpm2cpio program
$::cfg_path_cpio          = 'cpio';             # cpio program
$::cfg_path_find          = 'find';             # find program
$::cfg_path_xargs         = 'xargs';            # xargs program
$::cfg_path_cat           = 'cat';              # cat program
$::cfg_path_file          = 'file';             # file program
$::cfg_path_diff          = 'diff';             # diff program
$::cfg_args_diff          = '-ru';              # arguments to pass to diff program
$::cfg_tmpdir_name        = 'Unpack-%04d';      # extraction directory name
$::cfg_default_verbosity  = 1;                  # default verbosity level
$::cfg_show_extracted     = 1;                  # always show extracted file/directory
$::cfg_keep_compressed    = 1;                  # keep compressed file after pack/unpack
$::cfg_path_syscfg        = '/etc/atool.conf';  # system-wide configuration file
$::cfg_path_usercfg       = '.atoolrc';         # user configuration file

# Global variables
$::version = '0.25.0';
$::basename = quote(File::Basename::basename($0));
@::rmdirs = ();

# Parse arguments
Getopt::Long::config('bundling');
Getopt::Long::GetOptions(
	'l|list'         => \$::opt_cmd_list,
	'x|extract'      => \$::opt_cmd_extract,
	'X|extract-to=s' => \$::opt_cmd_extract_to,
	'a|add'          => \$::opt_cmd_add,
	'c|cat'          => \$::opt_cmd_cat,
	'd|diff'         => \$::opt_cmd_diff,
	'q|quiet'        => sub { $::opt_verbosity--; },
	'v|verbose'      => sub { $::opt_verbosity++; },
	'V|verbosity=i'  => \$::opt_verbosity,
	'config=s'       => \$::opt_config,
	'help'           => \$::opt_cmd_help,
	'version'        => \$::opt_cmd_version,
	'F|format=s'     => \$::opt_format,
	'f|force'        => \$::opt_force,
	'p|page'         => \$::opt_use_pager,
	'e|each'         => \$::opt_each,
	'E|explain'      => \$::opt_explain,
	'S|simulate'     => \$::opt_simulate,
	'save-outdir=s'  => \$::opt_save_outdir,
	'D|subdir'       => \$::opt_extract_subdir,
	'0|null'         => \$::opt_null,
) or exit 1;

# Display --version
if ($::opt_cmd_version) {
	print
"atool $::version\
Written by Oskar Liljeblad <oskar\@osk.mine.nu>.\
\
Copyright (C) 2001 Oskar Liljeblad.\
This is free software; see the source for copying conditions.  There is NO\
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n";
	exit;
}

# Display --help
if ($::opt_cmd_help) {
	print
"Usage: atool [OPTION]... ARCHIVE [FILE]...\
       atool -e [OPTION]... [ARCHIVE]...
Managing file archives of various types.\
\
Commands:\
  -l, --list              list files in archive (als)\
  -x, --extract           extract files from archive (aunpack)\
  -X, --extract-to=PATH   extract archive to specified directory\
  -a, --add               create archive (apack)\
  -c, --cat               extract file to standard out (acat)\
  -d, --diff              generate a diff between two archives (adiff)\
      --help              display this help and exit\
      --version           output version information and exit\
\
Options:\
  -e, --each              execute command above for each file specified
  -F, --format=EXT        override archive format (see below)\
  -D, --subdir            always create subdirectory when extracting\
  -f, --force             allow overwriting of local files\
  -q, --quiet             decrease verbosity level by one\
  -v, --verbose           increase verbosity level by one\
  -V, --verbosity=LEVEL   specify verbosity (0, 1 or 2)\
  -p, --page              send output through pager\
  -0, --null              filenames from standard in are null-byte separated\
  -E, --explain           explain what is being done by atool\
  -S, --simulate          simulation mode - no filesystem changes are made\
      --config=FILE       load configuration defaults from file\
\
Archive format (for --format) may be specified either as a\
file extension (\"tar.gz\") or as \"tar+gzip\".\
\
Report bugs to Oskar Liljeblad <oskar\@osk.mine.nu>.\
";
	exit;
}

# Read configuration files
if (defined $::opt_config) {
	readconfig($::opt_config, 0);
} else {
	readconfig($::cfg_path_syscfg, 1);
	if ($::cfg_path_usercfg !~ /^\//) {
		readconfig(File::Spec->catfile($ENV{HOME}, $::cfg_path_usercfg), 1);
	} else {
		readconfig($::cfg_path_usercfg, 1);
	}
}

# Verify option integrity
$::opt_verbosity += $::cfg_default_verbosity;
if ($::opt_explain && $::opt_simulate) {
	die "$::basename: --explain and --simulate options are mutually exclusive\n";	#OK
}

my $mode = getmode();

if (defined $::opt_save_outdir && $mode eq 'extract-to') {
	die "$::basename: --save-outdir cannot be used in extract-to mode\n";
}
if ($::opt_extract_subdir && $mode ne 'extract') {
	die "$::basename: --subdir can only be used in extract mode\n";
}

if ($mode eq 'diff') {
	die "$::basename: missing archive argument\n" if (@ARGV < 2);	#OK
	my $use_pager = $::opt_use_pager;
	$::opt_verbosity--;
	$::opt_use_pager = 0;

	my $outfile1 = makeoutdir() || exit 1;
	my $outfile2 = makeoutdir() || exit 1;
	$::opt_cmd_extract_to = $outfile1;
	exit 1 if (!runcmds('extract-to', $ARGV[0]));
	$::opt_cmd_extract_to = $outfile2;
	exit 1 if (!runcmds('extract-to', $ARGV[1]));

	my $match1 = find_comparable_file($outfile1);
	my $match2 = find_comparable_file($outfile2);

	my @cmd = ($::cfg_path_diff, split(/ /, $::cfg_args_diff), $match1, $match2);
	push @cmd, ['|'], get_pager_program() if $use_pager;
	my $allok = cmdexec(1, @cmd);

	foreach my $file ($outfile1,$outfile2) {
		if (-e $file && -d $file) {
		#if (-e $file) {
			#print "$::basename: remove `$file'? ";
			#select((select(STDOUT), $| = 1)[0]);
			#my $line = <STDIN>;
			#if (defined $line && $line =~ /^y/) {
				#if (-d $file) {
					unlink_directory($file);
				#} else {
					#unlink $file;
				#}
			#}
		}
	}

	exit ($allok ? 0 : 1);
} elsif ($::opt_each) {
	my $allok = 1;
	if ($mode eq 'cat') {
		die "$::basename: --each can not be used with cat or add command\n";	#OK
	}
	if ($mode eq 'add') {
		if (!defined $::opt_format) {
			die "$::basename: specify a format with -F when using --each in add mode\n";
		}
		my $format = findformat($::opt_format, 1);
		for (my $c = 0; $c < @ARGV; $c++) {
			my $archive = File::Spec->canonpath($ARGV[$c]) . formatext($format);
			warn quote($archive).":\n" if $::opt_verbosity > 1;
			runcmds($mode, $archive, $ARGV[$c]) or $allok = 0;
		}
	} else {
		for (my $c = 0; $c < @ARGV; $c++) {
			warn quote($ARGV[$c]).":\n" if $::opt_verbosity > 1;
			runcmds($mode, $ARGV[$c]) or $allok = 0;
		}
	}
	exit ($allok ? 0 : 1);
} else {
	die "$::basename: missing archive argument\n" if (@ARGV == 0);	#OK
	runcmds($mode, shift @ARGV, @ARGV) || exit 1;
}

# runcmds(mode, archive, args)
# Execute an atool command. This is where it all happens.
# If mode is 'extract', returns the directory (or only file)
# which was extracted.
sub runcmds($$;@) {
	my ($mode, $archive, @args) = @_;

	my $format;
	if (defined $::opt_format) {
		$format = findformat($::opt_format, 1);
	} else {
		$format = findformat($archive, 0);
	}
	return undef if !defined $format;

	my @cmd;
	my $outdir;
	if ($format eq 'tar+bzip2') {
		return undef if ($mode eq 'extract' && !defined ($outdir = makeoutdir()));
		if ($::cfg_use_tar_j_option) {
			push @cmd, maketarcmd($archive, $outdir, $mode, 'jf'), @args;
		} else {
			push @cmd, $::cfg_path_bzip2, '-cd', $archive, ['|'] if $mode ne 'add';
			push @cmd, maketarcmd(undef, $outdir, $mode, ''), @args;
			push @cmd, ['|'], $::cfg_path_bzip2, ['>'], $archive if $mode eq 'add';
		}
		@cmd = handle_empty_add(@cmd) if ($mode eq 'add' && @args == 0);
		return multiarchivecmd($archive, $outdir, $mode, 1, 0, \@args, @cmd);
	}
	elsif ($format eq 'tar+gzip') {
		return undef if ($mode eq 'extract' && !defined ($outdir = makeoutdir()));
		if ($::cfg_use_tar_z_option) {
			push @cmd, maketarcmd($archive, $outdir, $mode, 'zf'), @args;
		} else {
			push @cmd, $::cfg_path_gzip, '-cd', $archive, ['|'] if $mode ne 'add';
			push @cmd, maketarcmd(undef, $outdir, $mode, ''), @args;
			push @cmd, ['|'], $::cfg_path_gzip, ['>'], $archive if $mode eq 'add';
		}
		@cmd = handle_empty_add(@cmd) if ($mode eq 'add' && @args == 0);
		return multiarchivecmd($archive, $outdir, $mode, 1, 0, \@args, @cmd);
	}
	elsif ($format eq 'tar+bzip') {
		return undef if ($mode eq 'extract' && !defined ($outdir = makeoutdir()));
		push @cmd, $::cfg_path_bzip, '-cd', $archive, ['|']	if $mode ne 'add';
		push @cmd, maketarcmd(undef, $outdir, $mode, ''), @args;
		push @cmd, ['|'], $::cfg_path_bzip, ['>'], $archive if $mode eq 'add';
		@cmd = handle_empty_add(@cmd) if ($mode eq 'add' && @args == 0);
		return multiarchivecmd($archive, $outdir, $mode, 1, 0, \@args, @cmd);
	}
	elsif ($format eq 'tar+compress') {
		return undef if ($mode eq 'extract' && !defined ($outdir = makeoutdir()));
		if ($::cfg_use_gzip_for_z) {
			push @cmd, $::cfg_path_gzip, '-cd', $archive, ['|']	if $mode ne 'add';
		} else {
			push @cmd, $::cfg_path_compress, '-cd', $archive, ['|']	if $mode ne 'add';
		}
		push @cmd, maketarcmd(undef, $outdir, $mode, ''), @args;
		push @cmd, ['|'], $::cfg_path_compress, ['>'], $archive if $mode eq 'add';
		@cmd = handle_empty_add(@cmd) if ($mode eq 'add' && @args == 0);
		return multiarchivecmd($archive, $outdir, $mode, 1, 0, \@args, @cmd);
	}
	elsif ($format eq 'tar+lzop') {
		return undef if ($mode eq 'extract' && !defined ($outdir = makeoutdir()));
		push @cmd, $::cfg_path_lzop, '-Ucd', $archive, ['|']	if $mode ne 'add';
		push @cmd, maketarcmd(undef, $outdir, $mode, ''), @args;
		push @cmd, ['|'], $::cfg_path_lzop, ['>'], $archive if $mode eq 'add';
		@cmd = handle_empty_add(@cmd) if ($mode eq 'add' && @args == 0);
		return multiarchivecmd($archive, $outdir, $mode, 1, 0, \@args, @cmd);
	}
	elsif ($format eq 'tar') {
		return undef if ($mode eq 'extract' && !defined ($outdir = makeoutdir()));
		push @cmd, maketarcmd($archive, $outdir, $mode, 'f'), @args;
		@cmd = handle_empty_add(@cmd) if ($mode eq 'add' && @args == 0);
		return multiarchivecmd($archive, $outdir, $mode, 1, 0, \@args, @cmd);
	}
	elsif ($format eq 'jar' && $::cfg_use_jar) {
		return undef if ($mode eq 'extract' && !defined ($outdir = makeoutdir()));
		my $opts = '';
		if ($mode eq 'add') {
			warn "$::basename: ".quote($archive).": $mode command not supported for $format archives\n";
			return undef;
		}
		$opts .= 'v' if $::opt_verbosity >= 1;
		push @cmd, $::cfg_path_jar;
		push @cmd, "x$opts", '-C', $outdir if $mode eq 'extract';
		push @cmd, "x$opts", '-C', $::opt_cmd_extract_to if $mode eq 'extract-to';
		push @cmd, "t$opts" if $mode eq 'list';
		push @cmd, "c$opts" if $mode eq 'add';
		push @cmd, $archive, @args;
		@cmd = handle_empty_add(@cmd) if ($mode eq 'add' && @args == 0);
		return multiarchivecmd($archive, $outdir, $mode, 1, 0, \@args, @cmd);
	}
	elsif ($format eq 'jar' || $format eq 'zip') {
		return undef if ($mode eq 'extract' && !defined ($outdir = makeoutdir()));
		if ($mode eq 'add') {
			push @cmd, $::cfg_path_zip, '-r';
		} else {
			push @cmd, $::cfg_path_unzip;
			push @cmd, '-p' if $mode eq 'cat';
			push @cmd, '-l' if $mode eq 'list';
			push @cmd, '-d', $outdir if $mode eq 'extract';
			push @cmd, '-d', $::opt_cmd_extract_to if $mode eq 'extract-to';
		}
		push @cmd, '-v' if $::opt_verbosity > 1;
		push @cmd, '-qq' if $::opt_verbosity < 0;
		push @cmd, '-q' if $::opt_verbosity == 0;
		push @cmd, $archive, @args;
		@cmd = handle_empty_add(@cmd) if ($mode eq 'add' && @args == 0);
		return multiarchivecmd($archive, $outdir, $mode, 0, 0, \@args, @cmd);
	}
	elsif ($format eq 'rar') {
		return undef if ($mode eq 'extract' && !defined ($outdir = makeoutdir()));
		push @cmd, ($mode eq 'add' ? $::cfg_path_rar : $::cfg_path_unrar);
		push @cmd, 'a' if $mode eq 'add';
		push @cmd, 'vt' if $mode eq 'list' && $::opt_verbosity >= 3;
		push @cmd, 'v' if $mode eq 'list' && $::opt_verbosity == 2;
		push @cmd, 'l' if $mode eq 'list' && $::opt_verbosity <= 1;
		push @cmd, 'x' if ($mode eq 'extract' || $mode eq 'extract-to');
		push @cmd, '-ierr', 'p' if $mode eq 'cat';
		push @cmd, '-r' if ($mode eq 'add');
		push @cmd, $archive, @args;
		push @cmd, tailslash($outdir) if $mode eq 'extract';
		push @cmd, tailslash($::opt_cmd_extract_to) if $mode eq 'extract-to';
		@cmd = handle_empty_add(@cmd) if ($mode eq 'add' && @args == 0);
		return multiarchivecmd($archive, $outdir, $mode, 0, 0, \@args, @cmd);
	}
	elsif ($format eq 'lha') {
		return undef if ($mode eq 'extract' && !defined ($outdir = makeoutdir()));
		push @cmd, $::cfg_path_lha;
		push @cmd, 'a' if $mode eq 'add';
		push @cmd, 'v' if $mode eq 'list' && $::opt_verbosity >= 3;
		push @cmd, 'l' if $mode eq 'list' && $::opt_verbosity == 2;
		push @cmd, 'lq' if $mode eq 'list' && $::opt_verbosity <= 1;
		push @cmd, 'x' if ($mode eq 'extract' || $mode eq 'extract-to');
		push @cmd, 'p' if $mode eq 'cat';
		push @cmd, $archive, @args;
		push @cmd, tailslash($outdir) if $mode eq 'extract';
		push @cmd, tailslash($::opt_cmd_extract_to) if $mode eq 'extract-to';
		@cmd = handle_empty_add(@cmd) if ($mode eq 'add' && @args == 0);
		return multiarchivecmd($archive, $outdir, $mode, 0, 0, \@args, @cmd);
	}
	elsif ($format eq 'ace') {
		return undef if ($mode eq 'extract' && !defined ($outdir = makeoutdir()));
		push @cmd, $::cfg_path_unace;
		if ($mode eq 'add' || $mode eq 'cat') {
			warn "$::basename: ".quote($archive).": $mode command not supported for $format archives\n";
			return undef;
		}
		push @cmd, 'v', '-c' if $mode eq 'list' && $::opt_verbosity >= 3;
		push @cmd, 'v' if $mode eq 'list' && $::opt_verbosity == 2;
		push @cmd, 'l' if $mode eq 'list' && $::opt_verbosity <= 1;
		push @cmd, 'x' if ($mode eq 'extract' || $mode eq 'extract-to');
		push @cmd, $archive, @args;
		push @cmd, tailslash($outdir) if $mode eq 'extract';
		push @cmd, tailslash($::opt_cmd_extract_to) if $mode eq 'extract-to';
		@cmd = handle_empty_add(@cmd) if ($mode eq 'add' && @args == 0);
		return multiarchivecmd($archive, $outdir, $mode, 0, 0, \@args, @cmd);
	}
	elsif ($format eq 'arj') {
		return undef if ($mode eq 'extract' && !defined ($outdir = makeoutdir()));
		push @cmd, $::cfg_path_unarj;
		if ($mode eq 'add' || $mode eq 'cat') {
			warn "$::basename: ".quote($archive).": $mode command not supported for $format archives\n";
			return undef;
		}
		# XXX: cat mode might work for arj archives, but it extract to stderr!
		push @cmd, 'v' if $mode eq 'list' && $::opt_verbosity == 2;
		push @cmd, 'l' if $mode eq 'list' && $::opt_verbosity <= 1;
		push @cmd, 'x' if ($mode eq 'extract' || $mode eq 'extract-to');
		push @cmd, $archive if ($mode ne 'extract' && $mode ne 'extract-to');;
		# we call makeabsolute here because needcwd=1 to the multiarchivecmd call
		push @cmd, makeabsolute($archive) if ($mode eq 'extract' || $mode eq 'extract-to');
		push @cmd, @args;
		@cmd = handle_empty_add(@cmd) if ($mode eq 'add' && @args == 0);
		return multiarchivecmd($archive, $outdir, $mode, 0, 1, \@args, @cmd);
	}
	elsif ($format eq 'rpm') {
		return undef if ($mode eq 'extract' && !defined ($outdir = makeoutdir()));
		if ($mode eq 'list') {
			push @cmd, $::cfg_path_rpm;
			push @cmd, '-qlp' if $mode eq 'list';
			push @cmd, '-v' if $::opt_verbosity >= 1;
			push @cmd, $archive, @args;
			return multiarchivecmd($archive, $outdir, $mode, 0, 0, \@args, @cmd);
		}
		elsif ($mode eq 'extract' || $mode eq 'extract-to') {
			push @cmd, $::cfg_path_rpm2cpio;
			push @cmd, makeabsolute($archive);
			push @cmd, ['|'];
			push @cmd, $::cfg_path_cpio, '-imd', '--quiet', @args;
			return multiarchivecmd($archive, $outdir, $mode, 0, 1, \@args, @cmd);
		}
		else { # add and cat
			# FIXME: I guess cat could work too, but it would require that we
  		# extracted to a temporary dir, read and printed it, then removed it.
			warn "$::basename: ".quote($archive).": $mode command not supported for $format archives\n";
			return undef;
		}
	}
	elsif ($format eq 'cpio') {
		return undef if ($mode eq 'extract' && !defined ($outdir = makeoutdir()));
		if ($mode eq 'list') {
			push @cmd, $::cfg_path_cat, $archive, ['|'];
			push @cmd, $::cfg_path_cpio, '-t';
			push @cmd, '-v' if $::opt_verbosity >= 1;
			return multiarchivecmd($archive, $outdir, $mode, 0, 0, \@args, @cmd);
		}
		elsif ($mode eq 'extract' || $mode eq 'extract-to') {
			push @cmd, $::cfg_path_cat, makeabsolute($archive), ['|'];
			push @cmd, $::cfg_path_cpio, '-i';
			push @cmd, '-v' if $::opt_verbosity >= 1;
			return multiarchivecmd($archive, $outdir, $mode, 0, 1, \@args, @cmd);
		}
		elsif ($mode eq 'add') {
			if (@args == 0) {
				push @cmd, $::cfg_path_cpio;
				push @cmd, '-0' if $::opt_null;
				push @cmd, '-o';
				push @cmd, '-v' if $::opt_verbosity >= 1;
				push @cmd, ['>'], $archive;
			} else {
				push @cmd, $::cfg_path_find, @args;
				push @cmd, '-print0' if $::cfg_use_find_cpio_print0;
				push @cmd, ['|'], $::cfg_path_cpio;
				push @cmd, '-0' if $::cfg_use_find_cpio_print0;
				push @cmd, '-o';
				push @cmd, '-v' if $::opt_verbosity >= 1;
				push @cmd, ['>'], $archive;
			}
			return multiarchivecmd($archive, $outdir, $mode, 1, 1, \@args, @cmd);
		}
		else { # cat
			warn "$::basename: ".quote($archive).": $mode command not supported for $format archives\n";
			return undef;
		}
	}
	elsif ($format eq 'bzip2') {
		return singlearchivecmd($archive, $::cfg_path_bzip2, $format, $mode, @args);
	}
	elsif ($format eq 'bzip') {
		return singlearchivecmd($archive, $::cfg_path_bzip, $format, $mode, @args);
	}
	elsif ($format eq 'gzip') {
		return singlearchivecmd($archive, $::cfg_path_gzip, $format, $mode, @args);
	}
	elsif ($format eq 'compress') {
		if ($::cfg_use_gzip_for_z && $mode ne 'add') {
			return singlearchivecmd($archive, $::cfg_path_gzip, $format, $mode, @args);
		} else {
			return singlearchivecmd($archive, $::cfg_path_compress, $format, $mode, @args);
		}
	}
	elsif ($format eq 'lzop') {
		return singlearchivecmd($archive, $::cfg_path_lzop, $format, $mode, '-U', @args);
	}

	return undef;
}

# de(value):
# Return 1 if value defined and is non-zero, 0 otherwise.
sub de($) {
	my ($value) = @_;
	return defined $value && $value ? 1 : 0;
}

# getmode()
# Identify the execution mode, and return it.
# Possible modes are 'cat', 'extract', 'list', 'add' or 'extract-to'.
sub getmode() {
	my $mode;
	if (de($::opt_cmd_list) + de($::opt_cmd_cat)
			+ de($::opt_cmd_extract) + de($::opt_cmd_add) 
			+ de($::opt_cmd_extract_to) > 1) {
		die "$::basename: only one command may be specified\n"; #OK
	}
	$mode = 'cat'           if ($::basename eq 'acat');
	$mode = 'extract'       if ($::basename eq 'aunpack');
	$mode = 'list'          if ($::basename eq 'als');
	$mode = 'add'           if ($::basename eq 'apack');
	$mode = 'diff'          if ($::basename eq 'adiff');
	$mode = 'add'		        if ($::opt_cmd_add);
	$mode = 'cat'		        if ($::opt_cmd_cat);
	$mode = 'list'			    if ($::opt_cmd_list);
	$mode = 'extract'	      if ($::opt_cmd_extract);
	$mode = 'extract-to'    if ($::opt_cmd_extract_to);
	$mode = 'diff'          if ($::opt_cmd_diff);
	if (!defined $mode) {
		die "$::basename: don't know what to do - no command specified\n"; #OK
	}
	return $mode;
}

# singlearchivecmd(archive, command, format, mode, args)
# Execute a command for single-file archives.
# The command parameter specifies what command to execute.
# If mode is 'extract-to', returns the directory (or only file)
# which was extracted.
sub singlearchivecmd($$$$@) {
	my ($archive, $cmd, $format, $mode, @args) = @_;

	if ($mode eq 'add' && @ARGV > 1) {
		warn "$::basename: cannot add more than one file with this format\n";
		return;
	}
	if ($mode eq 'add' && !$::opt_force && (-e $archive || -l $archive)) {
		warn "$::basename: ".quote($archive).": refusing to overwrite existing file\n";
		return;
	}

	my $outfile;
	if ($mode eq 'extract-to') {
		$outfile = $::opt_cmd_extract_to;		# undef if not extract-to
		if (-d $outfile) {
			my $base = File::Basename::basename($archive);
			$outfile = File::Spec->catfile($outfile, stripext($base));
		}
	} elsif ($mode eq 'extract') {
		$outfile = stripext($archive);
	}

	if ($mode eq 'list') {
		warn "$::basename: ".quote($archive).": $mode command not supported for $format archives\n";
		return;
	}
	if ($mode eq 'add' && !$::cfg_keep_compressed && stripext($archive) ne $args[0]) {
		warn "$::basename: ".quote($archive).": cannot create a $format archive with this name (use -X)\n";
		return;
	}

	my @cmd;
	push @cmd, $cmd;
	push @cmd, '-v'                       if $::opt_verbosity > 1;
	if ($::cfg_keep_compressed) {
		if ($mode eq 'add') {
			push @cmd, '-c', @args, ['>'], $archive;
		} elsif ($mode eq 'cat') {
			push @cmd, '-c', '-d', $archive, @args;
		} elsif ($mode eq 'extract' || $mode eq 'extract-to') {
			push @cmd, '-c', '-d', $archive, @args, ['>'], $outfile;
		}
	} else {
		if ($mode eq 'add') {
			push @cmd, @args;
		} elsif ($mode eq 'cat') {
			push @cmd, '-c', '-d', $archive, @args;
		} elsif ($mode eq 'extract') {
			push @cmd, '-d', $archive, @args;
		} elsif ($mode eq 'extract-to') {
			push @cmd, '-c', '-d', $archive, @args, ['>'], $outfile;
		}
	}
	push @cmd, ['|'], get_pager_program() if $::opt_use_pager;
	cmdexec(0, @cmd) || return;

	return $outfile;
}

# maketarcmd(opts):
# Create (partial) command line arguments for a tar command.
# The parameter opts specifies additional arguments to add.
sub maketarcmd($$$$) {
	my ($archive, $outdir, $mode, $opts) = @_;
	$opts .= 'v' if $::opt_verbosity >= 1;
	my @cmd = ($::cfg_path_tar);
	push @cmd, "xO$opts" if $mode eq 'cat';
	push @cmd, "x$opts" if ($mode eq 'extract' || $mode eq 'extract-to');
	push @cmd, "t$opts" if $mode eq 'list';
	push @cmd, "c$opts" if $mode eq 'add';
	push @cmd, $archive if defined $archive;
	push @cmd, '-C', $outdir if $mode eq 'extract';
	push @cmd, '-C', $::opt_cmd_extract_to if $mode eq 'extract-to';
	return @cmd;
}

# cmdexec(ignore_return, cmdspec)
# Execute a command specification.
# The cmdspec parameter is a list of string arguments building
# the command line. If there's a list reference instead of a
# string, it is a shell meta character/string which shouldn't
# be quoted.
sub cmdexec($@) {
	my ($ignret, @cmd) = @_;
	
	if ($::opt_explain || $::opt_simulate) {
		my $spec = join(' ', map { ref $_ ? @{$_} : shquotemeta $_ } @cmd);
		explain quote($spec)."\n";
		return 1 if ($::opt_simulate);
	}

	my $cmds = makespec(@cmd);
	if (!shell_execute(@cmd)) {
		warn "$::basename: cannot execute `".quote($cmds)."': $::errmsg\n";
		return 0;
	}

	if ($? & 0xFF != 0) {
		warn "$::basename: $cmds: abnormal exit (exit code $?)\n";
		return 0;
	}
	
	if (!$ignret && $? >> 8 != 0) {
		warn "$::basename: $cmds: non-zero return-code\n";
		return 0;
	}

	return 1;
}

# makespec(@)
# Make a command specification when printing errors.
sub makespec(@) {
	my (@cmd) = @_;
	my $spec = $cmd[0].' ..';
	my $lastref = 0;
	foreach (@cmd, '') {
		if ($lastref) {
			$spec .= " | $_ ..";
			$lastref = 0;
		}
		$lastref = 1 if (ref);
	}
	return $spec;
}

# makeoutdir()
# Make a temporary (unique) output directory for extraction command.
sub makeoutdir() {
	my $dir = '';
	do {
		$dir = sprintf $::cfg_tmpdir_name, int rand 10000;
	} while (-e $dir);

	if (!$::opt_simulate) {
		if (!mkdir($dir, 0700)) {
			warn "$::basename: cannot create directory `".quote($dir)."': $!\n";
			return undef;
		}
	}
	push @::rmdirs, $dir;
	return $dir;
}

# explain($)
# Print on screen if $::opt_explain is true.
sub explain($) {
	my ($msg) = @_;
	print STDERR $msg if ($::opt_explain || $::opt_simulate);
}

# tailslash($)
# If specified filename does not end with a slash,
# add one and return the new filename.
sub tailslash($) {
	my ($file) = @_;
	return ($file =~ /\/$/ ? $file : "$file/");
}

# shquotemeta($)
# A more sophisticated quotemeta for bourne shells.
# (This should be used for printing only.)
sub shquotemeta($) {
	my ($str) = @_;
	$str =~ s/([^A-Za-z0-9_.+,\/:=@%^-])/\\$1/g;
	return $str;
}

# multiarchivecmd(archive, outdir, mode, create, argref, cmdspec)
# Execute a command for multi-file archives.
# The `create' argument controls whether the archive
# will be created (1) or just added to (0) if mode is "add".
# If mode is 'extract', returns the directory (or only file)
# which was extracted.
# If needcwd is true, the outdir must be changed to.
sub multiarchivecmd($$$$@) {
	my ($archive, $outdir, $mode, $create, $needcwd, $argref, @cmd) = @_;
	my @args = @{$argref};

	if ($mode eq 'cat' && @args == 0) {
		die "$::basename: missing file argument\n";	#OK
	}

	if ($mode eq 'add' && $create && !$::opt_force && (-e $archive || -l $archive)) {
		warn "$::basename: ".quote($archive).": refusing to overwrite existing file\n";
		return undef;
	}

	push @cmd, ['|'], get_pager_program() if $::opt_use_pager;

	my $olddir = undef;
	if ($needcwd && !$::opt_simulate) {
		$olddir = getcwd();
		if ($mode eq 'extract' && !chdir($outdir)) {
			warn "$::basename: ".quote($outdir).": $!\n";
			return undef;
		}
		if ($mode eq 'extract-to' && !chdir($::opt_cmd_extract_to)) {
			warn "$::basename: $::opt_cmd_extract_to: $!\n";
			return undef;
		}
	}

	if ($mode ne 'extract') {
		cmdexec(0, @cmd) || return undef;
		if (defined $olddir && !chdir($olddir)) {
			warn "$::basename: ".quote($olddir).": $!\n";
			return undef;
		}
		# XXX: can't save outdir with extract-to.
		return 1;
	}

	if (!cmdexec(0, @cmd)) {
		warn "$::basename: ".quote($olddir).": $!\n" if (defined $olddir && !chdir($olddir));
	 	return undef;
	}

	return undef if $::opt_simulate;

	if (defined $olddir && !chdir($olddir)) {
		warn "$::basename: ".quote($olddir).": $!\n";
		return undef;
	}

	if (!opendir(DIR, $outdir)) {
		warn "$::basename: ".quote($outdir).": cannot list: $!\n";
		return undef;
	}
	my @files = grep !/^\.\.?$/, readdir DIR;
	closedir DIR;

	my $archivebase = File::Basename::basename($archive);
	my $reason;
	my $adddir = 0;
	if (@files == 0) {
		warn quote($archivebase).": archive is empty\n";
		rmdir $outdir;
		return undef;
	} elsif ($::opt_extract_subdir) {
		$reason = 'forced';
	} elsif (@files == 1) {
		my $fromfile = File::Spec->catfile($outdir, $files[0]);
		if ($::opt_force || (!-l $files[0] && !-e $files[0])) {

			# If the file is a directory, it can only be moved if writable
			my $oldmode = undef;
			if (!-l $fromfile && -d $fromfile) {
				my @statinfo = stat($fromfile);
				if (!@statinfo) {
					warn quote($fromfile).": cannot stat: $!\n";
					return undef;
				}
				$oldmode = $statinfo[2];
				if (!chmod(0700, $fromfile)) {
					warn quote($fromfile).": cannot change mode: $!\n";
					return undef;
				}
			}

			if (!rename $fromfile, $files[0]) {
				warn quote($fromfile).": cannot rename: $!\n";
				return undef;
			}
			rmdir $outdir;

			# If we changed mode previously, restore that mode now
			if (defined $oldmode) {
				if (!chmod($oldmode, $files[0])) {
					warn quote($files[0]).": cannot change mode: $!\n";
					return undef;
				}
			}

			if ($::cfg_show_extracted) {
				my $file = ($files[0] =~ /\// ? dirname($files[0]) : $files[0]);
				warn quote($archivebase).": extracted to `".quote($file)."'\n" ;
			}

			save_outdir($files[0]);
			return $files[0];
		}
		$reason = 'local file exists';
		$adddir = 1 if (!-l $files[0] && -d $files[0]);
	} else {
		$reason = 'multiple files in root';
	}

	my $localoutdir = stripext($archivebase);
	if (!-e $localoutdir) {
		if (!rename $outdir, $localoutdir) {
			warn quote($outdir).": cannot rename: $!\n";
			return undef;
		}
		$outdir = $localoutdir;
	}

	warn quote($archivebase).": extracted to `".quote($outdir)."' ($reason)\n";
	save_outdir($adddir ? File::Spec->catfile($outdir, $files[0]) : $outdir);
	return $outdir;
}

# stripext(file)
# Strip extension from the specified file.
sub stripext($) {
	my ($file) = @_;
	return $file if ($file =~ s/(\.tar\.bz2|\.tbz2)$//);
	return $file if ($file =~ s/(\.tar\.bz|\.tbz)$//);
	return $file if ($file =~ s/(\.tar\.gz|\.tgz)$//);
	return $file if ($file =~ s/(\.tar\.Z|\.tZ)$//);
	return $file if ($file =~ s/\.tar$//);
	return $file if ($file =~ s/\.bz2$//);
	return $file if ($file =~ s/\.bz$//);
	return $file if ($file =~ s/\.gz$//);
	return $file if ($file =~ s/\.zip$//);
	return $file if ($file =~ s/\.jar$//);
	return $file if ($file =~ s/\.war$//);
	return $file if ($file =~ s/\.Z$//);
	return $file if ($file =~ s/\.rar$//);
	return $file if ($file =~ s/\.(lha|lzh)$//);
	return $file if ($file =~ s/\.ace$//);
	return $file if ($file =~ s/\.arj$//);
	return $file if ($file =~ s/\.rpm$//);
	return $file if ($file =~ s/\.cpio$//);
	return $file;
}

# formatext(format)
# Return the usual extension for the specified file format
sub formatext($) {
	my ($format) = @_;
	return '.tar.lzo'  if $format eq 'tar+lzop';
	return '.tar.bz2'  if $format eq 'tar+bzip2';
	return '.tar.bz'   if $format eq 'tar+bzip';
	return '.tar.gz'   if $format eq 'tar+gzip';
	return '.tar.Z'    if $format eq 'tar+compress';
	return '.tar'      if $format eq 'tar';
	return '.bz2'      if $format eq 'bzip2';
	return '.bz'       if $format eq 'bzip';
	return '.gz'       if $format eq 'gzip';
	return '.lzo'      if $format eq 'lzop';
	return '.zip'      if $format eq 'zip';
	return '.jar'      if $format eq 'jar';
	return '.Z'        if $format eq 'compress';
	return '.rar'      if $format eq 'rar';
	return '.ace'      if $format eq 'ace';
	return '.arj'      if $format eq 'arj';
	return '.lha'      if $format eq 'lha';
	return '.rpm'      if $format eq 'rpm';
	return '.cpio'     if $format eq 'cpio';
	die "$::basename: ".quote($format).": don't know file extension for format\n";
}

# findformat(spec, manual)
# Figure out format from specified file/string.
# If manual is 0, spec is a filename, otherwise
# it is a format description string.
sub findformat($$) {
	my ($file, $manual) = @_;
	my $spec = lc $file;
	if ($manual) {
		$spec =~ tr/+/./;
		$spec =~ s/^\.*/\./;
		$spec =~ s/lzop/lzo/;
		$spec =~ s/bzip2/bz2/;
		$spec =~ s/bzip/bz/;
		$spec =~ s/gzip/gz/;
		$spec =~ s/compress/Z/;
	}
	return 'tar+bzip2'    if ($spec =~ /(\.tar\.bz2|\.tbz2)$/);
	return 'tar+bzip'     if ($spec =~ /(\.tar\.bz|\.tbz)$/);
	return 'tar+gzip'     if ($spec =~ /(\.tar\.gz|\.tgz)$/);
	return 'tar+compress' if ($spec =~ /(\.tar\.[zZ]|\.t[zZ])$/);
	return 'tar+lzop'     if ($spec =~ /(\.tar\.lzo|\.tzo)$/);
	return 'tar'          if ($spec =~ /\.tar$/);
	return 'bzip2'        if ($spec =~ /\.bz2$/);
	return 'bzip'         if ($spec =~ /\.bz$/);
	return 'gzip'         if ($spec =~ /\.gz$/);
	return 'zip'          if ($spec =~ /\.zip$/);
	return 'jar'          if ($spec =~ /\.(jar|war)$/);
	return 'compress'     if ($spec =~ /\.[zZ]$/);
	return 'rar'          if ($spec =~ /\.rar$/);
	return 'lha'          if ($spec =~ /\.(lha|lzh)$/);
	return 'ace'          if ($spec =~ /\.ace$/);
	return 'arj'          if ($spec =~ /\.arj$/);
	return 'rpm'          if ($spec =~ /\.rpm$/);
	return 'cpio'         if ($spec =~ /\.cpio$/);
	return 'lzop'         if ($spec =~ /\.lzo$/);
	if (!$manual && $::cfg_use_file) {
		if (!-e $file) {
			warn "$::basename: ".quote($file).": No such file and cannot identify format from extension\n";
			return;
		}
		if (!sysopen(TMP, $file, O_RDONLY)) {
			warn "$::basename: ".quote($file).": $!\n";
			return;
		}
		close TMP;
		if (!-f $file) {
			warn "$::basename: ".quote($file).": not a regular file\n";
			return;
		}
		if ($::opt_verbosity >= 1) {
			warn "$::basename: ".quote($file).": format not known, identifying using file\n";
		}
		my @cmd = ($::cfg_path_file, '-b', '-L', '-z', '--', $file);
		$spec = backticks(@cmd);
		if (!defined $spec) {
			warn "$::basename: $::errmsg\n";
			return;
		}
		if ($? & 0xFF != 0) {
			warn "$::basename: ".quote($::cfg_path_file).": abnormal exit\n";
			return;
		}
		if ($? >> 8 != 0) {
			warn "$::basename: ".quote($file).": unknown file format\n";
			return;
		}
		chomp $spec;
		my $f;
		$f = 'tar+bzip2'    if (!$f && $spec =~ /^(GNU|POSIX) tar archive \(bzip2 compressed data(\W|$)/);
		$f = 'tar+gzip'     if (!$f && $spec =~ /^(GNU|POSIX) tar archive \(gzip compressed data(\W|$)/);
		$f = 'tar+bzip'     if (!$f && $spec =~ /^(GNU|POSIX) tar archive \(bzip compressed data(\W|$)/);
		$f = 'tar+compress' if (!$f && $spec =~ /^(GNU|POSIX) tar archive \(compress'd data(\W|$)/);
		$f = 'tar'        	if (!$f && $spec =~ /^(GNU|POSIX) tar archive(\W|$)/);
		$f = 'zip'        	if (!$f && $spec =~ /^Zip archive data(\W|$)/);
		$f = 'rar'        	if (!$f && $spec eq 'RAR archive data');
		$f = 'lha'					if (!$f && $spec =~ /^LHa \(2\.x\) archive data /);
		$f = 'lha'					if (!$f && $spec =~ /^LHa 2\.x\? archive data /);
		$f = 'lha'					if (!$f && $spec =~ /^LHarc 1\.x archive data /);
		$f = 'arj'					if (!$f && $spec =~ /^ARJ archive data\, /);
		$f = 'cpio'					if (!$f && $spec =~ /^cpio archive$/);
		$f = 'cpio'					if (!$f && $spec =~ /^ASCII cpio archive /);
		$f = 'rpm'					if (!$f && $spec =~ /^RPM v/);
#		$f = 'bzip2'      	if (!$f && $spec =~ /^bzip2 compressed data(\W|$)/);
#		$f = 'bzip'       	if (!$f && $spec =~ /^bzip compressed data(\W|$)/);
#		$f = 'gzip'       	if (!$f && $spec =~ /^gzip compressed data(\W|$)/);
#		$f = 'compress'   	if (!$f && $spec =~ /^compress'd data(\W|$)/);
		$f = 'bzip2'      	if (!$f && $spec =~ / \(bzip2 compressed data(\W|$)/);
		$f = 'bzip'       	if (!$f && $spec =~ / \(bzip compressed data(\W|$)/);
		$f = 'gzip'					if (!$f && $spec =~ / \(gzip compressed data(\W|$)/);
		$f = 'compress'   	if (!$f && $spec =~ / \(compress'd data(\W|$)/);
		$f = 'lzop'       	if (!$f && $spec =~ /^lzop compressed data /);
		if ($f) {
			warn "$::basename: ".quote($file).": format is `$f'\n" if $::opt_verbosity >= 1;
			return $f;
		}
		warn "$::basename: ".quote($file).": unsupported file format `$spec'\n";
		return;
	}
	warn "$::basename: ".quote($file).": unrecognized file format\n";
	return;
}

# backticks(cmdargs, ..)
# An implementation of the backtick (qx//) operator.
# The difference is that command STDERR output will still
# be printed on STDERR, and the shell isn't used to parse
# the command line.
sub backticks(@) {
  if (!pipe(IN,OUT)) {
		$::errmsg = "pipe failed: $!";
		return;
	}
  my $child = fork;
	if (!defined $child) {
		$::errmsg = "fork failed: $!";
		return;
	}
  if ($child == 0) {
    close IN || exit 1;
    close STDOUT || exit 1;
    open(STDOUT, '>&OUT') || exit 1;
		close OUT || exit 1;
    $SIG{__WARN__} = sub {};
    exec(@_) || exit 1;
  }
  close OUT;
  my $text = join('', <IN>);
  close IN;
  if (waitpid($child,0) != $child) {
		$::errmsg = "waitpid failed: $!";
		return;
	}
  return $text;
}

# readconfig(file)
# Read and parse the specified configuration file.
# If the file does not exist, just return.
# If there is an error in the configuration file,
# the program will be terminated. This could be a
# problem when there are errors in the system-wide
# configuration file.
sub readconfig($$) {
	my ($file, $failok) = @_;
	return if ($failok && !-e $file);
	sysopen(FILE, $file, O_RDONLY) || die "$::basename: cannot open `".quote($file)."' for reading: $!\n";	#OK
	while (<FILE>) {
		chomp;
		next if /^\s*(#(.*))?$/;
		my ($var,$val) = /^(.*?)\s+([^\s].*)$/;
		my $varref = undef;
		($var eq 'use_tar_j_option')   && ($varref = \$::cfg_use_tar_j_option);
		($var eq 'use_tar_z_option')   && ($varref = \$::cfg_use_tar_z_option);
		($var eq 'use_gzip_for_z')     && ($varref = \$::cfg_use_gzip_for_z);
		($var eq 'use_find_cpio_print0') && ($varref = \$::cfg_use_find_cpio_print0);
		($var eq 'use_jar')            && ($varref = \$::cfg_use_jar);
		($var eq 'use_file')           && ($varref = \$::cfg_use_file);
		($var eq 'path_pager')         && ($varref = \$::cfg_path_pager);
		($var eq 'path_jar')           && ($varref = \$::cfg_path_jar);
		($var eq 'path_tar')           && ($varref = \$::cfg_path_tar);
		($var eq 'path_zip')           && ($varref = \$::cfg_path_zip);
		($var eq 'path_unzip')         && ($varref = \$::cfg_path_unzip);
		($var eq 'path_gzip')          && ($varref = \$::cfg_path_gzip);
		($var eq 'path_bzip')          && ($varref = \$::cfg_path_bzip);
		($var eq 'path_bzip2')         && ($varref = \$::cfg_path_bzip2);
		($var eq 'path_compress')      && ($varref = \$::cfg_path_compress);
		($var eq 'path_lzop')          && ($varref = \$::cfg_path_lzop);
		($var eq 'path_rar')           && ($varref = \$::cfg_path_rar);
		($var eq 'path_unrar')         && ($varref = \$::cfg_path_unrar);
		($var eq 'path_lha')           && ($varref = \$::cfg_path_lha);
		($var eq 'path_unace')         && ($varref = \$::cfg_path_unace);
		($var eq 'path_unarj')         && ($varref = \$::cfg_path_unarj);
		($var eq 'path_rpm')           && ($varref = \$::cfg_path_rpm);
		($var eq 'path_rpm2cpio')      && ($varref = \$::cfg_path_rpm2cpio);
		($var eq 'path_cpio')          && ($varref = \$::cfg_path_cpio);
		($var eq 'path_find')          && ($varref = \$::cfg_path_find);
		($var eq 'path_xargs')         && ($varref = \$::cfg_path_xargs);
		($var eq 'path_cat')           && ($varref = \$::cfg_path_cat);
		($var eq 'path_file')          && ($varref = \$::cfg_path_file);
		($var eq 'path_diff')          && ($varref = \$::cfg_path_diff);
		($var eq 'args_diff')          && ($varref = \$::cfg_args_diff);
		($var eq 'path_usercfg')       && ($varref = \$::cfg_path_usercfg);
		($var eq 'default_verbosity')  && ($varref = \$::cfg_default_verbosity);
		($var eq 'show_extracted')     && ($varref = \$::cfg_show_extracted);
		($var eq 'keep_compressed')    && ($varref = \$::cfg_keep_compressed);
		($var eq 'tmpdir_name')        && ($varref = \$::cfg_tmpdir_name);
		die "$::basename: ".quote($file).": error in line $.\n" if (!defined $varref);	#OK
		${$varref} = $val;
	}
	close(FILE);
}

# Remove a directory recursively. This function used to change
# the mode on the directories is traverses, but I now consider
# that to be unsafe (what if there's a bug in atool and it
# removes a file it shouldn't?).
sub unlink_directory($) {
	my ($dir) = @_;
	die "$::basename: internal error 1 - please report this bug\n"
		if ($dir eq '/' || $dir eq $ENV{HOME});
#	chmod 0700, $dir || die "$::basename: cannot chmod `".quote($dir)."': $!\n";
	chdir $dir || die "$::basename: cannot chdir `".quote($dir)."': $!\n";
	opendir(DIR, '.') || die "$::basename: cannot list `".quote($dir)."': $!\n";
	my @files = readdir(DIR);
	closedir(DIR);
	foreach my $file (@files) {
		next if $file eq '.' || $file eq '..';
		if (!-d $file) {
			unlink $file || die "$::basename: cannot remove `".quote($file)."': $!\n";
		} else {
			unlink_directory($file);
		}
	}
	chdir '..' || die "$::basename: cannot change directory: $!\n";
	rmdir $dir || die "$::basename: cannot remove `".quote($dir)."': $!\n";
}

# find_comparable_file(dir)
# Assuming that the contents of some archive has been extracted to dir,
# this function will determine the main file or directory in this
# archive - the file or directory which will be compared when this
# archive is compared to some other.
sub find_comparable_file($) {
	my ($dir) = @_;
	my $result = $dir;
	if (opendir(DIR, $dir)) {
		my (@files) = map { readdir(DIR) } 0..3;
		if (@files == 3 && $files[0] eq '.' && $files[1] eq '..') {
			$result = File::Spec->catfile($dir, $files[2]);
		}
		closedir(DIR);
	}
	return $result;
}

# makeabsolute(file)
# Return the absolute version of file.
sub makeabsolute($) {
	my ($file) = @_;
	return $file if (substr($file, 0, 1) eq '/');
	return File::Spec->catfile(getcwd(), $file);
}

# quote(string)
# Quote a style like the GNU fileutils would do (`locale'
# quoting style).
sub quote($) {
  my ($in) = @_;
	my $out = '';
	for (my $c = 0; $c < length($in); $c++) {
		my $ch = substr($in, $c, 1);
		if ($ch eq "\b") {
			$out .= "\\b";
		} elsif ($ch eq "\f") {
			$out .= "\\f";
		} elsif ($ch eq "\n") {
			$out .= "\\n";
		} elsif ($ch eq "\r") {
			$out .= "\\r";
		} elsif ($ch eq "\t") {
			$out .= "\\t";
		} elsif (ord($ch) == 11) {			# Vertical Tab, \v
			$out .= "\\v";
		} elsif ($ch eq "\\") {
			$out .= "\\\\";
		} elsif ($ch eq "'") {
			$out .= "\\'";
		} elsif (!POSIX::isprint($ch)) {
			$out .= sprintf('\\%03o', ord($ch));
		} else {
			$out .= $ch;
		}
	}
  return $out;
}

# shell_execute(@)
# Execute a command with pipes and output redirection like the
# shell does. Only difference is we do it without the shell.
# This reason for this is because we don't have to quote
# meta-characters - some meta-characters like LF and DEL are
# unquotable!
sub shell_execute(@) {
	my (@cmdspec) = @_;

	my @cmds = ();
	my $start = 0;
	my $redir_out = undef;
	for (my $c = 0; $c < @cmdspec; $c++) {
		if (ref $cmdspec[$c]) {
			push @cmds, [ @cmdspec[$start..$c-1] ];
			$start = $c+1;
			$redir_out = $cmdspec[$c+1] if (${$cmdspec[$c]}[0] eq '>');
		}
	}
	push @cmds, [ @cmdspec[$start..$#cmdspec] ] if !defined $redir_out;

	$SIG{INT} = 'IGNORE';

	my @ip = ();
	my @op = ();
	my @children = ();
	for (my $c = 0; $c <= $#cmds; $c++) {
		if ($c != $#cmds) {
			@op = reverse POSIX::pipe();
			if (!@op || !defined $op[0] || !defined $op[1]) {
				$::errmsg = "pipe failed: $!";
				return 0;
			}
		}
		if ($c == $#cmds && defined $redir_out) {
			@_ = (); # XXX: necessary to overcome POSIX autoload bug!
			@op = (POSIX::open($redir_out, &POSIX::O_WRONLY | &POSIX::O_CREAT));
			if (!@op || !defined $op[0]) {
				$::errmsg = "cannot open `".quote($redir_out)."' for writing: $!";
				return 0;
			}
		}
		my $pid = fork();
		die "fork failed: $!\n" if !defined $pid;
		if ($pid == 0) {
			$SIG{INT} = '';
			if (@ip) {
				die "dup2 failed: $!\n" if POSIX::dup2($ip[1], 0) < 0;
				POSIX::close($_) foreach (@ip);
			}
			if (@op) {
				die "dup2 failed: $!\n" if POSIX::dup2($op[0], 1) < 0;
				POSIX::close($_) foreach (@op);
			}
			exec(@{$cmds[$c]}) || die "cannot execute `${$cmds[$c]}[0]': $!\n";
		}
		POSIX::close($op[0]) if ($c == $#cmds && defined $redir_out);
		POSIX::close($_) foreach (@ip);
		@ip = @op;
		@op = ();
		push @children, $pid;
	}

	foreach (@children) {
		if (waitpid($_,0) < 0) {
			$::errmsg = "waitpid failed: $!";
			return 0;
		}
	}
	$SIG{INT} = '';

	return 1;
}

# Write dir to file indicated by $::opt_save_outdir.
#
sub save_outdir($) {
	my ($dir) = @_;
	if (defined $::opt_save_outdir && !-l $dir && -d $dir) {
		if (!sysopen(TMP, $::opt_save_outdir, O_WRONLY)) {
			warn die "$::basename: cannot open `".quote($::opt_save_outdir)."' for writing: $!\n";
		} else {
			print TMP $dir, "\n";
			close(TMP);
		}
	}
}

# Somewhat stupid subroutine to add xargs to the command line.
#
sub handle_empty_add(@) {
	my @cmd = @_;
	unshift @cmd, '--';
	unshift @cmd, '-0' if ($::opt_null);
	unshift @cmd, $::cfg_path_xargs;
	return @cmd;
}

# Return a suitable pager command
#
sub get_pager_program {
	return $ENV{PAGER} if (exists $ENV{PAGER});
	return $::cfg_path_pager;
}

sub END {
	map (rmdir, @::rmdirs);	# Errors are ignored
}
