truckle/truckle

516 lines
13 KiB
Ruby
Executable File

#!/usr/bin/env ruby
# -*- encoding : utf-8 -*-
#
# Author 2013 Denis Knauf
#
# 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 3 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, see <http://www.gnu.org/licenses/>.
require 'shellwords'
require 'getoptlong'
begin
require 'irb-pager'
rescue LoadError
STDERR.puts <<EOF
Loading IRB::Pager failed. Please install it first:
sudo gem install irb-pager
EOF
raise
end
class GetoptLong
def argv=( v) @argv = v end
def argv() @argv end
def ARGV=( v) @argv = v end
def ARGV() @argv end
end
class Commands < Hash
class CommandError < Exception
end
class ExpectingCommandError < CommandError
def initialize *args
args = ['Command expected'] if args.empty?
super *args
end
end
class UnknownCommandError < CommandError
def initialize *args
args = ['This Command i do not know'] if args.empty?
super *args
end
end
attr_accessor :exe, :prefix, :fallback_cmd
def self.arg_unshift arg, args
arg = arg.is_a?(Array) ? arg : [arg]
i = 1+args.index {|x|/^[^-]/=~x}
args[0...i] + arg + args[i..-1]
end
def self.new prefix, exe
r = super()
r.exe, r.prefix = exe, prefix
r.on {|cmd, *argv| raise UnknownCommandError, 'Unknown Command: %s' % cmd }
r
end
def sym_name name
name.to_s.to_sym
end
def on *names, &run
options = names.last.is_a?(Hash) ? names.pop.dup : {}
names = names.flatten.compact
if names.empty?
@fallback_cmd = run
else
names.each {|name| self[sym_name name] = run }
end
end
def cmd argv
if @prefix == @exe
raise ExpectingCommandError if argv.empty?
[argv[0].to_sym, *argv[1..-1]]
else
@exe =~ /^(?:#{Regexp.escape @prefix}-)?(.*)$/
[$1.to_sym, *argv]
end
end
def each all=nil, &block
if not block_given?
Enumerator.new self, :each, all
elsif all
super(&block)
else
super() {|k,v| yield k,v unless /^-/ =~ k.to_s }
end
end
def run *argv
c, *argv = self.cmd( argv)
(self[c] || @fallback_cmd).call c, *argv
rescue CommandError
STDERR.puts $!.message #, $!.backtrace
exit 1
end
alias call run
def to_proc
method(:call).to_proc
end
end
# RunCave
# =======
#
# Prepare cave-commands.
class RunCave
class CommandExpected <Exception
end
class ResumeFileExpected <Exception
end
attr_accessor :preargs, :cmd, :args, :log_level
attr_reader :dummy
attr_writer :resume_file
def initialize preargs = nil, cmd = nil, args = nil
@resume_file, @preargs, @cmd, @args = nil, preargs || [], cmd || [], args || []
@colored = @dummy = nil
end
def can_be_dummy
@dummy = true unless false == @dummy
self
end
def dummy?() @dummy end
def dummy!() @dummy = true; self end
def dummy=(v) @dummy = !!v; self end
def can_be_colored
@colored = true unless false == @colored
self
end
def colored?() @colored end
def colored!() @colored = true; self end
def colored=(v) @colored = !!v; self end
def prepare_colored
colored? ? ['--colour', 'yes'] : []
end
def prepare_log_level
@log_level ? ['--log-level', @log_level] : []
end
def prepare
raise CommandExpected, "Set #{self}.cmd = yourcommand." if @cmd.nil? or @cmd.empty?
[
:cave, prepare_colored, prepare_log_level, @preargs, @cmd, @args
].flatten.select{|x|x}.map {|x| x.to_s }
end
def run
a = prepare
if dummy?
puts a.shelljoin
0
else
begin
Kernel.system *a
$? && $?.exitstatus
rescue Interrupt
nil
end
end
end
def this cmd, *args
@cmd, @args = cmd, args
self
end
alias method_missing this
def resume_file f
if f.nil?
@resume_file
else
@resume_file = f
self
end
end
def run_exit
exit run || 130
end
alias call run_exit
end
# Shelled
# =======
#
# Some helper functions like for sudo and pager
class Shelled
class ExecutionError < Exception
end
class Caller
attr_accessor :pager, :argv
def initialize *argv
@argv, @pager = argv, false
end
# Calls exe while IRB::Pager redirect output to your PAGER.
def pager *args, &exe
if @nopager
exe.call *args
else
IRB::Pager::pager &lambda{ exe.call *args }
end
end
# Restarts, if not @nosudo and not root yet, your Truckle as root
def sudo *argv, &exe
if @nosudo or 0 == Process.egid
exe.call *argv
else
argv = [:sudo, @argv, argv].flatten.select{|x|x}.map {|x| x.to_s }
Kernel.exec *argv
raise ExecutionError, "Can't exec #{argv.shelljoin}"
end
end
end
attr_reader :caller
attr_accessor :_stack_
def argv() @caller.argv end
def initialize *argv
@caller = Caller.new *argv
@_stack_ = []
end
def to_proc() method(:call).to_proc end
def _push_ exe = nil, &block
n = dup
n._stack_ = @_stack_.dup
n._stack_.push block || exe
n
end
def method_missing meth, &block
n = _push_ {|*as, &b| @caller.send( meth, *as, &b) }
n = n._push_ block if block_given?
n
end
def call *args
@_stack_.reverse.inject {|s,b| lambda {|*as| b.call *as, &s } }.call *args
end
end
# Truckle
# =======
#
# cave-wrapper
class Truckle
def markdown_format t, colored = nil
if colored
t = t.gsub( /^\t+/) {|indent| ' '*indent.length}
t.gsub! /^([^\n]+)\n===+/m, "\n«««««« \033[1;4;33m\\1\033[0m »»»»»»"
t.gsub! /^([^\n]+)\n---+/m, "\n««« \033[1;33m\\1\033[0m »»»"
t.gsub! /^ (.*?)( *#.*)$/, " \033[1;44m\\1\033[0m\\2"
t.gsub! /`([^`]+)`/, "\033[1;44m \\1 \033[0m"
t.gsub! /\*([^*]+)\*/, "\033[1m\\1\033[0m"
end
t = t[1..-1] while t.start_with? "\n"
t
end
def initialize argv0
ENV['LESS'] = "-FLRBb-1 #{ENV['LESS']}"
ENV['CAVE'] ||= 'truckle'
@argv0 = argv0
@exename = File.basename argv0
# initialize our program.
@cave = RunCave.new
@cmds = Commands.new 'truckle', @exename
@shell = Shelled.new
prepare_commands
end
def helptext
cmd = @cmds.prefix
markdown_format <<EOF, @cave.colored?
Usage
=====
#{cmd} *PREARGS Command *ARGS
Arguments
=========
Precede the command, it's possible to set some arguments.
These Arguments really must precede the command.
- `-C|--list-commands` will list all possible commands and exit.
- `-h|--help` will print this help and exit.
- `-n|--dummy` will print the shellcode, which will be executed, if not --dummy.
- `-s|--sudo on|off|auto` will use (or not) sudo if you are not root.
- `-c|--color|--colour on|off|auto` will control the colorfull output of `cave`.
- `-L|--log-level LOGLEVEL` will control the log-level of `cave`.
- `-p|--pager PAGER` will change the pager to display.
Instead of on|off you can use 0|true or 1|false too. Default is everytime auto.
Environment
===========
- `DUMMY=1` will print the shellcode, which will be executed, like `--dummy`.
- `NOSUDO=1` will prevent of using sudo, if you are not root, like `--sudo off`.
- `PAGER=more` will change the pager to more, instead of less, like `--pager more`.
Commands
========
Most commands are like by cave.
- `search` is pagered.
- `show` is pagered.
- `resolve` is pagered and resumable. Do *not* use `-x`! Use `resume`.
- `install` is resumable and like `cave resolve -x`.
- `upgrade` is pagered and resumable and like `cave resolve -c world`. Do *not* use `-x`! Use `resume`.
- `remove` is pagered, resumable and like `cave uninstall`. Do *not* use `-x`! Use `resume`.
- `uninstall` is resumable and like `cave uninstall -x`.
- `fix-linkage` is pagered and resumable.
- `do` and `resume` are resumable and will resume or execute the last command.
- `retry` are resumable, will resume or execute the last command and will retry the last failed job.
Resumable
=========
Resume will be defined via environment `CAVE_*_OPTIONS='--resume-file PATH'`, which is provided by `/etc/profile.d/cave.sh`.
Like cave but different
=======================
Some commands will be displayed by a pager, so you can scroll up and down like
truckle resolve WHAT # cave -cy resolve WHAT | less -r
truckle remove WHAT # cave -cy uninstall WHAT | less -r
Some commands are not displayed by a pager, but will execute:
truckle install WHAT # cave -cy resolve -x WHAT
truckle uninstall WHAT # cave -cy uninstall -x WHAT
«do», «resume» and «retry» are special, to execute the last command:
truckle resume # | do # cave resume
trdo # | tresume # shortcuts
truckle retry # like resume but --retry-failed --retry-skipped
tretry # shortcut
EOF
end
def prepare_commands
cmds, cave, s = @cmds, @cave, @shell
# default: simple use cave
cmds.on &s.sudo {|*args| cave.this(*args).() }
cmds.on :help, '-h', '--help', &s.pager { STDOUT.puts helptext }
cmds.on *%w[configs print-package-path sync], &s.sudo {|*args| cave.this(*args).() }
cmds.on *%w[contents info print-unused-distfiles search show],
&s.sudo.pager {|*args| cave.this(*args).() }
cmds.on *%w[resolve fix-linkage], &s.sudo.pager {|*args| cave.this(*args).() }
cmds.on :remove, &s.sudo.pager {|cmd, *args| cave.uninstall(*args).()}
cmds.on :upgrade, &s.sudo.pager {|*args| cave.this( *args).() }
cmds.on :install, &s.sudo {|cmd, *argv| cave.resolve( '-x', *argv).() }
cmds.on :uninstall, &s.sudo {|cmd, *argv| cave.uninstall( '-x', *argv).() }
cmds.on :do, :resume, &s.sudo {|cmd, *args| cave.resume( *args).() }
cmds.on :retry, &s.sudo {|*args| cave.this( *args).() }
cmds.on '--list-commands', &s.pager { puts cmds.map {|k,v| k } }
end
def setup_params options
on_off_auto = lambda do |arg|
case arg
when 'on', '1', 'true', 't' then true
when 'off', '0', 'false', 'f' then false
end
end
opts = GetoptLong.new(
[ '-h', '--help', GetoptLong::NO_ARGUMENT ],
[ '-C', '--list-commands', GetoptLong::NO_ARGUMENT ],
[ '-n', '--dummy', GetoptLong::NO_ARGUMENT ],
[ '-p', '--pager', GetoptLong::REQUIRED_ARGUMENT ],
[ '-c', '--color', '--colour', GetoptLong::REQUIRED_ARGUMENT ],
[ '-L', '--log-level', GetoptLong::REQUIRED_ARGUMENT ],
[ '-s', '--sudo', GetoptLong::REQUIRED_ARGUMENT ]
)
opts.ordering = GetoptLong::REQUIRE_ORDER
opts.each do |opt, arg|
case opt
when '-C' then options[:cmd] = '--list-commands'
when '-h' then options[:cmd] = 'help'
when '-n' then options[:dummy] = true
when '-p' then options[:pager] = on_off_auto[arg]
when '-c' then options[:color] = on_off_auto[arg]
when '-L' then options[:log_level] = arg
when '-s' then options[:sudo] = on_off_auto[arg]
else opts.terminate
end
end
options
end
def setup_env options
options[:dummy] = true if %w[1 true yes].include?( ENV['DUMMY'].to_s.downcase)
options[:sudo] = false if %w[1 true yes].include?( ENV['NOSUDO'].to_s.downcase)
options[:tty] = true if STDOUT.tty?
end
def setup_run options
on_off_auto = lambda {|x| nil == x ? true : x }
ARGV.unshift options[:cmd] if options[:cmd]
@cave.dummy! if options[:dummy]
@cave.log_level if options[:log_level]
@cave.can_be_colored if on_off_auto.( options[:color]) and options[:tty]
@nopager = true unless on_off_auto.( options[:pager]) or options[:tty]
@nosudo = true unless on_off_auto.( options[:sudo])
end
def setup_recall options
on_off_auto = lambda {|n,x|
case x
when true then [n, 'on']
when false then [n, 'off']
end
}
param = lambda {|n,x| n if x }
@params = [
param.( '--dummy', options[:dummy]),
param.( ['--log-level', options[:log_level]], options[:log_level]),
on_off_auto.( '--sudo', options[:sudo]),
on_off_auto.( '--color', options[:color]),
on_off_auto.( '--pager', options[:pager])
].flatten.select{|x|x}.map(&:to_s)
@shell.argv.push File.join( File.dirname( @argv0), @cmds.prefix), *@params
end
def run
options = {}
setup_env options
setup_params options
setup_recall options
setup_run options
@cmds.run *ARGV
end
def self.run argv0
self.new( argv0).run
end
end
class Shortcut < Hash
attr_reader :argv0
def initialize argv0
@argv0 = argv0
prepare
end
def on cmd, &exe
self[cmd.to_s.to_sym] = exe
end
def prepare
on( :trdo) { exec :truckle, :do, *ARGV }
on( :tresume) { exec :truckle, :resume, *ARGV }
on( :tretry) { exec :truckle, :retry, *ARGV }
end
def exec exe, *args
exe = File.join File.dirname( @argv0), exe.to_s
Kernel.exec exe, *args.flatten.select{|x|x}.map(&:to_s)
end
def run
exe = File.basename @argv0
exe = self[exe.to_sym]
exe.call if exe
end
alias call run
def self.run argv0
self.new( argv0).run
end
end
if __FILE__ == $0
Shortcut.run $0
Truckle.run $0
end