require 'forwardable' require 'shellwords' require 'socket' class Sh class ForkError < RuntimeError end class ForkFailed < ForkError end class ProcessError < ForkError end class Fork extend Forwardable def_delegators :@status, :exitstatus attr_reader :pid, :status def initialize &exe #@io, s = UNIXSocket.pair @pid = Process.fork do Thread.list.each do |th| th.kill unless Thread.current == th end #STDOUT.reopen s.dup #STDIN.reopen s #STDOUT.close_read #STDIN.close_write yield end raise ForkFailed unless @pid #s.close end def wait _, @status = Process.waitpid2 @pid @status end def kill signal = nil Process.kill signal||9, @pid end def ok?() 0 == @status.exitstatus end def failed?() 0 != @status.exitstatus end end class IOFork < Fork extend Forwardable def_delegators :@io, :close, :closed?, :close_read, :closed_read?, :close_write, :closed_write? def_delegators :@io, :getbyte, :getc, :getch, :getpass, :gets def_delegators :@io, :read, :read_nonblock, :readbyte, :readchar, :readline, :readlines, :readpartial, :sysread def_delegators :@io, :syswrite, :write, :write_nonblock def_delegators :@io, :putc, :puts, :print, :printf def_delegators :@io, :each_line attr_reader :io def initialize &exe @io, s = UNIXSocket.pair r = nil super do @io.close STDOUT.reopen s STDIN.reopen s #STDOUT.close_read #STDIN.close_write r = yield end s.close r end end class Command attr_reader :shell, :command, :args, :opts def initialize shell, command, *args, **opts @shell, @command, @args, @opts = shell, command, args, opts end def fork **opts opts = @opts.merge opts cl = case opts[:mode] when :io then IOFork when Class then opts[:mode] else Fork end cl.new do cmd = @command.to_s $0 = cmd #STDERR.printf "\n|\n| %p\n|\n", cmd: cmd, args: @args if opts.has_key? :stdio stdin, stdout, stderr = *opts[:stdio] stdin ? STDIN.reopen( stdin) : STDIN.close STDOUT.reopen stdout STDERR.reopen stderr if stderr end if opts[:chroot] STDERR.printf "\e[1;34mchroot(%s)\e[0m ", opts[:chroot].to_s Dir.chroot opts[:chroot].to_s end if @shell.opts[:pwd] STDERR.printf "%s ", @shell.opts[:pwd] Dir.chdir @shell.opts[:pwd] end #STDERR.puts "\e[0m#{opts[:return] ? '<=' : '#'} \e[33m#{cmd.shellescape} \e[35m#{usable_args.shelljoin}\e[0m" STDERR.printf "\e[0m%s \e[33m%s \e[35m%s\e[0m\n", opts[:return] ? '<=' : '#', cmd.shellescape, usable_args.shelljoin exec cmd, *usable_args raise ForkFailed, "Cannot execute" end rescue ForkFailed => e raise ForkFailed, "<<#{self}>> cannot be forked" end def usable_args @uargs ||= @args.flatten.map &:to_s end def to_s "#{@command.to_s.shellescape}" + (usable_args.empty? ? '' : " #{usable_args.shelljoin}") end def run **opts, &exe opts = @opts.merge opts case opts[:return] when :lines opts[:mode], exe = :io, lambda( &:readlines) when :string opts[:mode], exe = :io, lambda( &:read) when :line opts[:mode], exe = :io, lambda{|f|f.read.chomp} end r = f = fork **opts begin r = exe.call f if exe rescue Object f.kill raise $! ensure f.close if f.respond_to? :close f.wait end unless opts[:expect_status].nil? or opts[:expect_status] == f.exitstatus raise ProcessError, "«#{self}» exited: #{f.status}" end r end def run! **opts, &exe run expect_status: 0, **opts, &exe end alias call run! alias !@ run! end attr_reader :opts def initialize dir = nil, **opts opts = opts.merge dir: dir if dir @opts = opts.dup (@opts[:commands]||={}).each do |name, exe| define_singleton_method name, &exe end @opts.freeze end def chdir dir Sh.new dir, **opts end alias cd chdir def newopts **opts Sh.new **@opts.merge( opts) end def chroot dir newopts chroot: dir end def return_string newopts mode: nil, return: :string end alias _ return_string def return_lines newopts mode: nil, return: :lines end alias [] return_lines def with_io newopts mode: :io end alias io with_io def system command, *args, immediately: nil, **opts, &exe cmd = Command.new self, command, *args, **@opts.merge( opts) if immediately or @opts[:immediately] or block_given? cmd.run &exe else cmd end end def define_command name, &exe @opts[:commands][name] = exe define_singleton_method name, &exe end def def_system_command name, cmd = nil cmd ||= name define_command name do |*args, **opts, &exe| system cmd, *args, **opts, &exe end #e = <<-EOF # def #{name}(*args, **opts, &exe) # Command.new self, #{cmd.to_s.inspect}, *args, **opts # end #EOF #eval e, nil, __FILE__, __LINE__ - 4 end def def_system_commands *names names.each {|cmd| def_system_command cmd } end def alias_command name, cmd, *args, **opts, &exe opts = @opts.merge opts define_command name do |*as, **os, &e| *as = exe.call( *as) if exe system cmd, *args, *as, **opts, **os end end end #f = Sh::IOFork.new do # STDERR.puts f.inspect # STDOUT.puts "hallo welt" # sleep 5 #end # #STDERR.puts "> #{f.inspect}" #f.each_line do |l| # STDERR.puts "> #{l}" #end #f.wait #exit 1