require 'thread'
#require 'rbconfig'
require 'ratch/core_ext'
require 'ratch/batch'
module Ratch
# TODO: Better base class?
class FileNotFound < StandardError
end
# = Shell Prompt class
#
# Ratch Shell object provides a limited file system shell in code.
# It is similar to having a shell prompt available to you in Ruby.
#
# NOTE: We have used the term *trace* in place of *verbose* for command
# line options. Even though Ruby itself uses the term *verbose* with respect
# to FileUtils, the term is commonly used for command specific needs, so we
# want to leave it open for such cases.
class Shell
# New Shell object.
#
# Shell.new(:noop=>true)
# Shell.new('..', :quiet=>true)
#
def initialize(*args)
path, opts = parse_arguments(*args)
opts.rekey!(&:to_sym)
set_options(opts)
if path.empty?
path = Dir.pwd
else
path = File.join(*path)
end
raise FileNotFound, "#{path}" unless ::File.exist?(path)
raise FileNotFound, "#{path}" unless ::File.directory?(path)
@_work = Pathname.new(path).expand_path
end
private
#
def parse_arguments(*args)
opts = (Hash===args.last ? args.pop : {})
return args, opts
end
#
def set_options(opts)
@_quiet = opts[:quiet]
@_noop = opts[:noop] || opts[:dryrun]
@_trace = opts[:trace] || opts[:dryrun]
#@_force = opts[:force]
end
#
def mutex
@mutex ||= Mutex.new
end
public
# Opertaton mode. This can be :noop, :verbose or :dryrun.
# The later is the same as the first two combined.
#def mode(opts=nil)
# return @mode unless opts
# opts.each do |key, val|
# next unless val
# case key
# when :noop
# @mode = (@mode == :verbose ? :dryrun : :noop)
# when :verbose
# @mode = (@mode == :noop ? :dryrun : :verbose)
# when :dryrun
# @mode = :dryrun
# end
# end
#end
def quiet? ; @_quiet ; end
def trace? ; @_trace ; end
def noop? ; @_noop ; end
#def force? ; @_force ; end
def dryrun? ; noop? && trace? ; end
# String representation is work directory path.
def to_s ; work.to_s ; end
# Two Shell's are equal if they have the same working path.
def ==(other)
return false unless other.is_a?(self.class)
return false unless work == other.work
true
end
# Same as #== except that #noop? must also be the same.
def eql?(other)
return false unless other.is_a?(self.class)
return false unless work == other.work
return false unless noop? == other.noop?
true
end
# Provides convenient starting points in the file system.
#
# root #=> #
# home #=> #
# work #=> #
#
# TODO: Replace these with Folio when Folio's is as capable.
# Current root path.
#def root(*args)
# Pathname['/', *args]
#end
# Current home path.
#def home(*args)
# Pathname['~', *args].expand_path
#end
# Current working path.
#def work(*args)
# Pathname['.', *args]
#end
# TODO: Should these take *args?
# Root location.
def root(*args)
dir('/', *args)
end
# Current home path.
def home(*args)
dir(File.expand_path('~'), *args)
end
# Current working path.
def work(*args)
return @_work if args.empty?
return dir(@_work, *args)
end
# Alias for #work.
#alias_method :pwd, :work
# Return a new prompt with the same location.
# NOTE: Use #dup or #clone ?
#def new ; Shell.new(work) ; end
def parent
dir('..')
end
#
#def [](name)
# #FileObject[localize(name)]
# #Pathname.new(localize(path))
# Pathlist.new(localize(path))
#end
# Returns a Batch of file +patterns+.
def batch(*patterns)
Batch.new patterns.map{|pattern| localize(pattern)}
end
# Returns a Batch of file +patterns+, without any exclusions.
def batch_all(*patterns)
Batch.all patterns.map{|pattern| localize(pattern)}
end
#
def path(path)
Pathname.new(localize(path))
end
alias_method :pathname, :path
#
def file(path)
#FileObject[name]
path = localize(path)
raise FileNotFound unless File.file?(path)
Pathname.new(path)
end
#def doc(name)
# Document.new(name)
#end
#
def dir(path)
#Directory.new(name)
path = localize(path)
raise FileNotFound unless File.directory?(path)
Pathname.new(path)
end
# Lists all entries.
def entries
work.entries
end
#alias_method :ls, :entries
# Lists directory entries.
def directory_entries
entries.select{ |d| d.directory? }
end
#
alias_method :dir_entries, :directory_entries
# Lists file entries.
def file_entries
entries.select{ |f| f.file? }
end
# Likes entries but omits '.' and '..' paths.
def pathnames
work.entries - %w{. ..}.map{|f|Pathname.new(f)}
end
# Returns list of directories.
def directories
pathnames.select{ |f| f.directory? }
end
alias_method :dirs, :directories
alias_method :folders, :directories
# Returns list of files.
def files
pathnames.select{ |f| f.file? }
end
# Glob pattern. Returns matches as strings.
def glob(*patterns, &block)
opts = (::Integer===patterns.last ? patterns.pop : 0)
matches = []
locally do
matches = patterns.map{ |pattern| ::Dir.glob(pattern, opts) }.flatten
end
if block_given?
matches.each(&block)
else
matches
end
end
# Glob files.
#def glob(*args, &blk)
# Dir.glob(*args, &blk)
#end
# TODO: Ultimately merge #glob and #multiglob.
def multiglob(*args, &blk)
Dir.multiglob(*args, &blk)
end
def multiglob_r(*args, &blk)
Dir.multiglob_r(*args, &blk)
end
=begin
# Match pattern. Like #glob but returns file objects.
# TODO: There is no FileObject any more. Should there be?
def match(*patterns, &block)
opts = (::Integer===patterns.last ? patterns.pop : 0)
patterns = localize(patterns)
matches = patterns.map{ |pattern| ::Dir.glob(pattern, opts) }.flatten
matches = matches.map{ |f| FileObject[f] }
if block_given?
matches.each(&block)
else
matches
end
end
=end
# Join paths.
# TODO: Should this return a new directory object? Or should it change directories?
def /(path)
#@_work += dir # did not work, why?
@_work = dir(localize(path))
self
end
# Alias for #/.
alias_method '+', '/'
# TODO: Tie this into the System class.
def system(cmd)
locally do
super(cmd)
end
end
# Shell runner.
def sh(cmd)
#puts "--> system call: #{cmd}" if trace?
puts cmd if trace?
return true if noop?
#locally do
if quiet?
silently{ system(cmd) }
else
system(cmd)
end
#end
end
# Shell runner.
#def sh(cmd)
# if dryrun?
# puts cmd
# true
# else
# puts "--> system call: #{cmd}" if trace?
# if quiet?
# silently{ system(cmd) }
# else
# system(cmd)
# end
# end
#end
# Change working directory.
#
# TODO: Make thread safe.
#
def cd(path, &block)
if block
work_old = @_work
begin
@_work = dir(localize(path))
locally(&block)
#mutex.synchronize do
# Dir.chdir(@_work){ block.call }
#end
ensure
@_work = work_old
end
else
@_work = dir(localize(path))
end
end
#
alias_method :chdir, :cd
# Bonus FileUtils features.
#def cd(*a,&b)
# puts "cd #{a}" if dryrun? or trace?
# fileutils.chdir(*a,&b)
#end
# -- File IO Shortcuts -----------------------------------------------
# Read file.
def read(path)
File.read(localize(path))
end
# Write file.
def write(path, text)
$stderr.puts "write #{path}" if trace?
File.open(localize(path), 'w'){ |f| f << text } unless noop?
end
# Append to file.
def append(path, text)
$stderr.puts "append #{path}" if trace?
File.open(localize(path), 'a'){ |f| f << text } unless noop?
end
#############
# FileTest #
#############
#
def size(path) ; FileTest.size(localize(path)) ; end
def size?(path) ; FileTest.size?(localize(path)) ; end
def directory?(path) ; FileTest.directory?(localize(path)) ; end
def symlink?(path) ; FileTest.symlink?(localize(path)) ; end
def readable?(path) ; FileTest.readable?(localize(path)) ; end
def chardev?(path) ; FileTest.chardev?(localize(path)) ; end
def exist?(path) ; FileTest.exist?(localize(path)) ; end
def exists?(path) ; FileTest.exists?(localize(path)) ; end
def zero?(path) ; FileTest.zero?(localize(path)) ; end
def pipe?(path) ; FileTest.pipe?(localize(path)) ; end
def file?(path) ; FileTest.file?(localize(path)) ; end
def sticky?(path) ; FileTest.sticky?(localize(path)) ; end
def blockdev?(path) ; FileTest.blockdev?(localize(path)) ; end
def grpowned?(path) ; FileTest.grpowned?(localize(path)) ; end
def setgid?(path) ; FileTest.setgid?(localize(path)) ; end
def setuid?(path) ; FileTest.setuid?(localize(path)) ; end
def socket?(path) ; FileTest.socket?(localize(path)) ; end
def owned?(path) ; FileTest.owned?(localize(path)) ; end
def writable?(path) ; FileTest.writable?(localize(path)) ; end
def executable?(path) ; FileTest.executable?(localize(path)) ; end
def safe?(path) ; FileTest.safe?(localize(path)) ; end
def relative?(path) ; FileTest.relative?(path) ; end
def absolute?(path) ; FileTest.absolute?(path) ; end
def writable_real?(path) ; FileTest.writable_real?(localize(path)) ; end
def executable_real?(path) ; FileTest.executable_real?(localize(path)) ; end
def readable_real?(path) ; FileTest.readable_real?(localize(path)) ; end
def identical?(path, other)
FileTest.identical?(localize(path), localize(other))
end
alias_method :compare_file, :identical?
# Assert that a path exists.
#def exists?(path)
# paths = Dir.glob(path)
# paths.not_empty?
#end
#alias_method :exist?, :exists? #; module_function :exist?
#alias_method :path?, :exists? #; module_function :path?
# Is a given path a regular file? If +path+ is a glob
# then checks to see if all matches are regular files.
#def file?(path)
# paths = Dir.glob(path)
# paths.not_empty? && paths.all?{ |f| FileTest.file?(f) }
#end
# Is a given path a directory? If +path+ is a glob
# checks to see if all matches are directories.
#def dir?(path)
# paths = Dir.glob(path)
# paths.not_empty? && paths.all?{ |f| FileTest.directory?(f) }
#end
#alias_method :directory?, :dir? #; module_function :directory?
#############
# FileUtils #
#############
# Low-level Methods Omitted
# -------------------------
# getwd -> pwd
# compare_file -> cmp
# remove_file -> rm
# copy_file -> cp
# remove_dir -> rmdir
# safe_unlink -> rm_f
# makedirs -> mkdir_p
# rmtree -> rm_rf
# copy_stream
# remove_entry
# copy_entry
# remove_entry_secure
# compare_stream
# Present working directory.
def pwd
work.to_s
end
# Same as #identical?
def cmp(a,b)
fileutils.compare_file(a,b)
end
#
def mkdir(dir, options={})
dir = localize(dir)
fileutils.mkdir(dir, options)
end
def mkdir_p(dir, options={})
dir = localize(dir)
unless File.directory?(dir)
fileutils.mkdir_p(dir, options)
end
end
alias_method :mkpath, :mkdir_p
def rmdir(dir, options={})
dir = localize(dir)
fileutils.rmdir(dir, options)
end
# ln(list, destdir, options={})
def ln(old, new, options={})
old = localize(old)
new = localize(new)
fileutils.ln(old, new, options)
end
alias_method :link, :ln
# ln_s(list, destdir, options={})
def ln_s(old, new, options={})
old = localize(old)
new = localize(new)
fileutils.ln_s(old, new, options)
end
alias_method :symlink, :ln_s
def ln_sf(old, new, options={})
old = localize(old)
new = localize(new)
fileutils.ln_sf(old, new, options)
end
# cp(list, dir, options={})
def cp(src, dest, options={})
src = localize(src)
dest = localize(dest)
fileutils.cp(src, dest, options)
end
alias_method :copy, :cp
# cp_r(list, dir, options={})
def cp_r(src, dest, options={})
src = localize(src)
dest = localize(dest)
fileutils.cp_r(src, dest, options)
end
# mv(list, dir, options={})
def mv(src, dest, options={})
src = localize(src)
dest = localize(dest)
fileutils.mv(src, dest, options)
end
alias_method :move, :mv
def rm(list, options={})
list = localize(list)
fileutils.rm(list, options)
end
alias_method :remove, :rm
def rm_r(list, options={})
list = localize(list)
fileutils.rm_r(list, options)
end
def rm_f(list, options={})
list = localize(list)
fileutils.rm_f(list, options)
end
def rm_rf(list, options={})
list = localize(list)
fileutils.rm_rf(list, options)
end
def install(src, dest, mode, options={})
src = localize(src)
dest = localize(dest)
fileutils.install(src, dest, mode, options)
end
def chmod(mode, list, options={})
list = localize(list)
fileutils.chmod(mode, list, options)
end
def chmod_r(mode, list, options={})
list = localize(list)
fileutils.chmod_r(mode, list, options)
end
#alias_method :chmod_R, :chmod_r
def chown(user, group, list, options={})
list = localize(list)
fileutils.chown(user, group, list, options)
end
def chown_r(user, group, list, options={})
list = localize(list)
fileutils.chown_r(user, group, list, options)
end
#alias_method :chown_R, :chown_r
def touch(list, options={})
list = localize(list)
fileutils.touch(list, options)
end
#
# TODO: should this have SOURCE diectory?
# stage(directory, source_dir, files)
#
def stage(stage_dir, files)
#dir = localize(directory)
#files = localize(files)
locally do
fileutils.stage(stage_dir, work, files)
end
end
# An intergrated glob like method that takes a set of include globs,
# exclude globs and ignore globs to produce a collection of paths.
#
# Ignore_globs differ from exclude_globs in that they match by
# the basename of the path rather than the whole pathname.
#
def amass(include_globs, exclude_globs=[], ignore_globs=[])
locally do
fileutils.amass(include_globs, exclude_globs, ignore_globs)
end
end
#
def outofdate?(path, *sources)
#fileutils.outofdate?(localize(path), localize(sources)) # DIDN'T WORK, why?
locally do
fileutils.outofdate?(path, sources.flatten)
end
end
# # Does a path need updating, based on given +sources+?
# # This compares mtimes of give paths. Returns false
# # if the path needs to be updated.
# #
# # TODO: Put this in FileTest instead?
#
# def out_of_date?(path, *sources)
# return true unless File.exist?(path)
#
# sources = sources.collect{ |source| Dir.glob(source) }.flatten
# mtimes = sources.collect{ |file| File.mtime(file) }
#
# return true if mtimes.empty? # TODO: This the way to go here?
#
# File.mtime(path) < mtimes.max
# end
#
def uptodate?(path, *sources)
locally do
fileutils.uptodate?(path, sources.flatten)
end
end
#
#def uptodate?(new, old_list, options=nil)
# new = localize(new)
# old = localize(old_list)
# fileutils.uptodate?(new, old, options)
#end
=begin
# TODO: Deprecate these?
# Assert that a path exists.
def exists!(*paths)
abort "path not found #{path}" unless paths.any?{|path| exists?(path)}
end
alias_method :exist!, :exists! #; module_function :exist!
alias_method :path!, :exists! #; module_function :path!
# Assert that a given path is a file.
def file!(*paths)
abort "file not found #{path}" unless paths.any?{|path| file?(path)}
end
# Assert that a given path is a directory.
def dir!(*paths)
paths.each do |path|
abort "Directory not found: '#{path}'." unless dir?(path)
end
end
alias_method :directory!, :dir! #; module_function :directory!
=end
#private ?
# Returns a path local to the current working path.
def localize(local_path)
# some path arguments are optional
return local_path unless local_path
#
case local_path
when Array
local_path.collect do |lp|
if absolute?(lp)
lp
else
File.expand_path(File.join(work.to_s, lp))
end
end
else
# do not localize an absolute path
return local_path if absolute?(local_path)
File.expand_path(File.join(work.to_s, local_path))
#(work + local_path).expand_path.to_s
end
end
# Change directory to the shell's work directory,
# process the +block+ and then return to user directory.
def locally(&block)
if work.to_s == Dir.pwd
block.call
else
mutex.synchronize do
#work.chdir(&block)
Dir.chdir(work, &block)
end
end
end
# TODO: Should naming policy be in a utility extension module?
#
#
def naming_policy(*policies)
if policies.empty?
@naming_policy ||= ['down', 'ext']
else
@naming_policy = policies
end
end
#
#
def apply_naming_policy(name, ext)
naming_policy.each do |policy|
case policy.to_s
when /^low/, /^down/
name = name.downcase
when /^up/
name = name.upcase
when /^cap/
name = name.capitalize
when /^ext/
name = name + ".#{ext}"
end
end
name
end
private
# Returns FileUtils module based on mode.
def fileutils
if dryrun?
::FileUtils::DryRun
elsif noop?
::FileUtils::Noop
elsif trace?
::FileUtils::Verbose
else
::FileUtils
end
end
# This may be used by script commands to allow for per command
# noop and trace options. Global options have precedence.
def util_options(options)
noop = noop? || options[:noop] || options[:dryrun]
trace = trace? || options[:trace] || options[:dryrun]
return noop, trace
end
public#class
def self.[](path)
new(path)
end
end
end