1# 2# shell/command-controller.rb - 3# $Release Version: 0.7 $ 4# $Revision: 38201 $ 5# by Keiju ISHITSUKA(keiju@ruby-lang.org) 6# 7# -- 8# 9# 10# 11 12require "e2mmap" 13require "thread" 14 15require "shell/error" 16require "shell/filter" 17require "shell/system-command" 18require "shell/builtin-command" 19 20class Shell 21 # In order to execute a command on your OS, you need to define it as a 22 # Shell method. 23 # 24 # Alternatively, you can execute any command via 25 # Shell::CommandProcessor#system even if it is not defined. 26 class CommandProcessor 27# include Error 28 29 # 30 # initialize of Shell and related classes. 31 # 32 m = [:initialize, :expand_path] 33 if Object.methods.first.kind_of?(String) 34 NoDelegateMethods = m.collect{|x| x.id2name} 35 else 36 NoDelegateMethods = m 37 end 38 39 def self.initialize 40 41 install_builtin_commands 42 43 # define CommandProcessor#methods to Shell#methods and Filter#methods 44 for m in CommandProcessor.instance_methods(false) - NoDelegateMethods 45 add_delegate_command_to_shell(m) 46 end 47 48 def self.method_added(id) 49 add_delegate_command_to_shell(id) 50 end 51 end 52 53 # 54 # include run file. 55 # 56 def self.run_config 57 begin 58 load File.expand_path("~/.rb_shell") if ENV.key?("HOME") 59 rescue LoadError, Errno::ENOENT 60 rescue 61 print "load error: #{rc}\n" 62 print $!.class, ": ", $!, "\n" 63 for err in $@[0, $@.size - 2] 64 print "\t", err, "\n" 65 end 66 end 67 end 68 69 def initialize(shell) 70 @shell = shell 71 @system_commands = {} 72 end 73 74 # 75 # CommandProcessor#expand_path(path) 76 # path: String 77 # return: String 78 # returns the absolute path for <path> 79 # 80 def expand_path(path) 81 @shell.expand_path(path) 82 end 83 84 # call-seq: 85 # foreach(path, record_separator) -> Enumerator 86 # foreach(path, record_separator) { block } 87 # 88 # See IO.foreach when +path+ is a file. 89 # 90 # See Dir.foreach when +path+ is a directory. 91 # 92 def foreach(path = nil, *rs) 93 path = "." unless path 94 path = expand_path(path) 95 96 if File.directory?(path) 97 Dir.foreach(path){|fn| yield fn} 98 else 99 IO.foreach(path, *rs){|l| yield l} 100 end 101 end 102 103 # call-seq: 104 # open(path, mode, permissions) -> Enumerator 105 # open(path, mode, permissions) { block } 106 # 107 # See IO.open when +path+ is a file. 108 # 109 # See Dir.open when +path+ is a directory. 110 # 111 def open(path, mode = nil, perm = 0666, &b) 112 path = expand_path(path) 113 if File.directory?(path) 114 Dir.open(path, &b) 115 else 116 if @shell.umask 117 f = File.open(path, mode, perm) 118 File.chmod(perm & ~@shell.umask, path) 119 if block_given? 120 f.each(&b) 121 end 122 f 123 else 124 f = File.open(path, mode, perm, &b) 125 end 126 end 127 end 128 # public :open 129 130 # call-seq: 131 # unlink(path) 132 # 133 # See IO.unlink when +path+ is a file. 134 # 135 # See Dir.unlink when +path+ is a directory. 136 # 137 def unlink(path) 138 @shell.check_point 139 140 path = expand_path(path) 141 if File.directory?(path) 142 Dir.unlink(path) 143 else 144 IO.unlink(path) 145 end 146 Void.new(@shell) 147 end 148 149 # See Shell::CommandProcessor#test 150 alias top_level_test test 151 # call-seq: 152 # test(command, file1, file2) -> true or false 153 # [command, file1, file2] -> true or false 154 # 155 # Tests if the given +command+ exists in +file1+, or optionally +file2+. 156 # 157 # Example: 158 # sh[?e, "foo"] 159 # sh[:e, "foo"] 160 # sh["e", "foo"] 161 # sh[:exists?, "foo"] 162 # sh["exists?", "foo"] 163 # 164 def test(command, file1, file2=nil) 165 file1 = expand_path(file1) 166 file2 = expand_path(file2) if file2 167 command = command.id2name if command.kind_of?(Symbol) 168 169 case command 170 when Integer 171 if file2 172 top_level_test(command, file1, file2) 173 else 174 top_level_test(command, file1) 175 end 176 when String 177 if command.size == 1 178 if file2 179 top_level_test(command, file1, file2) 180 else 181 top_level_test(command, file1) 182 end 183 else 184 if file2 185 FileTest.send(command, file1, file2) 186 else 187 FileTest.send(command, file1) 188 end 189 end 190 end 191 end 192 # See Shell::CommandProcessor#test 193 alias [] test 194 195 # call-seq: 196 # mkdir(path) 197 # 198 # Same as Dir.mkdir, except multiple directories are allowed. 199 def mkdir(*path) 200 @shell.check_point 201 notify("mkdir #{path.join(' ')}") 202 203 perm = nil 204 if path.last.kind_of?(Integer) 205 perm = path.pop 206 end 207 for dir in path 208 d = expand_path(dir) 209 if perm 210 Dir.mkdir(d, perm) 211 else 212 Dir.mkdir(d) 213 end 214 File.chmod(d, 0666 & ~@shell.umask) if @shell.umask 215 end 216 Void.new(@shell) 217 end 218 219 # call-seq: 220 # rmdir(path) 221 # 222 # Same as Dir.rmdir, except multiple directories are allowed. 223 def rmdir(*path) 224 @shell.check_point 225 notify("rmdir #{path.join(' ')}") 226 227 for dir in path 228 Dir.rmdir(expand_path(dir)) 229 end 230 Void.new(@shell) 231 end 232 233 # call-seq: 234 # system(command, *options) -> SystemCommand 235 # 236 # Executes the given +command+ with the +options+ parameter. 237 # 238 # Example: 239 # print sh.system("ls", "-l") 240 # sh.system("ls", "-l") | sh.head > STDOUT 241 # 242 def system(command, *opts) 243 if opts.empty? 244 if command =~ /\*|\?|\{|\}|\[|\]|<|>|\(|\)|~|&|\||\\|\$|;|'|`|"|\n/ 245 return SystemCommand.new(@shell, find_system_command("sh"), "-c", command) 246 else 247 command, *opts = command.split(/\s+/) 248 end 249 end 250 SystemCommand.new(@shell, find_system_command(command), *opts) 251 end 252 253 # call-seq: 254 # rehash 255 # 256 # Clears the command hash table. 257 def rehash 258 @system_commands = {} 259 end 260 261 def check_point # :nodoc: 262 @shell.process_controller.wait_all_jobs_execution 263 end 264 alias finish_all_jobs check_point # :nodoc: 265 266 # call-seq: 267 # transact { block } 268 # 269 # Executes a block as self 270 # 271 # Example: 272 # sh.transact { system("ls", "-l") | head > STDOUT } 273 def transact(&block) 274 begin 275 @shell.instance_eval(&block) 276 ensure 277 check_point 278 end 279 end 280 281 # call-seq: 282 # out(device) { block } 283 # 284 # Calls <code>device.print</code> on the result passing the _block_ to 285 # #transact 286 def out(dev = STDOUT, &block) 287 dev.print transact(&block) 288 end 289 290 # call-seq: 291 # echo(*strings) -> Echo 292 # 293 # Returns a Echo object, for the given +strings+ 294 def echo(*strings) 295 Echo.new(@shell, *strings) 296 end 297 298 # call-seq: 299 # cat(*filename) -> Cat 300 # 301 # Returns a Cat object, for the given +filenames+ 302 def cat(*filenames) 303 Cat.new(@shell, *filenames) 304 end 305 306 # def sort(*filenames) 307 # Sort.new(self, *filenames) 308 # end 309 # call-seq: 310 # glob(pattern) -> Glob 311 # 312 # Returns a Glob filter object, with the given +pattern+ object 313 def glob(pattern) 314 Glob.new(@shell, pattern) 315 end 316 317 def append(to, filter) 318 case to 319 when String 320 AppendFile.new(@shell, to, filter) 321 when IO 322 AppendIO.new(@shell, to, filter) 323 else 324 Shell.Fail Error::CantApplyMethod, "append", to.class 325 end 326 end 327 328 # call-seq: 329 # tee(file) -> Tee 330 # 331 # Returns a Tee filter object, with the given +file+ command 332 def tee(file) 333 Tee.new(@shell, file) 334 end 335 336 # call-seq: 337 # concat(*jobs) -> Concat 338 # 339 # Returns a Concat object, for the given +jobs+ 340 def concat(*jobs) 341 Concat.new(@shell, *jobs) 342 end 343 344 # %pwd, %cwd -> @pwd 345 def notify(*opts) 346 Shell.notify(*opts) {|mes| 347 yield mes if iterator? 348 349 mes.gsub!("%pwd", "#{@cwd}") 350 mes.gsub!("%cwd", "#{@cwd}") 351 } 352 end 353 354 # 355 # private functions 356 # 357 def find_system_command(command) 358 return command if /^\// =~ command 359 case path = @system_commands[command] 360 when String 361 if exists?(path) 362 return path 363 else 364 Shell.Fail Error::CommandNotFound, command 365 end 366 when false 367 Shell.Fail Error::CommandNotFound, command 368 end 369 370 for p in @shell.system_path 371 path = join(p, command) 372 if FileTest.exist?(path) 373 @system_commands[command] = path 374 return path 375 end 376 end 377 @system_commands[command] = false 378 Shell.Fail Error::CommandNotFound, command 379 end 380 381 # call-seq: 382 # def_system_command(command, path) -> Shell::SystemCommand 383 # 384 # Defines a command, registering +path+ as a Shell method for the given 385 # +command+. 386 # 387 # Shell::CommandProcessor.def_system_command "ls" 388 # #=> Defines ls. 389 # 390 # Shell::CommandProcessor.def_system_command "sys_sort", "sort" 391 # #=> Defines sys_sort as sort 392 # 393 def self.def_system_command(command, path = command) 394 begin 395 eval((d = %Q[def #{command}(*opts) 396 SystemCommand.new(@shell, '#{path}', *opts) 397 end]), nil, __FILE__, __LINE__ - 1) 398 rescue SyntaxError 399 Shell.notify "warn: Can't define #{command} path: #{path}." 400 end 401 Shell.notify "Define #{command} path: #{path}.", Shell.debug? 402 Shell.notify("Definition of #{command}: ", d, 403 Shell.debug.kind_of?(Integer) && Shell.debug > 1) 404 end 405 406 # call-seq: 407 # undef_system_command(command) -> self 408 # 409 # Undefines a command 410 def self.undef_system_command(command) 411 command = command.id2name if command.kind_of?(Symbol) 412 remove_method(command) 413 Shell.module_eval{remove_method(command)} 414 Filter.module_eval{remove_method(command)} 415 self 416 end 417 418 @alias_map = {} 419 # Returns a list of aliased commands 420 def self.alias_map 421 @alias_map 422 end 423 # call-seq: 424 # alias_command(alias, command, *options) -> self 425 # 426 # Creates a command alias at the given +alias+ for the given +command+, 427 # passing any +options+ along with it. 428 # 429 # Shell::CommandProcessor.alias_command "lsC", "ls", "-CBF", "--show-control-chars" 430 # Shell::CommandProcessor.alias_command("lsC", "ls"){|*opts| ["-CBF", "--show-control-chars", *opts]} 431 # 432 def self.alias_command(ali, command, *opts) 433 ali = ali.id2name if ali.kind_of?(Symbol) 434 command = command.id2name if command.kind_of?(Symbol) 435 begin 436 if iterator? 437 @alias_map[ali.intern] = proc 438 439 eval((d = %Q[def #{ali}(*opts) 440 @shell.__send__(:#{command}, 441 *(CommandProcessor.alias_map[:#{ali}].call *opts)) 442 end]), nil, __FILE__, __LINE__ - 1) 443 444 else 445 args = opts.collect{|opt| '"' + opt + '"'}.join(",") 446 eval((d = %Q[def #{ali}(*opts) 447 @shell.__send__(:#{command}, #{args}, *opts) 448 end]), nil, __FILE__, __LINE__ - 1) 449 end 450 rescue SyntaxError 451 Shell.notify "warn: Can't alias #{ali} command: #{command}." 452 Shell.notify("Definition of #{ali}: ", d) 453 raise 454 end 455 Shell.notify "Define #{ali} command: #{command}.", Shell.debug? 456 Shell.notify("Definition of #{ali}: ", d, 457 Shell.debug.kind_of?(Integer) && Shell.debug > 1) 458 self 459 end 460 461 # call-seq: 462 # unalias_command(alias) -> self 463 # 464 # Unaliases the given +alias+ command. 465 def self.unalias_command(ali) 466 ali = ali.id2name if ali.kind_of?(Symbol) 467 @alias_map.delete ali.intern 468 undef_system_command(ali) 469 end 470 471 # :nodoc: 472 # 473 # Delegates File and FileTest methods into Shell, including the following 474 # commands: 475 # 476 # * Shell#blockdev?(file) 477 # * Shell#chardev?(file) 478 # * Shell#directory?(file) 479 # * Shell#executable?(file) 480 # * Shell#executable_real?(file) 481 # * Shell#exist?(file)/Shell#exists?(file) 482 # * Shell#file?(file) 483 # * Shell#grpowned?(file) 484 # * Shell#owned?(file) 485 # * Shell#pipe?(file) 486 # * Shell#readable?(file) 487 # * Shell#readable_real?(file) 488 # * Shell#setgid?(file) 489 # * Shell#setuid?(file) 490 # * Shell#size(file)/Shell#size?(file) 491 # * Shell#socket?(file) 492 # * Shell#sticky?(file) 493 # * Shell#symlink?(file) 494 # * Shell#writable?(file) 495 # * Shell#writable_real?(file) 496 # * Shell#zero?(file) 497 # * Shell#syscopy(filename_from, filename_to) 498 # * Shell#copy(filename_from, filename_to) 499 # * Shell#move(filename_from, filename_to) 500 # * Shell#compare(filename_from, filename_to) 501 # * Shell#safe_unlink(*filenames) 502 # * Shell#makedirs(*filenames) 503 # * Shell#install(filename_from, filename_to, mode) 504 # 505 # And also, there are some aliases for convenience: 506 # 507 # * Shell#cmp <- Shell#compare 508 # * Shell#mv <- Shell#move 509 # * Shell#cp <- Shell#copy 510 # * Shell#rm_f <- Shell#safe_unlink 511 # * Shell#mkpath <- Shell#makedirs 512 # 513 def self.def_builtin_commands(delegation_class, command_specs) 514 for meth, args in command_specs 515 arg_str = args.collect{|arg| arg.downcase}.join(", ") 516 call_arg_str = args.collect{ 517 |arg| 518 case arg 519 when /^(FILENAME.*)$/ 520 format("expand_path(%s)", $1.downcase) 521 when /^(\*FILENAME.*)$/ 522 # \*FILENAME* -> filenames.collect{|fn| expand_path(fn)}.join(", ") 523 $1.downcase + '.collect{|fn| expand_path(fn)}' 524 else 525 arg 526 end 527 }.join(", ") 528 d = %Q[def #{meth}(#{arg_str}) 529 #{delegation_class}.#{meth}(#{call_arg_str}) 530 end] 531 Shell.notify "Define #{meth}(#{arg_str})", Shell.debug? 532 Shell.notify("Definition of #{meth}: ", d, 533 Shell.debug.kind_of?(Integer) && Shell.debug > 1) 534 eval d 535 end 536 end 537 538 # call-seq: 539 # install_system_commands(prefix = "sys_") 540 # 541 # Defines all commands in the Shell.default_system_path as Shell method, 542 # all with given +prefix+ appended to their names. 543 # 544 # Any invalid character names are converted to +_+, and errors are passed 545 # to Shell.notify. 546 # 547 # Methods already defined are skipped. 548 def self.install_system_commands(pre = "sys_") 549 defined_meth = {} 550 for m in Shell.methods 551 defined_meth[m] = true 552 end 553 sh = Shell.new 554 for path in Shell.default_system_path 555 next unless sh.directory? path 556 sh.cd path 557 sh.foreach do 558 |cn| 559 if !defined_meth[pre + cn] && sh.file?(cn) && sh.executable?(cn) 560 command = (pre + cn).gsub(/\W/, "_").sub(/^([0-9])/, '_\1') 561 begin 562 def_system_command(command, sh.expand_path(cn)) 563 rescue 564 Shell.notify "warn: Can't define #{command} path: #{cn}" 565 end 566 defined_meth[command] = command 567 end 568 end 569 end 570 end 571 572 def self.add_delegate_command_to_shell(id) # :nodoc: 573 id = id.intern if id.kind_of?(String) 574 name = id.id2name 575 if Shell.method_defined?(id) 576 Shell.notify "warn: override definition of Shell##{name}." 577 Shell.notify "warn: alias Shell##{name} to Shell##{name}_org.\n" 578 Shell.module_eval "alias #{name}_org #{name}" 579 end 580 Shell.notify "method added: Shell##{name}.", Shell.debug? 581 Shell.module_eval(%Q[def #{name}(*args, &block) 582 begin 583 @command_processor.__send__(:#{name}, *args, &block) 584 rescue Exception 585 $@.delete_if{|s| /:in `__getobj__'$/ =~ s} #` 586 $@.delete_if{|s| /^\\(eval\\):/ =~ s} 587 raise 588 end 589 end], __FILE__, __LINE__) 590 591 if Shell::Filter.method_defined?(id) 592 Shell.notify "warn: override definition of Shell::Filter##{name}." 593 Shell.notify "warn: alias Shell##{name} to Shell::Filter##{name}_org." 594 Filter.module_eval "alias #{name}_org #{name}" 595 end 596 Shell.notify "method added: Shell::Filter##{name}.", Shell.debug? 597 Filter.module_eval(%Q[def #{name}(*args, &block) 598 begin 599 self | @shell.__send__(:#{name}, *args, &block) 600 rescue Exception 601 $@.delete_if{|s| /:in `__getobj__'$/ =~ s} #` 602 $@.delete_if{|s| /^\\(eval\\):/ =~ s} 603 raise 604 end 605 end], __FILE__, __LINE__) 606 end 607 608 # Delegates File methods into Shell, including the following commands: 609 # 610 # * Shell#atime(file) 611 # * Shell#basename(file, *opt) 612 # * Shell#chmod(mode, *files) 613 # * Shell#chown(owner, group, *file) 614 # * Shell#ctime(file) 615 # * Shell#delete(*file) 616 # * Shell#dirname(file) 617 # * Shell#ftype(file) 618 # * Shell#join(*file) 619 # * Shell#link(file_from, file_to) 620 # * Shell#lstat(file) 621 # * Shell#mtime(file) 622 # * Shell#readlink(file) 623 # * Shell#rename(file_from, file_to) 624 # * Shell#split(file) 625 # * Shell#stat(file) 626 # * Shell#symlink(file_from, file_to) 627 # * Shell#truncate(file, length) 628 # * Shell#utime(atime, mtime, *file) 629 # 630 def self.install_builtin_commands 631 # method related File. 632 # (exclude open/foreach/unlink) 633 normal_delegation_file_methods = [ 634 ["atime", ["FILENAME"]], 635 ["basename", ["fn", "*opts"]], 636 ["chmod", ["mode", "*FILENAMES"]], 637 ["chown", ["owner", "group", "*FILENAME"]], 638 ["ctime", ["FILENAMES"]], 639 ["delete", ["*FILENAMES"]], 640 ["dirname", ["FILENAME"]], 641 ["ftype", ["FILENAME"]], 642 ["join", ["*items"]], 643 ["link", ["FILENAME_O", "FILENAME_N"]], 644 ["lstat", ["FILENAME"]], 645 ["mtime", ["FILENAME"]], 646 ["readlink", ["FILENAME"]], 647 ["rename", ["FILENAME_FROM", "FILENAME_TO"]], 648 # ["size", ["FILENAME"]], 649 ["split", ["pathname"]], 650 ["stat", ["FILENAME"]], 651 ["symlink", ["FILENAME_O", "FILENAME_N"]], 652 ["truncate", ["FILENAME", "length"]], 653 ["utime", ["atime", "mtime", "*FILENAMES"]]] 654 655 def_builtin_commands(File, normal_delegation_file_methods) 656 alias_method :rm, :delete 657 658 # method related FileTest 659 def_builtin_commands(FileTest, 660 FileTest.singleton_methods(false).collect{|m| [m, ["FILENAME"]]}) 661 662 end 663 664 end 665end 666