find.rbにみるRubyのthrow/catchの大域脱出の例
Rubyのthrow/catchの大域脱出って、あまり見かけない気がしているけど、どういうときに使うのかなとぼんやり思っていたら、標準添付の lib/find.rbになるほどと思う例が見つかった。
# # find.rb: the Find module for processing all files under a given directory. # # # The +Find+ module supports the top-down traversal of a set of file paths. # # For example, to total the size of all files under your home directory, # ignoring anything in a "dot" directory (e.g. $HOME/.ssh): # # require 'find' # # total_size = 0 # # Find.find(ENV["HOME"]) do |path| # if FileTest.directory?(path) # if File.basename(path)[0] == ?. # Find.prune # Don't look any further into this directory. # else # next # end # else # total_size += FileTest.size(path) # end # end # module Find # # Calls the associated block with the name of every file and directory listed # as arguments, then recursively on their subdirectories, and so on. # # Returns an enumerator if no block is given. # # See the +Find+ module documentation for an example. # def find(*paths) # :yield: path block_given? or return enum_for(__method__, *paths) paths.collect!{|d| raise Errno::ENOENT unless File.exist?(d); d.dup} while file = paths.shift catch(:prune) do yield file.dup.taint begin s = File.lstat(file) rescue Errno::ENOENT, Errno::EACCES, Errno::ENOTDIR, Errno::ELOOP, Errno::ENAMETOOLONG next end if s.directory? then begin fs = Dir.entries(file) rescue Errno::ENOENT, Errno::EACCES, Errno::ENOTDIR, Errno::ELOOP, Errno::ENAMETOOLONG next end fs.sort! fs.reverse_each {|f| next if f == "." or f == ".." f = File.join(file, f) paths.unshift f.untaint } end end end end # # Skips the current file or directory, restarting the loop with the next # entry. If the current file is a directory, that directory will not be # recursively entered. Meaningful only within the block associated with # Find::find. # # See the +Find+ module documentation for an example. # def prune throw :prune end module_function :find, :prune end
Find.findでpathsを順繰りにyieldしている最中に途中でプツンと処理を切って、次のpathを使ってブロックを起動するには、Find.pruneを使う。これはクラスメソッドというかモジュール関数だけど、呼び先のfind.rbでは throw :prune として大域脱出してる。グローバルなクラス定数のメソッドで処理の流れをゴリッと変えるっていうのは、やや荒っぽい印象も受けるけど、スクリプト言語っぽいなと思った。ていうか、それが大域脱出というものか。あれ、じゃあ、Find.findしてないときにFind.pruneしたら? と思ったら、
> Find.prune ArgumentError: uncaught throw :prune
となるだけのことだった。
それより、? の単項演算子に戸惑った。
# if File.basename(path)[0] == ?.
文脈からすると、右辺はカレントディレクトリを示すピリオドのことかと思ったけど、実際、これは1文字からなる文字リテラルだった。そんなもんあったっけな……。Emacs Lisp由来なのかな。メタ文字も書ける。でも素直に "." と書くほうが分かりやすいと思う。
$ irb [1] pry(main)> ?. => "." [2] pry(main)> ?a => "a" [3] pry(main)> ?1 => "1" [4] pry(main)> ?abc SyntaxError: unexpected '?', expecting $end [4] pry(main)> ?\s => " " [5] pry(main)> ?\t => "\t" [6] pry(main)> ?\x20 => " "