#!/usr/bin/ruby
##############################################################################
# tpkg package management system
# License: MIT (http://www.opensource.org/licenses/mit-license.php)
##############################################################################

# When run from the source repository or from an unpacked copy of the
# distribution we want to find the local library, even if there's a copy
# installed on the system.  The installed copy is likely older, and the API
# may be out of sync with this executable.
$:.unshift(File.expand_path('../lib', File.dirname(__FILE__)))

require 'optparse'
require 'tpkg'

#
# Parse the command line options
#

@action = nil
@action_value = nil
@debug = false
@prompt = true
@quiet = false
@sudo = nil
@force = false
@deploy = false
@deploy_params = ARGV   # hold parameters for how to invoke tpkg on the machines we're deploying to
@deploy_options = {}    # options for how to run the deployer
@servers = []
@groups = nil
@worker_count = 10
@rerun_with_sudo = false
@sources = []
@tpkg_options = {}	# options for instantiating Tpkg object
@init_options = {}      # options for how to run init scripts
@other_options = {}     
@compress = "gzip"

#
# Subroutines
#

def rerun_with_sudo_if_necessary
  if Process.euid != 0 && @sudo
    warn "Executing with sudo" if !@quiet
    # Depending on how sudo is configured it might remove TPKG_HOME from the
    # environment.  As such we set the base as a command line option to ensure
    # it survives the sudo process.
    if ENV['TPKG_HOME']
      exec('sudo', '-H', $0, '--base', ENV['TPKG_HOME'], *ARGV)
    else
      exec('sudo', '-H', $0, *ARGV)
    end
  end
end

# This method can only be safely called after command line option parsing is
# complete
@config_file_settings = nil
def parse_config_files
  if @config_file_settings
    return @config_file_settings
  end
  
  # FIXME: Move config file parsing to tpkg.rb
  # http://sourceforge.net/apps/trac/tpkg/ticket/28
  fsroot = @tpkg_options[:file_system_root] ? @tpkg_options[:file_system_root] : ''
  settings = {:sources => []}
  [File.join(fsroot, Tpkg::DEFAULT_CONFIGDIR, 'tpkg.conf'),
   File.join(fsroot, ENV['HOME'], ".tpkg.conf")].each do |configfile|
    if File.exist?(configfile)
      IO.foreach(configfile) do |line|
        line.chomp!
        next if (line =~ /^\s*$/);  # Skip blank lines
        next if (line =~ /^\s*#/);  # Skip comments
        line.strip!  # Remove leading/trailing whitespace
        key, value = line.split(/\s*=\s*/, 2)
        if key == 'base'
          settings[:base] = value
          puts "Loaded base #{value} from #{configfile}" if @debug
        elsif key == 'source'
          settings[:sources] << value
          puts "Loaded source #{value} from #{configfile}" if @debug
        elsif key == 'report_server'
          settings[:report_server] = value
          puts "Loaded report server #{value} from #{configfile}" if @debug
        elsif key == 'host_group_script'
          settings[:host_group_script] = value
          puts "Loaded host group script #{value} from #{configfile}" if @debug
        elsif key == 'sudo'
          sudoval = nil
          # My kingdom for a String#to_boolean
          if value == 'true'
            sudoval = true
          elsif value == 'false'
            sudoval = false
          else
            puts "Unrecognized value #{value} for sudo setting in #{configfile}, ignoring"
          end
          if !sudoval.nil?
            settings[:sudo] = sudoval
            puts "Loaded sudo #{value} from #{configfile}" if @debug
          end
        end
      end
    end
  end
  
  @config_file_settings = settings
end

passphrase_callback = lambda do | package |
#  ask("Passphrase for #{package}: ", true)
  begin
    system 'stty -echo;' 
    print "Passphrase for #{package}: "
    input = STDIN.gets.chomp
  ensure
    system 'stty echo; echo ""'
  end
  input
end

#
# Begin main program logic
#

opts = OptionParser.new(nil, 24, '  ')
opts.banner = 'Usage: tpkg [options]'
opts.on('--make', '-m', '=DIRECTORY', 'Make a package out of the contents of the directory') do |opt|
  @action = :make
  @action_value = opt
end
installexample = "      --install pkgname=version=package_version\n          (Example: tpkg --install hive=2.1)  Will install hive version 2.1"
opts.on('--install', '-i', '=PACKAGES', "Install one or more packages\n#{installexample}", Array) do |opt|
  @rerun_with_sudo = true
  @action = :install
  @action_value = opt
end
opts.on('--upgrade', '-u', '=PACKAGES', 'Upgrade one or more packages', Array) do |opt|
  @rerun_with_sudo = true
  @action = :upgrade
  @action_value = opt
end
opts.on('--downgrade', '=PACKAGES', 'Downgrade one or more packages', Array) do |opt|
  @other_options[:downgrade] = true
  @rerun_with_sudo = true
  @action = :upgrade
  @action_value = opt
end
opts.on('--servers', '-s', '=SERVERS', Array, 'Servers on which to apply actions, defaults to local') do |opt|
  @servers.concat(opt)
  @deploy = true
  # FIXME: this won't remove options that the user specified as an
  # abbreviation.  I.e. if the option is --servers and the user specified
  # --serv (which OptionParser will accept as long as it is unambiguous) this
  # won't detect and remove it.
  @deploy_params = @deploy_params - ['--servers', '-s', @servers.join(","), "--servers=#{@servers.join(',')}"]
end
opts.on('--groups', '-g', '=GROUP', Array, 'Group of server on which to apply actions') do |opt|
  # We'll finish processing this later.  To expand the groups we need the name
  # of the host_group_script from the config file, but we can't safely call
  # parse_config_files until we're done processing command line options.
  @groups = opt
  @deploy = true
  # FIXME: this won't remove options that the user specified as an
  # abbreviation.  I.e. if the option is --servers and the user specified
  # --serv (which OptionParser will accept as long as it is unambiguous) this
  # won't detect and remove it.
  @deploy_params = @deploy_params - ['--groups', '-g', @groups.join(","), "--servers=#{@groups.join(',')}"]
end
opts.on('--ua', 'Upgrade all packages') do |opt|
  @rerun_with_sudo = true
  @action = :upgrade
end
opts.on('--remove', '-r', '=PACKAGES', 'Remove one or more packages', Array) do |opt|
  @rerun_with_sudo = true
  @action = :remove
  @action_value = opt
end
opts.on('--rd', '=PACKAGES', 'Similar to -r but also remove depending packages', Array) do |opt|
  @rerun_with_sudo = true
  @other_options[:remove_all_dep] = true
  @action = :remove
  @action_value = opt
end
opts.on('--rp', '=PACKAGES', 'Similar to -r but also remove prerequisites', Array) do |opt|
  @rerun_with_sudo = true
  @other_options[:remove_all_prereq] = true
  @action = :remove
  @action_value = opt
end
opts.on('--ra', 'Remove all packages') do |opt|
  @rerun_with_sudo = true
  @action = :remove
end
opts.on('--verify', '-V', '=NAME', 'Verify packages') do |opt|
  @rerun_with_sudo = true
  @action = :verify
  @action_value = opt
end
opts.on('--start', '=NAME', 'Start the init script for specified package', Array) do |opt|
  @rerun_with_sudo = true
  @action = :execute_init
  @init_options[:packages] = opt
  @init_options[:cmd] = 'start'
end
opts.on('--stop', '=NAME', 'Stop the init script for specified package', Array) do |opt|
  @rerun_with_sudo = true
  @action = :execute_init
  @init_options[:packages] = opt
  @init_options[:cmd] = 'stop'
end
opts.on('--restart', '=NAME', 'Restart the init script for specified package', Array) do |opt|
  @rerun_with_sudo = true
  @action = :execute_init
  @init_options[:packages] = opt
  @init_options[:cmd] = 'restart'
end
opts.on('--reload', '=NAME', 'Reload the init script for specified package', Array) do |opt|
  @rerun_with_sudo = true
  @action = :execute_init
  @init_options[:packages] = opt
  @init_options[:cmd] = 'reload'
end
opts.on('--status', '=NAME', 'Get status from init script for specified package', Array) do |opt|
  @rerun_with_sudo = true
  @action = :execute_init
  @init_options[:packages] = opt
  @init_options[:cmd] = 'status'
end
opts.on('--start-all', 'Start the init scripts for all packages') do |opt|
  @rerun_with_sudo = true
  @action = :execute_init
  @init_options[:cmd] = 'start'
end
opts.on('--stop-all', 'Stop the init script for all packages') do |opt|
  @rerun_with_sudo = true
  @action = :execute_init
  @init_options[:cmd] = 'stop'
end
opts.on('--restart-all', 'Restart the init script for all packages') do |opt|
  @rerun_with_sudo = true
  @action = :execute_init
  @init_options[:cmd] = 'restart'
end
opts.on('--reload-all', 'Reload the init script for all packages') do |opt|
  @rerun_with_sudo = true
  @action = :execute_init
  @init_options[:cmd] = 'reload'
end
opts.on('--exec-init', '=NAME', 'Execute init scripts for specified packages', Array) do |opt|
  @rerun_with_sudo = true
  @init_options[:packages] = opt
  @action = :execute_init
end
opts.on('--init-script', '=NAME', 'What init scripts to execute', Array) do |opt|
  @rerun_with_sudo = true
  @init_options[:scripts] = opt
end
opts.on('--init-cmd', '=CMD', 'Invoke specified init script command') do |opt|
  @rerun_with_sudo = true
  @init_options[:cmd] = opt
end
opts.on('--query', '-q', '=NAMES', 'Check if a package is installed', Array) do |opt|
  # People mistype -qa instead of --qa frequently
  if opt == ['a']
    warn "NOTE: tpkg -qa queries for a pkg named 'a', you probably want --qa for all pkgs"
  end
  @action = :query_installed
  @action_value = opt
end
# --qv is deprecated
opts.on('--qs', '--qv', '=NAME', 'Check if a package is available on server', Array) do |opt|
  @action = :query_available
  @action_value = opt
end
opts.on('--qa', 'List all installed packages') do |opt|
  @action = :query_installed
end
# --qva is deprecated
opts.on('--qas', '--qva', 'List all packages on server') do |opt|
  @action = :query_available
end
opts.on('--qi', '=NAME', 'Display the info for a package') do |opt|
  @action = :query_info
  @action_value = opt
end
opts.on('--qis', '=NAME', 'Display the info for a package on the server') do |opt|
  @action = :query_info_available
  @action_value = opt
end
opts.on('--ql', '=NAME', 'List the files in a package') do |opt|
  @action = :query_list_files
  @action_value = opt
end
opts.on('--qls', '=NAME', 'List the files in a package on the server') do |opt|
  @action = :query_list_files_available
  @action_value = opt
end
opts.on('--qf', '=FILE', 'List the package that owns a file') do |opt|
  @action = :query_who_owns_file
  @action_value = opt
end
opts.on('--qr', '=NAME', 'List installed packages that require package') do |opt|
  @action = :query_requires
  @action_value = opt
end
opts.on('--qd', '=NAME', 'List the packages on which the given package depends') do |opt|
  @action = :query_depends
  @action_value = opt
end
opts.on('--qds', '=NAME', 'List pkgs on which given package on server depends') do |opt|
  @action = :query_depends_available
  @action_value = opt
end

opts.on('--dw', '=INTEGER', 'Number of workers for deploying') do |opt|
  @worker_count = opt.to_i
  # FIXME: this won't remove options that the user specified as an
  # abbreviation.  I.e. if the option is --servers and the user specified
  # --serv (which OptionParser will accept as long as it is unambiguous) this
  # won't detect and remove it.
  @deploy_params = @deploy_params - ['--dw', @worker_count, "--dw=#{opt}"]
end
opts.on('--qX', '=FILENAME', 'Display raw metadata (tpkg.yml) of the given package') do |opt|
  @action = :query_tpkg_metadata
  @action_value = opt
end
opts.on('--qXs', '=FILENAME', 'Display raw metadata of given package on the server') do |opt|
  @action = :query_tpkg_metadata_available
  @action_value = opt
end
opts.on('--history', 'Display package installation history') do |opt|
  @action = :query_history
end
opts.on('--qenv', "Display machine's information") do |opt|
  @action = :query_env
end
opts.on('--qconf', "Display tpkg's configuration settings") do |opt|
  @action = :query_conf
end
opts.on('--base', '=BASE', 'Base directory for tpkg operations') do |opt|
  @tpkg_options[:base] = opt
end
opts.on('--extract', '-x', '=DIRECTORY', 'Extract the metadata for a directory of packages') do |opt|
  @action = :extract
  @action_value = opt
end
opts.on('--source', '=NAME', 'Sources where packages are located', Array) do |opt|
  @sources = opt
end
opts.on('--download', '=PACKAGES', 'Download one or more packages', Array) do |opt|
  @action = :download
  @action_value = opt
end
opts.on('-n', '--no-prompt', 'No confirmation prompts') do |opt|
  @prompt = opt
  Tpkg::set_prompt(@prompt)
end
opts.on('--quiet', 'Reduce informative but non-essential output') do |opt|
  @quiet = opt
end
opts.on('--no-sudo', 'No calls to sudo for operations that might need root') do |opt|
  @sudo = opt
end
opts.on('--lock-force', 'Force the removal of an existing lockfile') do |opt|
  @tpkg_options[:lockforce] = opt
end
opts.on('--force-replace', 'Replace conflicting pkgs with the new one(s)') do |opt|
  @other_options[:force_replace] = opt
end
opts.on('--force', 'Force the execution of a given task') do |opt|
  @force = opt
end
opts.on('-o', '--out', '=DIR', 'Output directory for the --make option') do |opt|
  @other_options[:out] = opt
end
opts.on('--skip-remove-stop', 'Do not run init script stop on package removal') do |opt|
  @other_options[:skip_remove_stop] = opt
end
opts.on('--use-ssh-key [FILE]', 'Use ssh key for deploying instead of password') do |opt|
  @deploy_options["use-ssh-key"] = true
  @deploy_options["ssh-key"] = opt
  # FIXME: this won't remove options that the user specified as an
  # abbreviation.  I.e. if the option is --servers and the user specified
  # --serv (which OptionParser will accept as long as it is unambiguous) this
  # won't detect and remove it.
  @deploy_params = @deploy_params - ['--use-ssh-key', opt, "--use-ssh-key=#{opt}"]
end
opts.on('--deploy-as', '=USERNAME', 'What username to use for deploying to remote server') do |opt|
  @deploy_options["deploy-as"] = opt
  # FIXME: this won't remove options that the user specified as an
  # abbreviation.  I.e. if the option is --servers and the user specified
  # --serv (which OptionParser will accept as long as it is unambiguous) this
  # won't detect and remove it.
  @deploy_params = @deploy_params - ['--deploy-as']
end
acceptable_compress_arguments = ['gzip', 'bz2', 'no']
opts.on('--compress [TYPE]',
        acceptable_compress_arguments,
        "Compress files when making packages " +
        "(#{acceptable_compress_arguments.join(',')})") do |opt|
  # Acceptable 
  if opt == nil  # No argument specified by user
    @compress = true
  elsif opt == "no"
    @compress= false
  else
    @compress = opt
  end
end
opts.on('--test-root TESTDIR', 'For use by the test suite only.') do |opt|
  @tpkg_options[:file_system_root] = opt
end
opts.on('--debug', 'Print lots of messages about what tpkg is doing') do |opt|
  @debug = opt
  Tpkg::set_debug(@debug)
end
opts.on('--version', 'Show tpkg version') do |opt|
  @action = :query_version
end
opts.on_tail("-h", "--help", "Show this message") do
  puts opts
  exit
end

leftovers = nil
begin
  leftovers = opts.parse(ARGV)
rescue OptionParser::ParseError => e
  $stderr.puts "Error parsing arguments, try --help"
  $stderr.puts e.message
  exit 1
end

# Display a usage message if the user did not specify a valid action to perform.
if !@action
  puts opts
  exit
end

if @groups
  settings = parse_config_files
  if settings[:host_group_script]
    if !File.executable?(settings[:host_group_script])
      warn "Warning: host group script #{settings[:host_group_script]} is not executable, execution will likely fail"
    end
    servers = []
    @groups.each do |group|
      IO.popen(settings[:host_group_script]) do |pipe|
        pipe.each_line do |line|
          servers << line.chomp
        end
      end
    end
    servers.uniq!
    puts "Expanded groups into #{servers.length} servers" if @debug
    @servers.concat(servers)
  else
    abort "No host_group_script defined in config files, can't expand groups"
  end
end

#
# Figure out base directory, sources and other configuration
#

settings = parse_config_files

# base can come from four possible places.  They take precedence in this
# order:
# - command line option
# - TPKG_HOME environment variable
# - config file
# - Tpkg::DEFAULT_BASE constant defined in tpkg.rb
if ENV['TPKG_HOME']
  if !@tpkg_options[:base]
    @tpkg_options[:base] = ENV['TPKG_HOME']
    # Warn the user, as this could potentially be confusing
    # if they don't realize there's an environment variable set.
    warn "Using base '#{@tpkg_options[:base]}' base from $TPKG_HOME" if !@quiet
  else
    warn "Ignoring $TPKG_HOME" if @debug
  end
end
if settings[:base]
  if !@tpkg_options[:base]
    # Warn the user, as this could potentially be confusing
    # if they don't realize there's a config file lying
    # around
    @tpkg_options[:base] = settings[:base]
    warn "Using base #{@tpkg_options[:base]} from config file" if !@quiet
  else
    warn "Ignoring 'base' option in config file" if @debug
  end
end
if !@tpkg_options[:base]
  @tpkg_options[:base] = Tpkg::DEFAULT_BASE
end

# Sources can come from the command line and config files.  We use the
# combined set of sources.
@sources.concat(settings[:sources]).uniq!
@tpkg_options[:sources] = @sources

@tpkg_options[:report_server] = settings[:report_server]
# sudo can come from three possible places.  They take precedence in this
# order:
# - command line option
# - config file
# - Tpkg.sudo_default? method defined in tpkg.rb
case
when !@sudo.nil?
  @tpkg_options[:sudo] = @sudo
when settings.has_key?(:sudo)
  @sudo = @tpkg_options[:sudo] = settings[:sudo]
else
  @sudo = @tpkg_options[:sudo] = Tpkg.sudo_default?
end
@tpkg_options[:force] = @force

if !@sudo
  curruid = Process.euid
  if curruid == 0
    # Besides there being no point to running with --no-sudo when root, we
    # don't want users to accidentally create files/directories that can't be
    # modified by other users who properly run --no-sudo as a regular user.
    raise "--no-sudo cannot be used as 'root' user or via sudo"
  end
  fsroot = @tpkg_options[:file_system_root] ? @tpkg_options[:file_system_root] : ''
  base = File.join(fsroot, @tpkg_options[:base])
  if File.exist?(base)
    baseuid = File.stat(base).uid
    # We want to ensure that all --no-sudo usage within a given base directory
    # is done under the same account.
    if baseuid != curruid
      raise "Base dir #{@tpkg_options[:base]} owned by UID #{baseuid}, not your UID #{curruid}"
    end
  end
end

# Rerun with sudo if necessary, unless it's a deploy, then
# we don't need to run with sudo on this machine. It will run with sudo
# on the remote machine
if @rerun_with_sudo && !@deploy
  rerun_with_sudo_if_necessary
end

#
# Perform the user's requested operation
#

if @deploy
#  puts "Creating deployer with #{@worker_count} number of worker"
  @deploy_options["max-worker"] = @worker_count
  @deploy_options["abort-on-fail"] = false

  # Check to see if ssh-key is accessible
  # Net::SSH doesn't warn the user about problems with the key file
  # (i.e. if it doesn't exist or isn't readable), resulting in the user
  # just getting a generic "Bad username/password combination" error
  # message because authentication fails.  As such we do some of our own
  # checking and warning here so that the user gets a more specific
  # error message.
  if @deploy_options["use-ssh-key"] && @deploy_options["ssh-key"]
    ssh_key = @deploy_options["ssh-key"]
    if !File.readable?(ssh_key) && (Process.euid == 0 || !@sudo)
      raise "Unable to read ssh key from #{ssh_key}"
    elsif !File.readable?(ssh_key)
      warn "Warning: Unable to read ssh key from #{ssh_key}. Attempting to rerun tpkg with sudo."
      rerun_with_sudo_if_necessary
    end
  end

  Tpkg::deploy(@deploy_params, @deploy_options,  @servers) 
  exit
end

if @action_value.is_a?(Array)
  @action_value.uniq!
end

# tell tpkg not to prompt if stdin is not tty
if !$stdin.tty?
  Tpkg::set_prompt(false)
end

ret_val = 0
case @action
when :make
  @other_options[:force] = @force
  @other_options[:compress] = @compress
  madepkg = Tpkg::make_package(@action_value, passphrase_callback, @other_options)
  if madepkg
    puts "Package is #{madepkg}"
  else
    puts "Package build aborted or failed"
  end
when :extract
  Tpkg::extract_metadata(@action_value)
when :install
  tpkg = Tpkg.new(@tpkg_options)
  ret_val = tpkg.install(@action_value, passphrase_callback, @other_options)
when :upgrade
  tpkg = Tpkg.new(@tpkg_options)
  ret_val = tpkg.upgrade(@action_value, passphrase_callback, @other_options)
when :remove
  tpkg = Tpkg.new(@tpkg_options)
  ret_val = tpkg.remove(@action_value, @other_options)
when :download
  tpkg = Tpkg.new(@tpkg_options)
  ret_val = tpkg.download_pkgs(@action_value, @other_options)
when :verify
  result = nil
  # Verify a given .tpkg file
  if File.file?(@action_value)
    Tpkg::verify_package_checksum(@action_value)
  # Verify an installed pkg
  else
    tpkg = Tpkg.new(@tpkg_options)
    results = tpkg.verify_file_metadata([@action_value])
    if results.length == 0
      puts "No package found"
    end
    success = true
    results.each do | file, errors |
      if errors.length == 0
        puts "#{file}: Passed"
      else
        puts "#{file}: Failed (Reasons: #{errors.join(", ")})"
        success = false
      end
    end 
    puts "Package verification failed" unless success 
  end
when :execute_init
  tpkg = Tpkg.new(@tpkg_options)
  if @init_options[:cmd].nil?
    raise "You didn't specify what init command to run"
  end
  ret_val = tpkg.execute_init(@init_options)
when :query_installed
  tpkg = Tpkg.new(@tpkg_options)
  matches = []
  if @action_value
    # The --query switch is set to accept multiple values (the "Array"
    # parameter in the opts.on line for --query) so users can do things like
    # "tpkg -q foo,bar" to check multiple packages at once.  As such
    # @action_value is an Array for this switch.
    requirements = []
    packages = {}
    tpkg.parse_requests(@action_value, requirements, packages,
      :installed_only => true)
    if packages.values.all? {|pkg| pkg.empty?}
      # If the user requested specific packages and we found no matches
      # then exit with a non-zero value to indicate failure.  This allows
      # command-line syntax like "tpkg -q foo || tpkg -i foo" to ensure
      # that a package is installed.
      ret_val = 1
      $stderr.puts "No packages matching '#{@action_value.join(',')}' installed" if !@quiet
    else
      packages.each do | name, pkgs |
        matches.concat(pkgs)
      end
    end
  else
    # --qa is implemented by setting @action to :query_installed and
    # @action_value to nil
    matches = tpkg.installed_packages_that_meet_requirement
    if matches.empty?
      ret_val = 1
      $stderr.puts "No packages installed" if !@quiet
    end
  end
  if !@quiet
    matches.sort(&Tpkg::SORT_PACKAGES).each do |pkg|
      puts pkg[:metadata][:filename]
    end
  end
when :query_info
  tpkg = Tpkg.new(@tpkg_options)
  requirements = []
  packages = {}
  tpkg.parse_requests([@action_value], requirements, packages,
    :installed_only => true)
  metadatas = []
  packages.each do | name, pkgs |
    pkgs.each do | pkg |
      metadatas << pkg[:metadata]
    end
  end
  output_strings = []
  already_displayed = {}
  metadatas.each do |metadata|
    next if already_displayed[metadata[:filename]]
    already_displayed[metadata[:filename]] = true
    output_string = ''
    [:name, :version, :package_version, :operatingsystem, :architecture, :maintainer, :description, :bugreporting].each do |field|
      metadata[field] = 'any' if field == :operatingsystem && metadata[field].nil?
      metadata[field] = 'any' if field == :architecture && metadata[field].nil?
      if metadata[field]
        if metadata[field].kind_of?(Array)
          output_string << "#{field}: #{metadata[field].join(',')}\n"
        else
          output_string << "#{field}: #{metadata[field]}\n"
        end
      end
    end
    if metadata[:dependencies]
      output_string << "(This package depends on other packages, use --qd to view the dependencies)\n"
    end
    # Older versions of tpkg did not insert a tpkg_version field into the
    # package metadata when building packages
    tpkg_version = metadata[:tpkg_version] || "< 1.26.1"
    output_string << "(This package was built with tpkg version #{tpkg_version})\n"
    output_strings << output_string
  end
  print output_strings.join("================================================================================\n")
  if already_displayed.empty?
    ret_val = 1
    # --qi --quiet doesn't seem like a meaningful combination, so I'm not
    # suppressing this for @quiet
    $stderr.puts "No packages matching '#{@action_value}' installed"
  end
when :query_info_available
  tpkg = Tpkg.new(@tpkg_options)
  requirements = []
  packages = {}
  tpkg.parse_requests([@action_value], requirements, packages)
  availpkgs = []
  packages.each do | name, pkgs |
    availpkgs.concat(pkgs)
  end
  available = availpkgs.select do |pkg|
    pkg[:source] != :native_installed &&
    pkg[:source] != :native_available &&
    pkg[:source] != :currently_installed
  end
  if available.empty?
    ret_val = 1
    # --qis --quiet doesn't seem like a meaningful combination, so I'm not
    # suppressing this for @quiet
    $stderr.puts "No packages matching '#{@action_value}' available"
  else
    metadatas = available.collect {|avail| avail[:metadata]}
    output_strings = []
    already_displayed = {}
    metadatas.each do |metadata|
      next if already_displayed[metadata[:filename]]
      already_displayed[metadata[:filename]] = true
      output_string = ''
      [:name, :version, :package_version, :operatingsystem, :architecture, :maintainer, :description, :bugreporting].each do |field|
        metadata[field] = 'any' if field == :operatingsystem && metadata[field].nil?
        metadata[field] = 'any' if field == :architecture && metadata[field].nil?
        if metadata[field]
          if metadata[field].kind_of?(Array)
            output_string << "#{field}: #{metadata[field].join(',')}\n"
          else
            output_string << "#{field}: #{metadata[field]}\n"
          end
        end
      end
      if metadata[:dependencies]
        output_string << "(This package depends on other packages, use --qd to view the dependencies)\n"
      end
      # Older versions of tpkg did not insert a tpkg_version field into the
      # package metadata when building packages
      tpkg_version = metadata[:tpkg_version] || "< 1.26.1"
      output_string << "(This package was built with tpkg version #{tpkg_version})\n"
      output_strings << output_string
    end
    print output_strings.join("================================================================================\n")
  end
when :query_list_files
  tpkg = Tpkg.new(@tpkg_options)
  requirements = []
  packages = {}
  tpkg.parse_requests([@action_value], requirements, packages,
    :installed_only => true)
  if packages.values.all? {|pkg| pkg.empty?}
    ret_val = 1
    # --ql --quiet doesn't seem like a meaningful combination, so I'm not
    # suppressing this for @quiet
    $stderr.puts "No packages matching '#{@action_value}' installed"
  else
    # For this switch we need separate handling for installed and uninstalled
    # packages.  For installed packages we know where their relocatable files
    # ended up and can give the user regular paths.  For uninstalled packaages
    # we don't know what base will be used when the package is installed, so
    # we need to indicate to the user that their are relocatable files and
    # just display their path relative to the eventual base directory.
    ci_pkgfiles = []
    packages.each do | name, pkgs |
      pkgs.each do | pkg |
        if pkg[:source] == :currently_installed
          ci_pkgfiles << pkg[:metadata][:filename]
        else
          puts "#{pkg[:source]}:"
          fip = Tpkg.files_in_package(pkg[:source])
          fip[:root].each { |file| puts file }
          fip[:reloc].each { |file| puts '<relocatable>/' + file }
        end
      end
    end
    files = tpkg.files_for_installed_packages(ci_pkgfiles)
    files.each do |pkgfile, fip|
      puts "#{pkgfile}:"
      fip[:normalized].each { |file| puts file }
    end
  end
when :query_list_files_available
  tpkg = Tpkg.new(@tpkg_options)
  requirements = []
  packages = {}
  tpkg.parse_requests([@action_value], requirements, packages)
  availpkgs = []
  packages.each do | name, pkgs |
    availpkgs.concat(pkgs)
  end
  available = availpkgs.select do |pkg|
    pkg[:source] != :native_installed &&
    pkg[:source] != :native_available &&
    pkg[:source] != :currently_installed
  end
  if available.empty?
    ret_val = 1
    # --qls --quiet doesn't seem like a meaningful combination, so I'm not
    # suppressing this for @quiet
    $stderr.puts "No packages matching '#{@action_value}' available"
  else
    downloaddir = Tpkg::tempdir('download')
    available.each do |pkg|
      # FIXME: I've duplicated from the install and upgrade methods this logic
      # to calculate pkgfile, it should be encapsulated in a method
      pkgfile = nil
      if File.file?(pkg[:source])
        pkgfile = pkg[:source]
      elsif File.directory?(pkg[:source])
        pkgfile = File.join(pkg[:source], pkg[:metadata][:filename])
      else
        pkgfile = download(pkg[:source], pkg[:metadata][:filename], downloaddir)
      end
      puts "#{pkg[:metadata][:filename]}:"
      fip = Tpkg.files_in_package(pkgfile)
      fip[:root].each { |file| puts file }
      fip[:reloc].each { |file| puts '<relocatable>/' + file }
    end
    FileUtils.rm_rf(downloaddir)
  end
when :query_who_owns_file
  tpkg = Tpkg.new(@tpkg_options)
  owned = false
  expanded_file = File.expand_path(@action_value)
  tpkg.files_for_installed_packages.each do |pkgfile, fip|
    fip[:normalized].each do |file|
      if file == expanded_file
        puts "#{file}: #{pkgfile}"
        owned = true
      end
    end
  end
  if !owned
    ret_val = 1
    # --qf --quiet doesn't seem like a meaningful combination, so I'm not
    # suppressing this for @quiet
    $stderr.puts "No package owns file '#{@action_value}'"
  end
when :query_available
  tpkg = Tpkg.new(@tpkg_options)
  availpkgs = []
  if @action_value
    # The --qs switch is set to accept multiple values (the "Array"
    # parameter in the opts.on line for --qs) so users can do things like
    # "tpkg --qs foo,bar" to check multiple packages at once.  As such
    # @action_value is an Array for this switch.
    requirements = []
    packages = {}
    tpkg.parse_requests(@action_value, requirements, packages)
    packages.each do | name, pkgs |
      availpkgs.concat(pkgs)
    end
  else
    # --qas is implemented by setting @action to :query_available and
    # @action_value to nil
    availpkgs.concat(tpkg.available_packages_that_meet_requirement)
  end
  available = availpkgs.select do |pkg|
    pkg[:source] != :native_installed &&
    pkg[:source] != :native_available &&
    # The tpkg library treats currently installed packages as "available"
    # because they are available to meet a user's requirement.  I.e. if the
    # user asks to install ruby and a ruby package is already installed that
    # satisfies the user's requirement even if there's no ruby package in any
    # of the sources.  But for these query options I think the reasonable
    # interpretation is that the user would like to know if there's a package
    # in a source that could be installed.  For example, if the user queries
    # for the availability of ruby and we show it as available because it is
    # installed, but then they go to another machine with the same sources
    # defined and try to install ruby and it fails because there is no ruby in
    # any of the sources I think the user is likely to find that unexpected
    # and annoying.
    pkg[:source] != :currently_installed
  end
  if available.empty?
    ret_val = 1
    if !@quiet
      if @action_value
        $stderr.puts "No packages matching '#{@action_value.join(',')}' available"
      else
        $stderr.puts "No packages available"
      end
    end
  else
    if !@quiet
      available.sort(&Tpkg::SORT_PACKAGES).each do |pkg|
        puts "#{pkg[:metadata][:filename]} (#{pkg[:source]})"
      end
    end
  end
when :query_requires
  tpkg = Tpkg.new(@tpkg_options)
  
  # Parse the request
  requirements = [] 
  packages = {}
  tpkg.parse_requests([@action_value], requirements, packages,
    :installed_only => true)
  
  # Note that we don't stop here in the case of this switch, but continue to
  # check if anything depends on the package the user asked about.  There
  # shouldn't be a situation where there's another package installed that
  # depends on the package the user is asking about, but the user's package is
  # not installed.  I.e. if foo depends on bar but only foo is installed and
  # the user asks what depends on bar, the answer is still foo.  That
  # information might be useful to the user, even though that situation should
  # have been avoided in the first place.  Broken dependency trees due to
  # manually messing with the repo, using --force, etc. can happen and the
  # user may be using the --qr option just because they're trying to sort out
  # a mess.
  if packages.values.all? {|pkg| pkg.empty?}
    ret_val = 1
    # --qr --quiet doesn't seem like a meaningful combination, so I'm not
    # suppressing this for @quiet
    $stderr.puts "No packages matching '#{@action_value}' installed"
  end
  
  # Get dependencies of all installed packages
  dependencies = {}
  tpkg.metadata_for_installed_packages.each do |metadata|
    dependencies[metadata[:filename]] = metadata[:dependencies]
  end
  
  # Check to see if any dependencies match with what the user specified in the
  # request
  requirees = {}
  packages.each do |name, pkgs|
    pkgs.each do |pkg|
      next if pkg[:source] != :currently_installed
      dependencies.each do | requiree, deps |
        next if deps.nil?
        deps.each do | dep | 
          if Tpkg::package_meets_requirement?(pkg, dep)
            pkgfilename = pkg[:metadata][:filename]
            if !requirees[pkgfilename]
              requirees[pkgfilename] = []
            end
            requirees[pkgfilename] << requiree
          end
        end
      end
    end
  end
  
  if !requirees.empty?
    requirees.keys.sort.each do |pkgfilename|
      puts "The following package(s) require #{pkgfilename}:"
      # uniq probably isn't necessary, but it can't hurt
      requirees[pkgfilename].sort.uniq.each do |requiree|
        puts "  #{requiree}"
      end
    end
  else
    puts "No other package depends on '#{@action_value}'"
  end
when :query_depends
  tpkg = Tpkg.new(@tpkg_options)
  
  requirements = []
  packages = {}
  tpkg.parse_requests([@action_value], requirements, packages,
    :installed_only => true)
  
  if packages.values.all? {|pkg| pkg.empty?}
    ret_val = 1
    # --qd --quiet doesn't seem like a meaningful combination, so I'm not
    # suppressing this for @quiet
    $stderr.puts "No packages matching '#{@action_value}' installed"
  else
    depends = {}
    packages.each do |name, pkgs|
      pkgs.each do |pkg|
        if pkg[:metadata][:dependencies]
          pkgfilename = pkg[:metadata][:filename]
          if !depends[pkgfilename]
            depends[pkgfilename] = []
          end
          pkg[:metadata][:dependencies].each do |req|
            depends[pkgfilename] << req
          end
        end
      end
    end
    
    if !depends.empty?
      outputs = []
      depends.keys.sort.each do |pkgfilename|
        output = "Package #{pkgfilename} depends on:\n"
        # uniq probably isn't necessary, but it can't hurt
        outs = []
        depends[pkgfilename].sort{|a,b| a[:name]<=>b[:name]}.uniq.each do |req|
          out = "  name: #{req[:name]}\n"
          req.each do |field, value|
            next if field == 'name'
            out << "  #{field}: #{value}\n"
          end
          outs << out
        end
        outputs << output + outs.join("\n")
      end
      print outputs.join("\n")
    else
      puts "Package '#{@action_value}' does not depend on other packages"
    end
  end
when :query_depends_available
  tpkg = Tpkg.new(@tpkg_options)
  requirements = []
  packages = {}
  tpkg.parse_requests([@action_value], requirements, packages)
  availpkgs = []
  packages.each do | name, pkgs |
    availpkgs.concat(pkgs)
  end
  available = availpkgs.select do |pkg|
    pkg[:source] != :native_installed &&
    pkg[:source] != :native_available &&
    pkg[:source] != :currently_installed
  end
  
  if available.empty?
    ret_val = 1
    # --qds --quiet doesn't seem like a meaningful combination, so I'm not
    # suppressing this for @quiet
    $stderr.puts "No packages matching '#{@action_value}' available"
  else
    depends = {}
    available.each do |pkg|
      if pkg[:metadata][:dependencies]
        pkgfilename = pkg[:metadata][:filename]
        if !depends[pkgfilename]
          depends[pkgfilename] = []
        end
        pkg[:metadata][:dependencies].each do |req|
          depends[pkgfilename] << req
        end
      end
    end
    
    if !depends.empty?
      outputs = []
      depends.keys.sort.each do |pkgfilename|
        output = "Package #{pkgfilename} depends on:\n"
        # uniq probably isn't necessary, but it can't hurt
        outs = []
        depends[pkgfilename].sort{|a,b| a[:name]<=>b[:name]}.uniq.each do |req|
          out = "  name: #{req[:name]}\n"
          req.each do |field, value|
            next if field == 'name'
            out << "  #{field}: #{value}\n"
          end
          outs << out
        end
        outputs << output + outs.join("\n")
      end
      print outputs.join("\n")
    else
      puts "Package '#{@action_value}' does not depend on other packages"
    end
  end
when :query_tpkg_metadata
  tpkg = Tpkg.new(@tpkg_options)
  requirements = []
  packages = {}
  tpkg.parse_requests([@action_value], requirements, packages,
    :installed_only => true)
  
  if packages.values.all? {|pkg| pkg.empty?}
    ret_val = 1
    # --qX --quiet doesn't seem like a meaningful combination, so I'm not
    # suppressing this for @quiet
    $stderr.puts "No packages matching '#{@action_value}' installed"
  else
    packages.each do | name, pkgs |
      pkgs.each do | pkg |
        pkgfile = nil
        if pkg[:source] == :currently_installed
          pkgfile = File.join(tpkg.installed_directory, pkg[:metadata][:filename])
        else
          pkgfile = pkg[:source]
        end
        puts Tpkg::extract_tpkg_metadata_file(pkgfile)
      end
    end
  end
when :query_tpkg_metadata_available
  tpkg = Tpkg.new(@tpkg_options)
  requirements = []
  packages = {}
  tpkg.parse_requests([@action_value], requirements, packages)
  availpkgs = []
  packages.each do | name, pkgs |
    availpkgs.concat(pkgs)
  end
  available = availpkgs.select do |pkg|
    pkg[:source] != :native_installed &&
    pkg[:source] != :native_available &&
    pkg[:source] != :currently_installed
  end
  
  if available.empty?
    ret_val = 1
    # --qXs --quiet doesn't seem like a meaningful combination, so I'm not
    # suppressing this for @quiet
    $stderr.puts "No packages matching '#{@action_value}' available"
  else
    downloaddir = Tpkg::tempdir('download')
    available.each do |pkg|
      # FIXME: I've duplicated from the install and upgrade methods this logic
      # to calculate pkgfile, it should be encapsulated in a method
      pkgfile = nil
      if File.file?(pkg[:source])
        pkgfile = pkg[:source]
      elsif File.directory?(pkg[:source])
        pkgfile = File.join(pkg[:source], pkg[:metadata][:filename])
      else
        pkgfile = download(pkg[:source], pkg[:metadata][:filename], downloaddir)
      end
      puts Tpkg::extract_tpkg_metadata_file(pkgfile)
    end
    FileUtils.rm_rf(downloaddir)
  end
when :query_env
  puts "Operating System: #{Tpkg::get_os}"
  puts "Architecture: #{Tpkg::get_arch}"
  puts "Tar: #{Tpkg::find_tar}"
when :query_conf
  # This is somewhat arbitrarily limited to the options read from the
  # tpkg.conf config files.  The reason it exists at all is that it is
  # difficult for users to programatically find out what these will be set to
  # without recreating all of our logic about deciding which config files to
  # read, which order to read them in, what environment variables override the
  # config files, etc.
  tpkg = Tpkg.new(@tpkg_options)
  puts "Base: #{tpkg.base}"
  puts "Sources: #{tpkg.sources.inspect}"
  puts "Report server: #{tpkg.report_server}"
when :query_history
  tpkg = Tpkg.new(@tpkg_options)
  tpkg.installation_history
when :query_version
  puts Tpkg::VERSION
end
exit ret_val
