Module: NRSER::Rash

Includes:
SemanticLogger::Loggable
Defined in:
lib/nrser/rash/functions.rb,
lib/nrser/rash/version.rb,
lib/nrser/rash/testing.rb,
lib/nrser/rash/config.rb,
lib/nrser/rash/util.rb,
lib/nrser/rash.rb

Overview

Definitions

Defined Under Namespace

Modules: CLI, Config, Formatters, Functions, Helpers, Testing Classes: Writer

Constant Summary collapse

VERSION =

The current version.

Returns:

  • (String)
'0.2.3'
ROOT =

Absolute, expanded path to the gem's root directory.

Returns:

  • (Pathname)
( Pathname.new(__FILE__ ).dirname / '..' / '..' ).expand_path
COMMAND_LINE_CONFIG =
{}
ROOT_DIR =

Rash's project dir

File.expand_path(File.dirname(__FILE__) + '/..')
EXECUTABLE =

Executable

ROOT_DIR + '/bin/rash'

Rash Function Class Methods collapse

Testing Class Methods collapse

Utility Class Methods collapse

Class Method Summary collapse

Class Method Details

._test_function(module_or_function_name) ⇒ Object

TODO:

It seems like testing doesn't work in the state I came back to it 2018.02.22

runs tests declared for functions using the _test_case and _test_functions macros. this is hackety-hack-hackety.

rational:

i'm using Test::Unit. because it comes with the stdlib. Test::Unit looks to be based on [MiniTest][https://github.com/seattlerb/minitest], but i can't find great docs on using that either, and i though the Test::Unit API might be better known, so i went with that.

the only way i can really find using Test::Unit documented is to require 'test/unit', which runs all declared Test::Unit::TestCase subclasses, reading options off the command line and spitting results back to STDOUT.

there is an optional --name <pattern> arg that you can supply on the command line when invoking a script with a require 'test/unit' line in it, but it seems like it only matches against method names, not TestCasesubclasses or anything. under this assumption, the necessary pattern would need to be a part of all test method names. and i don't really want a ton of method names liketest_project_dir_name_space_sub so that i can turnrash test Project.dir_name` into a pattern that runs the NRSER::Rash::Functions::Project.dir_name tests.

the hack:

this gets around it by providing macros that will dynamically define Test::Unit::TestCase subclasses if and only if those tests are being run.

NOTE: why does this start with an '_'?



44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
# File 'lib/nrser/rash/testing.rb', line 44

def self._test_function(module_or_function_name)
  # if the *first* arg to `rash test` is `--all`, then all test will be run,
  # which is done by supplying an empty `module_or_function_name` to
  # `Testing.start`.
  #
  # TODO: I just realized that it's currently impossible to test all the
  #       functions defined in {NRSER::Rash::Functions} without testing all
  #       the submodules as well. ugh. solutions include requiring an
  #       explicit (never finished I guess...)
  #       
  #       
  if module_or_function_name == '--all'
    module_or_function_name = ''
  end
  NRSER::Rash::Testing.start module_or_function_name
  # going to need the functions loaded to find it
  load_functions
  if NRSER::Rash::Testing._test_cases.length == 0
    raise NRSER::Rash::Testing::NoTestCasesError.new(module_or_function_name)
  end
  require 'minitest/autorun'
end

.chdir(path, &block) ⇒ Object



92
93
94
95
96
97
# File 'lib/nrser/rash/util.rb', line 92

def self.chdir path, &block
  path  = File.expand_path path
  Dir.chdir path do
    block.(path)
  end
end

.config(name, *default) ⇒ Object

TODO:

This example is out of date...

Get a config value by name. First looks for an env var defined with an added 'RASH_' prefix, then for a const in the NRSER::Rash module with the provided name.

Easiest to explain by example:

NRSER::Rash::config('BLAH')

will first see if there is an env var named

'RASH_BLAH'

defined, and return it's value if so.

Otherwise, it will try and return

NRSER::RASH::Config::BLAH

raising an exception if it's not found.

Defined here so it can be used when defining the NRSER::Rash::Config::<name> consts.



58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
# File 'lib/nrser/rash/config.rb', line 58

def self.config name, *default
  if default.length > 1
    raise ArgumentError,
      "wrong number of arguments" \
      "(given #{ 1 + default.length }, expected 1-2)"
  end
  
  # command line config options take precedent (those that start with
  # '--RASH_', which are never passed to the function)
  if COMMAND_LINE_CONFIG.key? name
    COMMAND_LINE_CONFIG[name]
  elsif ENV.key? "RASH_#{ name }"
    ENV["RASH_#{ name }"]
  else
    begin
      Config.const_get(name)
    rescue NameError => e
      if default.empty?
        raise
      else
        default[0]
      end
    end
  end
end

.conflict?(name) ⇒ Boolean

TODO: doesn't work. was try to prevent overriding non-rash functions (without some yet to be implemented way of doing so explicitly) 'cause this can cause accidental CHAOS with other scripts

Returns:

  • (Boolean)


50
51
52
53
54
55
56
57
58
59
# File 'lib/nrser/rash/functions.rb', line 50

def self.conflict? name
  return false
  type = `type #{ name } 2>&1`
  if name == 'blah'
    raise type
  end
  return false if $?.exitstatus != 0
  return true
  not type =~ /rash\ call/
end

.dedent(string = NRSER::Rash.stdin) ⇒ Object



183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
# File 'lib/nrser/rash/util.rb', line 183

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

.esc(*args) ⇒ Object

shortcut for Shellwords.escape



54
55
56
# File 'lib/nrser/rash/util.rb', line 54

def self.esc *args
  Shellwords.escape *args
end

.falsy?(string) ⇒ Boolean

Proxy to NRSER.falsy?

Returns:

  • (Boolean)


150
151
152
# File 'lib/nrser/rash/config.rb', line 150

def self.falsy? string
  return string.falsy?
end

.filter_and_set_config(args) ⇒ Object



158
159
160
161
162
163
164
165
166
167
168
169
170
# File 'lib/nrser/rash/config.rb', line 158

def self.filter_and_set_config(args)
  args.reject! do |arg|
    # long options of form '--<name>=<value>'
    if m = arg.match(/\A--RASH_([^=]*)(=?)(.*)/)
      COMMAND_LINE_CONFIG[m[1]] = if m[2] == ''
        true
      else
        m[3]
      end
      true
    end
  end
end

.find_indent(string) ⇒ Object

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.



170
171
172
173
174
175
176
177
178
179
180
181
# File 'lib/nrser/rash/util.rb', line 170

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

.function_mapObject

stub out all the singleton methods under Functions



103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
# File 'lib/nrser/rash/functions.rb', line 103

def self.function_map
  results = []
  # method to recursively stub singleton functions in a module
  from_module = lambda do |mod|
    # when the name is split, 'Rash' and 'Functions' take up the
    # first two elements, so drop those to get the namespace
    # as an array of strings
    namespace = mod.name.split('::').drop(2)
    # get each singleton method (`def self.*` style)
    mod.singleton_methods.select {|method_name|
      # throw out those that should not be stubbed
      stub_function? mod, method_name
    }.each do |method_name|

      # the function name is the namespace plus the method name
      # with segments seperated by `'.'`, ex:
      #
      #     namespace       = ['Nrser', Url']
      #     method_name     = 'parse'
      #     function_name   = 'Nrser.Url.parse'
      #
      # funcs << (namespace.clone << method_name)
      function_name = stub_name namespace, method_name

      call_arg = if namespace.empty?
        method_name.to_s
      else
        "#{ namespace.join('.') }.#{ method_name }"
      end

      results << {'name' => function_name, 'sig' => call_arg}
    end

    # descend into submodules
    mod.constants.map {|name|
      mod.const_get(name)
    }.select {|const|
      # exclude any TestCase classes
      # (const.is_a? Module) && (not const < Minitest::Test)
      submodule? const
    }.each {|submod|
      from_module.call submod
    }
  end

  from_module.call NRSER::Rash::Functions
  results
end

.function_namesObject

Stub out all the singleton methods under Functions



62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
# File 'lib/nrser/rash/functions.rb', line 62

def self.function_names
  call_args = []
  # method to recursivly stub singleton functions in a module
  from_module = -> mod do
    # when the name is split, 'Rash' and 'Functions' take up the
    # first two elements, so drop those to get the namespace
    # as an array of strings
    namespace = mod.name.split( '::' ).drop( 3 )
    # get each singleton method (`def self.*` style)
    mod.singleton_methods.select {|method_name|
      # throw out those that should not be stubbed
      stub_function? mod, method_name
    }.each do |method_name|

      call_arg = if namespace.empty?
        method_name.to_s
      else
        "#{ namespace.join('.') }.#{ method_name }"
      end

      call_args << call_arg.downcase
    end

    # descend into submodules
    mod.constants.map {|name|
      mod.const_get(name)
    }.select {|const|
      # exclude any TestCase classes
      # (const.is_a? Module) && (not const < Minitest::Test)
      submodule? const
    }.each {|submod|
      from_module.call submod
    }
  end # from_module

  from_module.call NRSER::Rash::Functions
  call_args
end

.load_functionsObject

==========================================================================



21
22
23
24
# File 'lib/nrser/rash/functions.rb', line 21

def self.load_functions
  path = config('FUNCTIONS').to_pn
  require path
end

.log_error(headline, error, message = '') ⇒ Object

==========================================================================



16
17
18
19
20
# File 'lib/nrser/rash/util.rb', line 16

def self.log_error(headline, error, message='')
  logger.error  "#{ headline }:\n" +
                "#{ error } (#{ error.class })\n\t" +
                error.backtrace.join("\n\t") + "\n#{ message}"
end

.ref_path(*args) ⇒ Object

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']


157
158
159
160
161
# File 'lib/nrser/rash/util.rb', line 157

def self.ref_path(*args)
  args.map {|part|
    part.split('::').map {|_| _.split('.')}
  }.flatten
end

.require_submodules(file_path, deep: false) ⇒ Object

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__


109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
# File 'lib/nrser/rash/util.rb', line 109

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

.stdinObject



148
149
150
# File 'lib/nrser/rash/util.rb', line 148

def self.stdin
  return $stdin.read unless $stdin.tty?
end

.stub_function?(mod, method_name) ⇒ Boolean

Returns:

  • (Boolean)


27
28
29
30
31
# File 'lib/nrser/rash/functions.rb', line 27

def self.stub_function?(mod, method_name)
  return false unless mod.method(method_name)
  return false if method_name.to_s[0] == '_'[0]
  true
end

.stub_name(namespace, method_name) ⇒ Object



34
35
36
37
38
39
# File 'lib/nrser/rash/functions.rb', line 34

def self.stub_name namespace, method_name
  (
    (namespace.map {|s| s.downcase}) +
    [method_name]
  ).join(config('STUB_NAMESPACE_SEPERATOR'))
end

.sub(command, *subs) ⇒ Object

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.



32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
# File 'lib/nrser/rash/util.rb', line 32

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

.submodule?(obj) ⇒ Boolean

Returns:

  • (Boolean)


42
43
44
# File 'lib/nrser/rash/functions.rb', line 42

def self.submodule? obj
  obj.is_a?(Module) && (not obj.is_a?(Class))
end

.sys(command, *subs) ⇒ Object

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.



66
67
68
69
70
71
72
73
74
75
76
77
# File 'lib/nrser/rash/util.rb', line 66

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

.sys!(command, *subs) ⇒ Object

run and ignore exit code



87
88
89
90
# File 'lib/nrser/rash/util.rb', line 87

def self.sys! command, *subs
  cmd = sub command, *subs
  `#{ cmd }`
end

.sys?(command, *subs) ⇒ Boolean

just runs a command and return true if it exited with status 0

Returns:

  • (Boolean)


80
81
82
83
84
# File 'lib/nrser/rash/util.rb', line 80

def self.sys? command, *subs
  cmd = sub command, *subs
  `#{ cmd }`
  $?.exitstatus == 0
end

.truthy?(string) ⇒ Boolean

Proxy to NRSER.truthy?

Returns:

  • (Boolean)


140
141
142
# File 'lib/nrser/rash/config.rb', line 140

def self.truthy? string
  NRSER.truthy? string
end