#!/usr/bin/ruby -w # $Id: deb,v 0.4 2003/08/25 08:00:31 hip Exp $ # # show back/forward package dependencies on a Debian system # http://www.xs4all.nl/~hipster/lib/ruby/depends # # Copyright (C) 2000 Michel van de Ven # 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 # # TODO add sensible getopt handling # TODO add switch to print all packages on which no-one depends (orphans) # require "readline" REVISION = /\d+(\.\d+)*/.match(%Q($Revision: 0.4 $))[0] # MODIFY: location of the dependency cache CACHE = File.expand_path("~/lib/depends.cache") # ---- STATUS = "/var/lib/dpkg/status" def usage puts "Usage: depends PACKAGE [LEVEL]" puts "If package is `ALL', dependencies are printed for all packages." puts "Dependencies are printed LEVEL levels deep (default = 1)" exit end #package = (ARGV.shift || usage) level = (ARGV.shift || 1).to_i level = 1 if level < 1 class Dependency attr_reader :name, :relation, :version def initialize txt if /(.*) \((.*) (.*)\)/.match(txt) @name = $1 @relation = $2 @version = $3 elsif /(.*)/.match(txt) @name = $1 @relation = "=" @version = "*" else $stderr.puts "Dependency: could not parse string" end end end class Package attr_reader :name, :status, :priority, :section, :size, :maintainer, :source, :version, :replaces, :depends, :description, :provides, :recommends, :suggests, :conflicts, :essential, :conffiles def initialize rec @depends = [] @provides = [] rec.split(/\n/).each{ |field| case field when /^Package: (.*)/ @name = $1 when /^Status: (.*) (.*) (.*)/ @status = [$1, $2, $3] when /^Priority: (.*)/ @priority = $1 when /^Section: (.*)/ # NOTE make this an array? split on possible '/' (e.g. non-free) @section = $1 when /^Installed-Size: (.*)/ @size = $1 when /^Maintainer: (.*)/ @maintainer = $1 when /^Source: (.*)/ @source = $1 when /^Version: (.*)/ @version = $1 when /^Provides: (.*)/ @provides = $1.split(/,\s*/) when /^Recommends: (.*)/ @recommends = $1 when /^Suggests: (.*)/ @suggests = $1 when /^Conflicts: (.*)/ @conflicts = $1 when /^Essential: (.*)/ @essential = $1 when /^Conffiles:/ @conffiles = $1 when /^Replaces: (.*)/ @replaces = $1 when /^Depends: (.*)/ # split up 'foo (>= 42), bar, foo | bar' $1.split(/,\s*/).each{ |dep_raw| dep_raw.split(/\s*\|\s*/).each{ |dep| @depends << Dependency.new(dep) } } when /^Description: (.*)/ @description = $1 else # unknown field end } end def <=> other @name <=> other.name end def depends_to_s (@depends || []).collect{ |dep| format("%s %s %s", dep.name, dep.relation, dep.version) }.join(", ") end end class Pool def initialize @pool = [] @indent = 0 npkg = 0 File.foreach(STATUS, "\n\n"){ |rec| pkg = Package.new rec @pool << pkg if pkg.status[2] == "installed" print npkg += 1, "\r" } printf "%d packages available, %d installed\n", npkg, @pool.size end def deps package, level = 1 pkg_esc = Regexp.new(Regexp.escape(package)) @pool.sort.each{ |pkg| if pkg_esc.match(pkg.name) forward_deps pkg.name reverse_deps pkg.name, level indented { provided_deps pkg, level } else pkg.provides.each{ |name| if pkg_esc.match(name) indent "provided by #{pkg.name}:" reverse_deps name, level end } end } end def deps_all level = 1 @pool.sort.each{ |pkg| forward_deps pkg.name reverse_deps pkg.name, level indented { provided_deps pkg, level } } end def names @pool.collect{ |pkg| pkg.name }.sort end private def forward_deps package puts package + " -> " + @pool.find{ |pkg| pkg.name == package }.depends_to_s end def reverse_deps package, level return if level == 0 # select all packages that have `package' in their dependency list pkg_esc = Regexp.escape(package) deps = @pool.select{ |pkg| pkg.depends != nil and pkg.depends.find { |d| d.name == package } } indent "#{package} <- (" indented { deps.sort.each{ |pkg| if level == 1 indent pkg.name else reverse_deps pkg.name, level - 1 end } } indent ")" end def provided_deps package, level package.provides.each{ |name| indent "provided by #{package.name}:" reverse_deps name, level } end def indent txt print " " * @indent, txt, "\n" end def indented @indent += 1 yield @indent -= 1 end end class Cache attr :pool def initialize # persist/restore object pool if test(?f, CACHE) stime = File.stat(STATUS).mtime ctime = File.stat(CACHE).mtime if ctime < stime puts "cache is outdated, refreshing..." update_cache else load_cache end else puts "cache does not exist, creating..." update_cache end end private def update_cache @pool = Pool.new File.open(CACHE, "w"){ |f| Marshal.dump(@pool, f) } end def load_cache File.open(CACHE, "r"){ |f| @pool = Marshal.load(f) } end end class Executor def initialize cache @cmds = Hash.new @help = [] @cache = cache @user = ENV['USER'] private_methods.grep(/^cmd_/).each{ |meth| help = instance_eval("#{meth}(nil,true)") @cmds[help[0]] = meth @help << "#{help[0]} - #{help[1]}" } end def run line command, package = line.split(/ /) if @cmds.has_key?(command) instance_eval "#{@cmds[command]}('#{package}',false)" else puts "Sorry #@user, I don't understand that." end end private def cmd_help pkg, help return ["?", "display this help (duh)"] if help puts @help.sort.join("\n") end def cmd_dependencies pkg, help return ["d", "display (reverse) dependencies for package"] if help @cache.pool.deps pkg end def cmd_files pkg, help return ["l", "list package files"] if help system("dpkg -L #{pkg} | sort | less") end def cmd_status pkg, help return ["s", "display package status"] if help system("dpkg -s #{pkg}") end def cmd_findfile pkg, help return ["f", "find file in packages"] if help system("dpkg -S #{pkg} | sort | less") end def cmd_exit pkg, help return ["q", "quit (or hit ^D)"] if help exit end def cmd_showpkg pkg, help return ["c", "show apt-cache dependencies"] if help system("apt-cache showpkg #{pkg}") end end cache = Cache.new executor = Executor.new(cache) pkgs = cache.pool.names Readline.completion_proc = proc { |prefix| prefix = Regexp.escape(prefix) pkgs.select{ |name| /^#{prefix}/.match(name) } } Readline.completion_case_fold = false # TODO add cmdline param handling, use same syntax as interactive mode # go into interactive mode if no params given puts "deb #{REVISION}" puts "? for help. TAB for package name completion." loop do line = Readline.readline("> ") break unless line executor.run(line) Readline::HISTORY.push line end