# Refinements # ======================================================================= using NRSER using NRSER::Types # Definitions # ======================================================================= module NRSER::Rash # @!group Utility Class Methods # ========================================================================== def self.log_error(headline, error, message='') logger.error "#{ headline }:\n" + "#{ error } (#{ error.class })\n\t" + error.backtrace.join("\n\t") + "\n#{ message}" end # substitute stuff into a shell command after escaping with # `Shellwords.escape`. # # arguments after the first may be multiple values that will # be treated like a positional list for substitution, or a single # hash that will be treated like a key substitution. # # any substitution value that is an Array will be treated like a list of # path segments and joined with `File.join`. def self.sub command, *subs quoted = if subs.length == 1 && subs[0].is_a?(Hash) Hash[ subs[0].map do |key, sub| sub = File.join(*sub) if sub.is_a? Array # shellwords in 1.9.3 can't handle symbols sub = sub.to_s if sub.is_a? Symbol [key, Shellwords.escape(sub)] end ] else subs.map do |sub| sub = File.join(*sub) if sub.is_a? Array # shellwords in 1.9.3 can't handle symbols sub = sub.to_s if sub.is_a? Symbol Shellwords.escape sub end end command % quoted end # shortcut for `Shellwords.escape` def self.esc *args Shellwords.escape *args end # execute a system command, using shellwords to escape substituted values. # # arguments after the first may be multiple values that will # be treated like a positional list for substitution, or a single # hash that will be treated like a key substitution. # # any substitution value that is an Array will be treated like a list of # path segments and joined with `File.join`. def self.sys command, *subs cmd = sub command, *subs output = `#{ cmd }` if $?.exitstatus == 0 return output else raise SystemCallError.new( "cmd `#{ cmd }` failed with status #{ $?.exitstatus }", $?.exitstatus ) end end # just runs a command and return true if it exited with status 0 def self.sys? command, *subs cmd = sub command, *subs `#{ cmd }` $?.exitstatus == 0 end # run and ignore exit code def self.sys! command, *subs cmd = sub command, *subs `#{ cmd }` end def self.chdir path, &block path = File.expand_path path Dir.chdir path do block.(path) end end # finds direct submodules of `file_path` and requires them. # assumes they are in a folder with the same name as the file, e.g. # # /rash/functions/chinese.rb # /rash/functions/chinese/*.rb # # call it like: # # NRSER::Rash.require_submodules __FILE__ # def self.require_submodules file_path, deep: false file_path = file_path.to_pn file_dir = file_path.dirname mod_dir = file_dir / file_path.basename( '.rb' ) glob = if deep mod_dir / '**' / '*.rb' else mod_dir / '*.rb' end logger.trace "Requiring submodules", file_path: file_path, file_dir: file_dir, mod_dir: mod_dir, glob: glob Pathname.glob( glob ).each { |abs_path| rel_path = abs_path.relative_path_from file_dir req_path = rel_path.dirname / rel_path.basename( '.rb' ) logger.trace "Requiring file", abs_path: abs_path, rel_path: rel_path, req_path: req_path begin require abs_path.to_s rescue Exception => error if NRSER::Rash.config( 'DEBUG', false ) raise error else logger.warn "Failed to load file #{ abs_path.to_s.inspect }", error end end } end def self.stdin return $stdin.read unless $stdin.tty? end # splits up string args that represent what i'm calling a "path to a # reference" in Ruby. by example: # # NRSER::Rash.ref_path('NRSER::Rash::Functions.blah') # # -> ['NRSER', 'Rash', 'Functions', 'blah'] def self.ref_path(*args) args.map {|part| part.split('::').map {|_| _.split('.')} }.flatten end # find the common indent of each line of a string. # ignores lines that are all whitespace (`/^\s*$/`), which takes care # or newlines at the end of files, and prevents the method from # not working due to whitespace weirdness that you can't see. # # note that inconsistent whitespace will still cause problems since # there isn't anyway to guess the tabstop equating spaces and tabs. def self.find_indent(string) string.lines.reject {|line| # don't consider blank lines line =~ /^[\ \t]*$/ }.map {|line| # get the indent off each non-blank line line.match(/^([\ \t]*)/)[1] }.sort.reduce {|first, indent| return '' unless indent[0...(first.length)] == first first } end def self.dedent(string = NRSER::Rash.stdin) indent = find_indent(string) # short-circuit if there is no indent return string if indent == '' # process the lines new_lines = string.lines.map do |line| # does the line start with the indent? if line[0...(indent.length)] == indent # it does, so chop it off line[(indent.length)..(line.length)] elsif line =~ /^\s+$/ # it does not start with the indent. we're going to let this slide # if it's only whitespace line else # we shouldn't get here if `find_indent` is doing it's job # i guess report an error and return the string? logger.error "should not be so" return string end end new_lines.join end # @!endgroup Utility Class Methods class Writer < IO def initialize(options = {}) options = {:out => $stdout, :spacer => ' '}.merge options @spacer = options[:spacer] @level = 0 @new_line = true @out = options[:out] super options[:out].fileno end attr_accessor :out def indent(&b) @level += 1 if b yield dedent end end def dedent @level -= 1 end def block(start = nil, finish = nil, &body) [start].flatten.each {|line| puts line } if start indent do yield end [finish].flatten.each {|line| puts line} if finish end # add the indent, unless it's called with a newline (`puts` calls # write twice for every line, with the second call being "\n") def write(obj) if @new_line && @level > 0 super indent_string end str = obj.to_s @new_line = (str[-1] == "\n") super str.lines.to_a.join(indent_string) end private def indent_string @spacer * @level end end # class Writer end # module NRSER::Rash