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